DocumentsProduct CategoriesPerfect Assistant Deployment for Swift 3.1
Perfect Assistant Deployment for Swift 3.1
Jun 28, 2024
()) {
callback(.continue)
}
func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
if case .notFound = response.status {
response.bodyBytes.removeAll()
response.setBody(string: "The file \(response.request.path) was not found.")
response.setHeader(.contentLength, value: "\(response.bodyBytes.count)")
callback(.done)
} else {
callback(.continue)
}
}
}
try HTTPServer(documentRoot: webRoot)
.setResponseFilters([(Filter404(), .high)])
.start(port: 8181)
Getting Started From Scratch
This guide will take you through the steps of settings up a simple HTTP server from scratch with Swift and Perfect.
Prerequisites
Swift 3.0
After you have installed a Swift 3.0 toolchain from Swift.org, open up a terminal window and type swift --version
It will produce a message similar to this one:
Apple Swift version 3.0.1 (swiftlang-800.0.58.6 clang-800.0.42.1)
Target: x86_64-apple-macosx10.9
Make sure you are running the release version of Swift 3.0.1. Perfect will not compile successfully if you are running a version of Swift that is lower than 3.0.1.
You can find out which version of Swift you will need by looking in the README of the main Perfect repo.
OS X
Everything you need is already installed.
Ubuntu Linux
Perfect runs in Ubuntu Linux 14.04, 15.10 and 16.04 environments. Perfect relies on OpenSSL, libssl-dev, and uuid-dev. To install these, in the terminal, type:
sudo apt-get install openssl libssl-dev uuid-dev
Getting Started with Perfect
Now that you’re ready to build a simple web application from scratch, then go ahead and create a new folder where you will keep your project files:
mkdir MyAwesomeProject
cd MyAwesomeProject
As a good developer practice, make this folder a git repo:git init
touch README.md
git add README.md
git commit -m "Initial commit"
It''s also recommended to add a .gitignore similar to the contents of this Swift .gitignore template from gitignore.io.
Create the Swift Package
Now create a Package.swift file in the root of the repo with the following content. This is needed for the Swift Package Manager (SPM) to build the project.
import PackageDescription
let package = Package(
name: "MyAwesomeProject",
dependencies: [
.Package(
url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git",
majorVersion: 2
)
]
)
Next create a folder called Sources and create a main.swift in there:
mkdir Sources
echo ''print("Well hi there!")'' >> Sources/main.swift
Now the project is ready to be built and run by running by the following two commands:
swift build
.build/debug/MyAwesomeProject
You should see the following output:
Well hi there!
Setting up the server
Now that the Swift package is up and running, the next step is to implement the Perfect-HTTPServer. Open up the Sources/main.swift and change its content the following:import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
// Create HTTP server.
let server = HTTPServer()
// Register your own routes and handlers
var routes = Routes()
routes.add(method: .get, uri: "/", handler: {
request, response in
response.setHeader(.contentType, value: "text/html")
response.appendBody(string: "Hello, world!Hello, world!")
response.completed()
}
)
// Add the routes to the server.
server.addRoutes(routes)
// Set a listen port of 8181
server.serverPort = 8181
do {
// Launch the HTTP server.
try server.start()
} catch PerfectError.networkError(let err, let msg) {
print("Network error thrown: \(err) \(msg)")
}
Build and run the project again with:
swift build
.build/debug/MyAwesomeProject
The server is now running and waiting for connections. Access http://localhost:8181/ to see the greeting. Hit "control-c" to terminate the server.
Xcode
Swift Package Manager (SPM) can generate an Xcode project which can run the PerfectTemplate server and provide full source code editing and debugging for your project. Enter the
following in your terminal:
swift package generate-xcodeproj
Open the generated file "PerfectTemplate.xcodeproj" and add the following to the "Library Search Paths" for the project (not just the target):
$(PROJECT_DIR) - Recursive
Ensure that you have selected the executable target and selected it to run on "My Mac". Also ensure that the correct Swift toolchain is selected. You can now run and debug the server
directly in Xcode.
An HTTP and Web Services Primer
Most of the web as we know it, and as applies to the common usage of server side development, is built upon only a few main technologies.
This document will attempt to provide an overview of some of these as they relate to Perfect, server-side Swift, and general application programming interface (API) development.
What Is an API?
An API is a bridge between two systems. It generally accepts a standardized type and style of input, transforms it internally, and works with another system to perform an action or return
information.
An example of an API is GitHub''s API: most will be familiar with using GitHub''s web interface, but they also have an API which allows applications such as task and issue management
systems like Jira and continuous integration (CI) systems such as Bamboo to link directly to your repositories to provide greater functionality than any one system could by itself.
An API like these are generally built upon HTTP or HTTPS.HTTP and HTTPS
"HTTP" is the common acronym for "HyperText Transfer Protocol". It is is a set of standards that allows users to publish multimedia documents on the internet and exchange information
they find on webpages.
"HTTPS" is a protocol for secure, encrypted HTTP communications. Each end of the communication agrees on a trusted "key" that encrypts the information being transmitted, with the
ability to decrypt it on receipt. The more complex the security, the harder it is for a third party to intercept and read it, and potentially change it.
When you access a website in your browser, it will likely be over HTTP, or if the "lock" shows it will be using HTTPS.
When an iOS or Android application accesses a backend server to get information such as the most recent weather report, it is using HTTP or, more likely, HTTPS.
The General Form of an API
Routes
An API consists of "routes", which are similar to directory/folder name paths through a file system to a document. Each route points to a different action you wish to perform. A "show me
a list of users" route is different from a "create a new user" route.
In an API, these routes will often not exist as actual directories and documents, but as pointers to functions. The "directory structure" implied is usually a way of logically grouping
functionality. For example:
/api/v1/users/list
/api/v1/users/detail
/api/v1/users/create
/api/v1/users/modify
/api/v1/users/delete
/api/v1/companies/list
/api/v1/companies/detail
/api/v1/companies/create
/api/v1/companies/modify
/api/v1/companies/delete
This example illustrates a typical "CRUD" system. "CRUD" means "Create, Read, Update, Delete". The difference between the two groups seems to only be the users versus
companies part, but they will point to different functions.
Additionally, the "detail", "modify", and "delete" routes will include a unique identifier for the record. For example:
/api/v1/users/list
/api/v1/users/detail?id=5d096846-a000-43db-b6c5-a5883135d71d
/api/v1/users/create
/api/v1/users/modify?id=5d096846-a000-43db-b6c5-a5883135d71d
/api/v1/users/delete?id=5d096846-a000-43db-b6c5-a5883135d71d
The example above shows these routes passing to the server an id parameter that relates to a specific record. The list and create routes don''t have an id because for those routes an id
parameter is irrelevant.
In addition to routes, a request to each of these individual routes will include an HTTP "verb".
HTTP Verbs
HTTP verbs are additional pieces of information that a web browser or mobile application client supplies with every request to a route. These verbs can give additional "context" to the
API server as to the nature of the data being received.
Common HTTP verbs include:
GET
The GET method requests a specified route, and the only parameters passed from client to server are in the URL. Requests using GET should only retrieve data and have no other
effect. Using a GET request to delete a database record is possible but it is not recommended.
POST
Normally used for sending info to create a new record in a database, a POST request is what normally gets submitted when you fill in a form on a website. The name-value pairs of the
data are submitted in a POST request''s "POST Body" and are read by the API server as discrete pairs.PATCH
PATCH requests are generally considered to be the same as a POST but they are used for updates.
PUT
Used predominantly for file uploads, a PUT request will include a file in the body of the request sent to the server.
DELETE
A DELETE request is the most descriptive of all the HTTP verbs. When sending a DELETE request you are instructing the API server to remove a specific resource. Usually some form
of uniquely identifying information is included in the URL.
Using HTTP Verbs and Routes to Simplify an API
When used together, these two components of a request can reduce the perceived complexity of an API.
Looking at the following structure with the HTTP verb followed by a URL, you will see that the process is much simpler and more specific:
GET /api/v1/users
GET /api/v1/users/5d096846-a000-43db-b6c5-a5883135d71d
POST /api/v1/users
PATCH /api/v1/users/5d096846-a000-43db-b6c5-a5883135d71d
DELETE /api/v1/users/5d096846-a000-43db-b6c5-a5883135d71d
At first glance, all seem to be pointing to the same route: /api/v1/users . However, each route performs a different action. Similarly, the difference between the first two GET routes
is the id appended as a URL component, not a parameter as shown in the earlier example. This command is usually achieved with a "wildcard" that is specified in the route setup.
The next step is to review the Handling Requests chapters in the Perfect documentation. These sections will outline how to implement routes pointing to functions and methods, how to
access information passed to the API server from a frontend, and whether it be a web browser or a mobile application.
Repository Layout
The Perfect framework has been divided into several repositories to make it easy for you to find, download, and install the components you need for your project:
The Perfect Core Library
Perfect - This repository contains the core PerfectLib and will continue to be the main landing point for the project
The Perfect Toolkit
There are many components in the main Perfect Repo, https://github.com/PerfectlySoft that make up the comprehensive Perfect Toolkit. There are database drivers, utilities, session
management, and authentication systems. All components are documented here.
The Perfect Template
PerfectTemplate - A simple starter project which compiles with the Swift Package Manager into a standalone executable HTTP server. This repository is ideal for starting on your own
Perfect-based project
The Perfect Documentation - Open Source
PerfectDocs - Contains all API reference-related material
Perfect Examples
PerfectExamples - All the Perfect example projects and documentation
StORM - A Swift ORM
StORM is a Swift ORM, written in Perfect. The list of supported databases will continue to grow and mature.Perfect Servers
A collection of standalone servers written in Perfect, ready for deployment (with a little configuration on your part!)# Building with Swift Package Manager
Swift Package Manager (SPM) is a command-line tool for building, testing, and managing dependencies for Swift projects. All of the components in Perfect are designed to build with
SPM. If you create a project with Perfect, it will need to use SPM as well.
The best way to start a new Perfect project is to fork or clone the PerfectTemplate. It will give you a very simple "Hello, World!" server message which you can edit and modify however
you wish.
Before beginning, ensure you have read the dependencies document, and that you have a functioning Swift 3.0 toolchain for your platform.
The next step is to clone the template project. Open a new command-line terminal and change directory (cd) where you want the project to be cloned. Type the following into your
terminal to download a directory called “PerfectTemplate”:
git clone https://github.com/PerfectlySoft/PerfectTemplate.git
Within the Perfect Template, you will find two important file items:
A Sources directory containing all of the Swift source files for Perfect
An SPM manifest named “Package.swift” listing the dependencies this project requires
All SPM projects will have, at least, both a Sources directory and a Package.swift file. This project starts out with only one dependency: the Perfect-HTTPServer project.
Dependencies in Package.swift
The PerfectTemplate Package.swift manifest file contains the following content:
import PackageDescription
let package = Package(
name: "PerfectTemplate",
targets: [],
dependencies: [
.Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git",
majorVersion: 2)
]
)
Note: The version presented above may differ from what you have on your terminal. We recommend you consult the actual repository for the most up-to-date content.
There are two important elements in the Package.swift file that you may wish to edit.
The first one is the name element. It indicates the name of the project, and thus, the name of the executable file which will be generated when the project is built.
The second element is the dependencies list. This element indicates all of the subprojects that your application is dependent upon. Each item in this array consists of a “.Package” with
a repository URL and a version.
The example above indicates a wide range of versions so that the template will always grab the newest revision of the HTTPServer project. You may want to restrict your dependencies
to specific stable versions. For example, if you want to only build against version 2 of the Perfect HTTPServer project, your “.Package” element may look like the following:
.Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2)
As your project grows and you add dependencies, you will put all of them in the dependencies list. SPM will automatically download the appropriate versions and compile them along
with your project. All dependencies are downloaded into a Packages directory which SPM will automatically create. For example, if you wanted to use Mustache templates in your server,
your Package.swift file might look like the following:import PackageDescription
let package = Package(
name: "PerfectTemplate",
targets: [],
dependencies: [
.Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git",
majorVersion: 2),
.Package(url: "https://github.com/PerfectlySoft/Perfect-Mustache.git",
majorVersion: 2)
]
)
As you can see, the Perfect-Mustache project was added as a dependency. It provides Mustache template support for your Perfect server. Within your project code, you can now import
“PerfectMustache”, and use the facilities it offers.
As your dependency list grows, you may want to manage the list differently. The following example includes all the Perfect repositories. The list of URLs is maintained separately, and
they are mapped to the format required by the dependencies parameter.
import PackageDescription
let versions = majorVersion: 2
let urls = [
"https://github.com/PerfectlySoft/Perfect-HTTPServer.git",
"https://github.com/PerfectlySoft/Perfect-FastCGI.git",
"https://github.com/PerfectlySoft/Perfect-CURL.git",
"https://github.com/PerfectlySoft/Perfect-PostgreSQL.git",
"https://github.com/PerfectlySoft/Perfect-SQLite.git",
"https://github.com/PerfectlySoft/Perfect-Redis.git",
"https://github.com/PerfectlySoft/Perfect-MySQL.git",
"https://github.com/PerfectlySoft/Perfect-MongoDB.git",
"https://github.com/PerfectlySoft/Perfect-WebSockets.git",
"https://github.com/PerfectlySoft/Perfect-Notifications.git",
"https://github.com/PerfectlySoft/Perfect-Mustache.git"
]
let package = Package(
name: "PerfectTemplate",
targets: [],
dependencies: urls.map { .Package(url: $0, versions) }
)
Building
SPM provides the following commands for building your project, and for cleaning up any build artifacts:
swift build
This command will download any dependencies if they haven''t been already acquired and attempt to build the project. If the build is successful, then the resulting executable will be
placed in the (hidden) .build/debug/ directory. When building the PerfectTemplate project, you will see as the last line of SPM output:
Linking .build/debug/PerfectTemplate . Entering .build/debug/PerfectTemplate will run the server. By default, a debug version of the executable will be generated.
To build a production ready release version, you would issue the command swift build -c release . This will place the resulting executable in the .build/release/
directory.
swift build --clean
swift build --clean=dist
It can be useful to wipe out all intermediate data and do a fresh build. Providing the --clean argument will delete the .build directory, and permit a fresh build. Providing the
--clean=dist argument will delete both the .build directory and the Packages directory. Doing so will re-download all project dependencies during the next build to ensure
you have the latest version of a dependent project.
Xcode Projects
SPM can generate an Xcode project based on your Package.swift file. This project will permit you to build and debug your application within Xcode. To generate the Xcode project, issue
the following command:swift package generate-xcodeproj
The command will generate the Xcode project file into the same directory. For example, issuing the command within the PerfectTemplate project directory will produce the message
generated: ./PerfectTemplate.xcodeproj .
Note: It is not advised to edit or add files directly to this Xcode project. If you add any further dependencies, or require later versions of any dependencies, you will need to
regenerate this Xcode project. As a result, any modifications you have made will be overwritten.
Additional Information
For more information on the Swift Package Manager, visit:
https://swift.org/package-manager/
HTTPServer
This document describes the three methods by which you can launch new Perfect HTTP servers. These methods differ in their complexity and each caters to a different use case.
The first method is data driven whereby you provide either a Swift Dictionary or JSON file describing the servers you wish to launch. The second method describes the desired servers
using Swift language constructs complete with the type checking and compile-time constraints provided by Swift. The third method permits you to instantiate an HTTPServer object and
then procedurally configure each of the required properties before manually starting it.
HTTP servers are configured and started using the functions available in the HTTPServer namespace. A Perfect HTTP server consists of at least a name and a listen port, one or
more handlers, and zero or more request or response filters. In addition, a secure HTTPS server will also have TLS related configuration information such as a certificate or key file path.
When starting servers you can choose to wait until the servers have terminated (which will generally not happen until the process is terminated) or receive LaunchContext objects
for each server which permits them to be individually terminated and waited upon.
HTTPServer Configuration Data
One or more Perfect HTTP servers can be configured and launched using structured configuration data. This includes setting elements such as the listen port and bind address but also
permits pointing handlers to specific fuctions by name. This feature is required if loading server configuration data from a JSON file. In order to enable this functionality on Linux, you
must build your SPM executable with an additional flag:
swift build -Xlinker --export-dynamic
This is only required on Linux and only if you are going to be using the configuration data system described in this section with JSON text files.
Call one of the static HTTPServer.launch functions with either a path to a JSON configuration file, a File object pointing to the configuration file or a Swift Dictionary.
The resulting configuration data will be used to launch one or more HTTP servers.
public extension HTTPServer {
public static func launch(wait: Bool = true, configurationPath path: String) throws -> [LaunchContext]
public static func launch(wait: Bool = true, configurationFile file: File) throws -> [LaunchContext]
public static func launch(wait: Bool = true, configurationData data: [String:Any]) throws -> [LaunchContext]
}
The default value for the wait parameter indicates that the function should not return but should block until all servers have terminated or the process is killed. If false is given for
wait then the returned array of LaunchContext objects can be used to monitor or terminate the individual servers. Most applications will want the function to wait and so the
functions can be called without including the wait parameter.
do {
try HTTPServer.launch(configurationPath: "/path/to/perfecthttp.json")
} catch {
// handle critical failure
}
Note that the configuration file can be located or named whatever you''d like, but it should have the .json file extension. We may support other file formats in the future and ensuring
that your configuration file has an extension which describes its content is important.
After it is decoded from JSON, at its top level, the configuration data should contain a "servers" key with a value that is an array of Dictionary. These dictionaries describe
the servers which will be launched.[
"servers":[
[…],
[…],
[…]
]
]
A simple example single server configuration dictionary might look as follows. Note that the keys and values in this example are all explained in the subsequent sections of this
document.
[
"servers":[
[
"name":"localhost",
"port":8080,
"routes":[
[
"method":"get",
"uri":"/**",
"handler":PerfectHTTPServer.HTTPHandler.staticFiles,
"documentRoot":"/path/to/webroot"
],
[
"methods":["get", "post"],
"uri":"/api/**",
"handler":PerfectHTTPServer.HTTPHandler.redirect,
"base":"http://other.server.ca"
]
]
]
]
]
The options available for servers are as follows:
name:
This required string value is primarily used to identify the server and would generally be the same as the server''s domain name. Applications may use the server name to construct
URLs pointing back to the server.
It is permitted to use the same name for multiple servers. For example, you may have three servers on the same host all listening on different ports. These three servers could all have
the same name.
Corresponding HTTPServer property: HTTPServer.serverName .
port:
This required integer value indicates the port on which the server should listen. TCP ports range from 0 to 65535. Ports in the range of 0 to 1024 require root permissions to start. See
the runAs option to indicate the user which the process should switch to after binding on such a port.
Corresponding HTTPServer property: HTTPServer.serverPort .
address:
This optional String value should be a dotted IP address. This indicates the local address on which the server should bind. If not given, this value defaults to "0.0.0.0" which indicates
that the server should bind on all available local IP addresses. Using "::" for this value will enable listening on all local IPv6 & IPv4 addresses.
Corresponding HTTPServer property: HTTPServer.serverAddress .
routes:
This optional element should have a value that is an array of [String:Any] dictionaries. Each element of the array indicates a URI route or group of URI toues which map an incoming
HTTP request to a handler. See Routing for specifics on Perfect''s URI routing system.
Each [String:Any] dictionary in the "routes" array must be either a simple route with a handler or a group of routes.
If the dictionary has a "uri" and a "handler" key, then it is assumed to be a simple route. A group of yours must have at least a "children" key.A simple route consists of zero or more HTTP methods, a URI and the name of a RequestHandler function or a function which returns a RequestHandler . The key names are:
"method" or "methods", "uri", and "handler". The value for the "methods" key should be an array of strings. If no method values are provided then any HTTP method may trigger the
handler.
A group of handlers consists of an optional "baseURI", an optional "handler", and a required array of "children" whose value must be [[String:Any]]. Each of the children in this array can
in turn express either simple route handlers or further groups of routes. The optional "handler" function will be executed before the final request handler. Multiple handlers can be chained
in this manner, some running earlier to set up certain state or to screen the incomming requests. Handlers used in this manner should call response.next() to indicate that the
handler has finished executing and the next one can be called. If there are no further handlers then the request will be completed. If an intermediate handler determines that the request
should go no futher, it can call response.completed() and no futher handlers will be executed.
Perfect comes with request handlers that take care of various common tasks such as redirecting clients or serving static, on-disk files. The following example defines a server which
listens on port 8080 and has two handlers, one of which serves static files while the other redirects clients to a new URL.
[
"servers":[
[
"name":"localhost",
"port":8080,
"routes":[
[
"method":"get",
"uri":"/**",
"handler":PerfectHTTPServer.HTTPHandler.staticFiles,
"documentRoot":"/path/to/webroot"
],
[
"baseURI":"/api",
"children":[
[
"methods":["get", "post"],
"uri":"/**",
"handler":PerfectHTTPServer.HTTPHandler.redirect,
"base":"http://other.server.ca"
]
]
]
]
]
]
]
Corresponding HTTPServer property: HTTPServer.addRoutes .
Adding Custom Request Handlers
While the built-in Perfect request handlers can be handy, most developers will want to add custom behaviour to their servers. The "handler" key values can point to your own functions
which will each return the RequestHandler to be called when the route uri matches an incoming request.
It''s important to note that the function names which you would enter into the configuration data are static functions which return the RequestHandler that will be subsequently used.
These functions accept the current configuration data [String:Any] for the particular route in order to extract any available configuration data such as the staticFiles
"documentRoot" described above.
Alternatively, if you do not need any of the available configuration data (for example, your handler requires no configuration) you can simply indicate the RequestHandler itself.
It''s also vital that the name you provide be fully qualified. That is, it should include your Swift module name, the name of any interstitial nesting constructs such as struct or enum, and
then the function name itself. These should all be separated by "." (periods). For example, you can see the static file handler is given as "PerfectHTTPServer.HTTPHandler.staticFiles". It
resides in the module "PerfectHTTPServer", in an extension of the struct "HTTPHandler" and is named "staticFiles".
Note that if you are creating a configuration directly in Swift code as a dictionary then you do not have to quote the function names that you provide. The value for the "handler" (and
subsequently the "filters" described later in this chapter) can be given as direct function references.
An example request handler generator which could be used in a server configuration follows.public extension HTTPHandler {
public static func staticFiles(data: [String:Any]) throws -> RequestHandler {
let documentRoot = data["documentRoot"] as? String ?? "./webroot"
let allowResponseFilters = data["allowResponseFilters"] as? Bool ?? false
return {
req, resp in
StaticFileHandler(documentRoot: documentRoot, allowResponseFilters: allowResponseFilters)
.handleRequest(request: req, response: resp)
}
}
}
Note: the HTTPHandler struct is an abstract namespace defined in PerfectHTTPServer. It consists of only static request handler generators such as this.
Request handler generators are encouraged to throw when required configuration data is not provided by the user or if the data is invalid. Ensure that the Error you throw will provide
a helpful message when it is converted to String. This will ensure that users see such configuration problems early so that they can be corrected. If the handler generator cannot return a
valid RequestHandler then it should throw an error.
filters:
Request filters can screen or manipulate incoming request data. For example, an authentication filter might check to see if a request has certain permissions, and if not, return an error to
the client. Response filters do the same for outgoing data, having an opportunity to change response headers or body data. See Request and Response Filters for specifics on Perfect''s
request filtering system.
The value for the "filters" key is an array of dictionaries containing keys which describe each filter. The required keys for these dictionaries are "type", and "name". The possible values for
the "type" key are "request" or "response", to indicate either a request or a response filter. A "priority" key can also be provided with a value of either "high", "medium", or "low". If a
priority is not provided then the default value will be "high".
The following example adds two filters, one for requests and one for responses.
[
"servers": [
[
"name":"localhost",
"port":8080,
"routes":[
["method":"get", "uri":"/**", "handler":"PerfectHTTPServer.HTTPHandler.staticFiles",
"documentRoot":"./webroot"]
],
"filters":[
[
"type":"request",
"priority":"high",
"name":"PerfectHTTPServer.HTTPFilter.customReqFilter"
],
[
"type":"response",
"priority":"high",
"name":"PerfectHTTPServer.HTTPFilter.custom404",
"path":"./webroot/404.html"
]
]
]
]
Filter names work in much the same way as route handlers do, however, the function signatures are different. A request filter generator function takes the [String:Any] containing the
configuration data and returns a HTTPRequestFilter or a HTTPResponseFilter depending on the filter type.// a request filter generator
public func customReqFilter(data: [String:Any]) throws -> HTTPRequestFilter {
struct ReqFilter: HTTPRequestFilter {
func filter(request: HTTPRequest, response: HTTPResponse, callback: (HTTPRequestFilterResult) -> ()) {
callback(.continue(request, response))
}
}
return ReqFilter()
}
// a response filter generator
public func custom404(data: [String:Any]) throws -> HTTPResponseFilter {
guard let path = data["path"] as? String else {
fatalError("HTTPFilter.custom404(data: [String:Any]) requires a value for key \"path\".")
}
struct Filter404: HTTPResponseFilter {
let path: String
func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
if case .notFound = response.status {
do {
response.setBody(string: try File(path).readString())
} catch {
response.setBody(string: "An error occurred but I could not find the error file. \(response.status)")
}
response.setHeader(.contentLength, value: "\(response.bodyBytes.count)")
}
return callback(.continue)
}
func filterBody(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
callback(.continue)
}
}
return Filter404(path: path)
}
Corresponding HTTPServer properties: HTTPServer.setRequestFilters , HTTPServer.setResponseFilters .
tlsConfig:
If a "tlsConfig" key is provided then a secure HTTPS server will be attempted. The value for the TLS config should be a dictionary containing the following required and optional
keys/values:
certPath - required String file path to the certificate file
keyPath - optional String file path to the key file
cipherList - optional array of ciphers that the server will support
caCertPath - optional String file path to the CA cert file
verifyMode - optional String indicating how the secure connections should be verified. The value should be one of:
none
peer
failIfNoPeerCert
clientOnce
peerWithFailIfNoPeerCert
peerClientOnce
peerWithFailIfNoPeerCertClientOnce
The default values for the cipher list can be obtained through the TLSConfiguration.defaultCipherList property.
User Switching
After starting as root and binding the servers to the indicated ports (low, restricted ports such as 80, for example), it is recommended that the server process switch to a non-root
operating system user. These users are generally given low or restricted permissions in order to prevent security attacks which could be perpetrated were the server running as root.
At the top level of your configuration data (as a sibling to the "servers" key), you can include a "runAs" key with a string value. This value indicates the name of the desired user. The
process will switch to the user only after all servers have successfully bound their respective listen ports.
Only a server process which is started as root can switch users.
Corresponding HTTPServer function: HTTPServer.runAs(_ user: String) .HTTPServer.launch
There are several variants of the HTTPServer.launch functions which permit one or more servers to be started. These functions abstract the inner workings of the HTTPServer
object and provide a more streamlined interface for server launching.
The simplest of these methods launches a single server with options:
public extension HTTPServer {
public static func launch(wait: Bool = true, name: String, port: Int, routes: Routes,
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = []) throws -> LaunchContext
public static func launch(wait: Bool = true, name: String, port: Int, routes: [Route],
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = []) throws -> LaunchContext
public static func launch(wait: Bool = true, name: String, port: Int, documentRoot root: String,
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = []) throws -> LaunchContext
}
The remaining launch functions take one or more server descriptions, launches them and returns their LaunchContext objects.
public extension HTTPServer {
public static func launch(wait: Bool = true, _ servers: [Server]) throws -> [LaunchContext]
public static func launch(wait: Bool = true, _ server: Server, _ servers: Server...) throws -> [LaunchContext]
}
The Server , which describes the HTTPServer object that will eventually be launched, looks like so:
public extension HTTPServer {
public struct Server {
public init(name: String, address: String, port: Int, routes: Routes,
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = [])
public init(tlsConfig: TLSConfiguration, name: String, address: String, port: Int, routes: Routes,
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = [])
public init(name: String, port: Int, routes: Routes,
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = [])
public init(tlsConfig: TLSConfiguration, name: String, port: Int, routes: Routes,
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = [])
public static func server(name: String, port: Int, routes: Routes,
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = []) -> Server
public static func server(name: String, port: Int, routes: [Route],
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = []) -> Server
public static func server(name: String, port: Int, documentRoot root: String,
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = []) -> Server
public static func secureServer(_ tlsConfig: TLSConfiguration, name: String, port: Int, routes: [Route],
requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [],
responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = []) -> Server
}
}
The following examples show some common usages.// start a single server serving static files
try HTTPServer.launch(name: "localhost", port: 8080, documentRoot: "/path/to/webroot")
// start two servers. have one serve static files and the other handle API requests
let apiRoutes = Route(method: .get, uri: "/foo/bar", handler: {
req, resp in
//do stuff
})
try HTTPServer.launch(
.server(name: "localhost", port: 8080, documentRoot: "/path/to/webroot"),
.server(name: "localhost", port: 8181, routes: [apiRoutes]))
// start a single server which handles API and static files
try HTTPServer.launch(name: "localhost", port: 8080, routes: [
Route(method: .get, uri: "/foo/bar", handler: {
req, resp in
//do stuff
}),
Route(method: .get, uri: "/foo/bar", handler:
HTTPHandler.staticFiles(documentRoot: "/path/to/webroot"))
])
let apiRoutes = Route(method: .get, uri: "/foo/bar", handler: {
req, resp in
//do stuff
})
// start a secure server
try HTTPServer.launch(.secureServer(TLSConfiguration(certPath: "/path/to/cert"), name: "localhost", port: 8080, routes: [apiRoutes]))
The TLSConfiguration struct configures the server for HTTPS and is defined as:
public struct TLSConfiguration {
public init(certPath: String, keyPath: String? = nil,
caCertPath: String? = nil, certVerifyMode: OpenSSLVerifyMode? = nil,
cipherList: [String] = TLSConfiguration.defaultCipherList)
}
LaunchContext
If wait: false is given to any of the HTTPServer.launch functions then one or more LaunchContext objects are returned. These objects permit each server''s status to be
checked and permit the server to be terminated.
public extension HTTPServer {
public struct LaunchFailure: Error {
let message: String
let configuration: Server
}
public class LaunchContext {
public var terminated: Bool
public let server: Server
public func terminate() -> LaunchContext
public func wait(seconds: Double = Threading.noTimeout) throws -> Bool
}
}
If a launched server fails because an error is thrown then that error will be translated and thrown when the wait function is called.
HTTPServer Object
An HTTPServer object can be instantiated, configured and manually started.public class HTTPServer {
/// The directory in which web documents are sought.
/// Setting the document root will add a default URL route which permits
/// static files to be served from within.
public var documentRoot: String
/// The port on which the server is listening.
public var serverPort: UInt16 = 0
/// The local address on which the server is listening. The default of 0.0.0.0 indicates any address.
public var serverAddress = "0.0.0.0"
/// Switch to user after binding port
public var runAsUser: String?
/// The canonical server name.
/// This is important if utilizing the `HTTPRequest.serverName` property.
public var serverName = ""
public var ssl: (sslCert: String, sslKey: String)?
public var caCert: String?
public var certVerifyMode: OpenSSLVerifyMode?
public var cipherList: [String]
/// Initialize the server object.
public init()
/// Add the Routes to this server.
public func addRoutes(_ routes: Routes)
/// Set the request filters. Each is provided along with its priority.
/// The filters can be provided in any order. High priority filters will be sorted above lower priorities.
/// Filters of equal priority will maintain the order given here.
public func setRequestFilters(_ request: [(HTTPRequestFilter, HTTPFilterPriority)]) -> HTTPServer
/// Set the response filters. Each is provided along with its priority.
/// The filters can be provided in any order. High priority filters will be sorted above lower priorities.
/// Filters of equal priority will maintain the order given here.
public func setResponseFilters(_ response: [(HTTPResponseFilter, HTTPFilterPriority)]) -> HTTPServer
/// Start the server. Does not return until the server terminates.
public func start() throws
/// Stop the server by closing the accepting TCP socket. Calling this will cause the server to break out of the otherwise blocking `start` f
unction.
public func stop()
}
Handling Requests
As an internet server, Perfect''s main function is to receive and respond to requests from clients. Perfect provides objects to represent the request and the response components, and it
permits you to install handlers to control the resulting content generation.
Everything begins with the creation of the server object. The server object is configured and subsequently binds and listens for connections on a particular port. When a connection
occurs, the server begins reading the request data. Once the request has been completely read, the server will pass the request object through any request filters.
These filters permit the incoming request to be modified. The server will then use the request''s path URI and search the routing system for an appropriate handler. If a handler is found, it
is given a chance to populate a response object. Once the handler indicates that it is done responding, the response object is passed through any response filters. These filters permit
the outgoing data to be modified. The resulting data is then pushed to the client, and the connection is either closed, or it can be reused as an HTTP persistent connection, a.k.a. HTTP
keep-alive, for additional requests and responses.
Consult the following sections for more details on each specific phase and what can be accomplished during each:
Routing - Describes the routing system and shows how to install URL handlers
HTTPRequest - Provides details on the request object protocol
HTTPResponse - Provides details on the response object protocol
Request & Response Filters - Shows how to add filters and illustrates how they are useful
In addition, the following sections show how to use some of the pre-made, specialized handlers to accomplish specific tasks:
Static File Handler - Describes how to serve static file content
Mustache - Shows how to populate and serve Mustache template based content # Routing
Routing determines which handler receives a specific request. A handler is a routine, function, or method dedicated to receiving and acting on certain types of requests or signals.
Requests are routed based on two pieces of information: the HTTP request method, and the request path. A route refers to an HTTP method, path, and handler combination. Routes are
created and added to the server before it starts listening for requests. For example:var routes = Routes()
routes.add(method: .get, uri: "/path/one") {
request, response in
response.setBody(string: "Handler was called")
.completed()
}
server.addRoutes(routes)
Once the Perfect server receives a request, it will pass the request object through any registered request filters. These filters have a chance to modify the request object in ways which
may affect the routing process, such as changing the request path. The server will then search for a route which matches the current request method and path. If a route is successfully
found for the request, the server will deliver both the request and response objects to the found handler. If a route is not found for a request, the server will send a “404” or “Not Found”
error response to the client.
Creating Routes
The routing API is part of the PerfectHTTP project. Interacting with the routing system requires that you first import PerfectHTTP .
Before adding any route, you will need an appropriate handler function. Handler functions accept the request and response objects, and are expected to generate content for the
response. They will indicate when they have completed a task. The typealias for a request handler is as follows:
/// Function which receives request and response objects and generates content.
public typealias RequestHandler = (HTTPRequest, HTTPResponse) -> ()
Requests are considered active until a handler indicates that it has concluded. This is done by calling either the HTTPResponse.next() or the HTTPResponse.completed()
function. Request handling in Perfect is fully asynchronous, so a handler function can return, spin off into new threads, or perform any other sort of asynchronous activity before calling
either of these functions.
Request handlers can be chained, in that one request URI can identify multiple handlers along its path. The handlers will be executed in order, and each given a chance to either
continue the request processing or halt it.
A request is considered to be active up until HTTPResponse.completed() is called. Calling .next() when there are no more handlers to execute is equivalent to calling
.completed() . Once the response is marked as completed, the outgoing headers and body data, if any, will be sent to the client.
Individual uri handlers are added to a Routes object before they are added to the server. When a Routes object is created, one or more routes are added using its add functions.
Routes provides the following functions:
public struct Routes {
/// Initialize with no baseUri.
public init(handler: RequestHandler? = nil)
// Initialize with a baseUri.
public init(baseUri: String, handler: RequestHandler? = nil)
/// Add all the routes in the Routes object to this one.
public mutating func add(routes: Routes)
/// Add the given method, uri and handler as a route.
public mutating func add(method: HTTPMethod, uri: String, handler: RequestHandler)
/// Add the given method, uris and handler as a route.
public mutating func add(method: HTTPMethod, uris: [String], handler: RequestHandler)
/// Add the given uri and handler as a route.
/// This will add the route got both GET and POST methods.
public mutating func add(uri: String, handler: RequestHandler)
/// Add the given method, uris and handler as a route.
/// This will add the route got both GET and POST methods.
public mutating func add(uris: [String], handler: RequestHandler)
/// Add one Route to this object.
public mutating func add(_ route: Route)
}
A Routes object can be initialized with a baseURI. The baseURI will be prepended to any route added to the object. For example, one could initialize a Routes object for version one of
an API and give it a baseURI of "/v1". Every route added will be prefixed with /v1. Routes objects can also be added to other Routes objects, and each route therein will be prefixed in the
same manner. The following example shows the creation of two sets of routes for two versions of an API. The second version differs in behavior in only one endpoint:var routes = Routes()
// Create routes for version 1 API
var api = Routes()
api.add(method: .get, uri: "/call1", handler: { _, response in
response.setBody(string: "API CALL 1")
response.completed()
})
api.add(method: .get, uri: "/call2", handler: { _, response in
response.setBody(string: "API CALL 2")
response.completed()
})
// API version 1
var api1Routes = Routes(baseUri: "/v1")
// API version 2
var api2Routes = Routes(baseUri: "/v2")
// Add the main API calls to version 1
api1Routes.add(routes: api)
// Add the main API calls to version 2
api2Routes.add(routes: api)
// Update the call2 API
api2Routes.add(method: .get, uri: "/call2", handler: { _, response in
response.setBody(string: "API v2 CALL 2")
response.completed()
})
// Add both versions to the main server routes
routes.add(routes: api1Routes)
routes.add(routes: api2Routes)
A Routes object can also be given an optional handler. This handler will be called if that part of a path is matched against another subsequent handler.
For example, a "/v1" version of an API might enforce a particular authentication mechanism on the clients. The authentication handler could be added to the "/v1" portion of the api and
as each actual endpoint is reached the authentication handler would be given a chance to evaluate the request before passing it down to the remaining handlers(s).
var routes = Routes(baseUri: "/v1") {
request, response in
if authorized(request) {
response.next()
} else {
response.completed(.unauthorized)
}
}
routes.add(method: .get, uri: "/call1") {
_, response in
response.setBody(string: "API CALL 1").next()
}
routes.add(method: .get, uri: "/call2") {
_, response in
response.setBody(string: "API CALL 2").next()
}
Accessing either "/v1/call1" or "/v1/call2" will pass the request to the handler set on "/v1" routes.
Routes can be nested like this as deeply as you wish. Handlers set directly on Routes objects are considered non-terminal. That is, they can not be accessed directly by clients and will
only be executed if the request goes on to match a handler which is terminal. Likewise, handlers which are terminal but lie on the path of a more complete match will not be executed.
For example with a handler on "/v1/users" and on "/v1/users/foo", accessing "/v1/users/foo" will not execute "/v1/users".
Adding Server Routes
Both Perfect-HTTPServer and Perfect-FastCGI support routing. Consult [HTTPServer](Routing for details on how to apply routes to HTTP servers.
Variables
Route URIs can also contain variable components. A variable component begins and ends with a set of curly brackets { } . Within the brackets is the variable identifier. A variable
identifier can consist of any character except the closing curly bracket } . Variable components work somewhat like single wildcards do in that they match any single literal path
component value. The actual value of the URL component which is matched by the variable is saved and made available through the HTTPRequest.urlVariables dictionary. This
dictionary is of type [String:String] . URI variables are a good way to gather dynamic data from a request. For example, a URL might make a user management related request,and include the user id as a component in the URL.
For example, when given the URI /foo/{bar}/baz , a request to the URL /foo/123/baz would match and place it in the HTTPRequest.urlVariables dictionary with the
value "123" under the key "bar".
Wildcards
A wildcard, also referred to as a wild character, is a symbol used to replace or represent one or more characters. Beyond full literal URI paths, routes can contain wildcard segments.
Wildcards match any portion of a URI and can be used to route groups of URIs to a single handler. Wildcards consist of either one or two asterisks. A single asterisk can occur anywhere
in a URI path as long as it represents one full component of the URI. A double asterisk, or trailing wildcard, can occur only at the end of a URI. Trailing wildcards match any remaining
portion of a URI.
A route with the the URI /foo/*/baz would match the both of these URLs:
/foo/123/baz
/foo/bar/baz
A route with the URI /foo/** would match all of the following URLs:
/foo/bar/baz
/foo
A route with the URI /** would match any request.
A trailing wildcard route will save the URI portion which is matched by the wildcard. It will place this path segment in the HTTPRequest.urlVariables map under the key indicated
by the global variable routeTrailingWildcardKey . For example, given the route URI "/foo/**" and a request URI "/foo/bar/baz", the following snippet would be true:
request.urlVariables[routeTrailingWildcardKey] == "/bar/baz"
Priority/Ordering
Because route URIs could potentially conflict, literal, wildcard and variable paths are checked in a specific order. Path types are checked in the following order:
1. Variable paths
2. Literal paths
3. Wildcard paths
4. Trailing wildcard paths are checked last
Implicit Trailing Wildcard
When the .documentRoot property of the server is set, the server will automatically add a /** trailing wildcard route which will enable the serving of static content from the
indicated directory. For example, setting the document root to "./webroot" would permit the server to deliver any files located within that directory.
Further Information
For more information and examples of URL routing, see the URL Routing example application.
HTTPRequest
When handling a request, all client interaction is performed through HTTPRequest and HTTPResponse objects.
The HTTPRequest object makes available all client headers, query parameters, POST body data, and other relevant information such as the client IP address and URL variables.
HTTPRequest objects will handle parsing and decoding all "application/x-www-form-urlencoded", as well as "multipart/form-data" content type requests. It will make the data for any other
content types available in a raw, unparsed form. When handling multipart form data, HTTPRequest will automatically decode the data and create temporary files for any file uploads
contained therein. These files will exist until the request ends after which they will be automatically deleted. In all of the sections below, the properties and functions are part of the
HTTPRequest protocol.
Relevant Examples
Perfect-HTTPRequestLogging
MetadataHTTPRequest provides several pieces of data which are not explicitly sent by the client. Information such as the client and server IP addresses, TCP ports, and document root fit into this
category.
Client and server addresses are made available as tuples (a finite ordered list of elements) containing the IP addresses and respective ports of each:
/// The IP address and connecting port of the client.
var remoteAddress: (host: String, port: UInt16) { get }
/// The IP address and listening port for the server.
var serverAddress: (host: String, port: UInt16) { get }
When a server is created, you can set its canonical name, or CNAME. This can be useful in a variety of circumstances, such as when creating full links to your server. When a
HTTPRequest is created, the server will apply a CNAME to it. It is made available through the following property:
/// The canonical name for the server.
var serverName: String { get }
The server''s document root is the directory from which static content is generally served. If you are not serving static content, then this document root may not exist. The document root
is configured on the server before it begins accepting requests. Its value is transferred to the HTTPRequest when it is created. When attempting to access static content such as a
Mustache template, one would generally prefix all file paths with this document root value.
/// The server''s document root from which static file content will generally be served.
var documentRoot: String { get }
Request Line
An HTTPRequest line consists of a method, path, query parameters, and an HTTP protocol identifier. An example HTTPRequest line may appear as follows:
GET /path?q1=v1&q2=v2 HTTP/1.1
HTTPRequest makes the parsed request line available through the properties below. The query parameters are presented as an array of name/value tuples in which all names and
values have been URL decoded:
/// The HTTP request method.
var method: HTTPMethod { get set }
/// The request path.
var path: String { get set }
/// The parsed and decoded query/search arguments.
var queryParams: [(String, String)] { get }
/// The HTTP protocol version. For example (1, 0), (1, 1), (2, 0)
var protocolVersion: (Int, Int) { get }
During the routing process, the route URI may have consisted of URL variables, and these will have been parsed and made available as a dictionary:
/// Any URL variables acquired during routing the path to the request handler.
var urlVariables: [String:String] { get set }
An HTTPRequest also makes the full request URI available. It will include the request path as well as any URL encoded query parameters:
/// Returns the full request URI.
var uri: String { get }
Client Headers
Client request headers are made available either keyed by name or through an iterator permitting all header names and values to be accessed. HTTPRequest will automatically parse
and make available all HTTP cookie names and values. All possible request header names are represented in the enumeration type HTTPRequestHeader.Name , which also
includes a .custom(name: String) case for unaccounted header names.
It is possible to set client headers after the request has been read. This would be useful in, for example, HTTPRequest filters as they may need to rewrite or add certain headers:/// Returns the requested incoming header value.
func header(_ named: HTTPRequestHeader.Name) -> String?
/// Add a header to the response.
/// No check for duplicate or repeated headers will be made.
func addHeader(_ named: HTTPRequestHeader.Name, value: String)
/// Set the indicated header value.
/// If the header already exists then the existing value will be replaced.
func setHeader(_ named: HTTPRequestHeader.Name, value: String)
/// Provide access to all current header values.
var headers: AnyIterator<(HTTPRequestHeader.Name, String)> { get }
Cookies are made available through an array of name/value tuples.
/// Returns all the cookie name/value pairs parsed from the request.
var cookies: [(String, String)]
GET and POST Parameters
For a detailed discussion of accessing GET and POST paramaters, see Using Form Data.
Body Data
For the content types "application/x-www-form-urlencoded" and "multipart/form-data", HTTPRequest will automatically parse and make the values available through the postParams
or postFileUploads properties, respectively.
For a more detailed discussion of file upload handling, see File Uploads. For more details on postParams , see Using Form Data.
Request body data with other content types are not parsed and are made available either as raw bytes or as String data. For example, for a client submitting JSON data, one would want
to access the body data as a String which would then be decoded into a useful value.
HTTPRequest makes body data available through the following properties:
/// POST body data as raw bytes.
/// If the POST content type is multipart/form-data then this will be nil.
var postBodyBytes: [UInt8]? { get set }
/// POST body data treated as UTF-8 bytes and decoded into a String, if possible.
/// If the POST content type is multipart/form-data then this will be nil.
var postBodyString: String? { get }
It''s important to note that if the request has the "multipart/form-data" content type, then the postBodyBytes property will be nil. Otherwise, it will always contain the request body data
regardless of the content type.
The postBodyString property will attempt to convert the body data from UTF-8 into a String. It will return nil if there is no body data or if the data could not successfully be
converted from UTF-8.# Using Form Data
In a REST application, there are several common HTTP "verbs" that are used. The most common of these are the "GET" and "POST" verbs.
The best-practice assignment of when to use each verb can vary between methodologies and is beyond the scope of this documentation.
An HTTP "GET" request only passes parameters in the URL:
http://www.example.com/page.html?message=Hello,%20World!
The "query parameters" in the above example are accessed using the .queryParams method:
let params = request.queryParams
While the above example only refers to a GET request, the .queryParams method applies to any HTTP request as they all can contain query parameters.
POST Parameters
POST parameters, or params, are the standard method for passing complex data between browsers and other sources to APIs for creating or modifying content.
Perfect’s HTTP libraries make it easy to access arrays of POST params or specific params.
To return all params (Query or POST) as a [(String,String)] array:let params = request.params()
To return only POST params as a [(String,String)] array:
let params = request.postParams()
To return all params with a specific name such as multiple checkboxes, type:
let params = request.postParams(name: )
This returns an array of strings: [String]
To return a specific parameter, as an optional String? :
let param = request.param(name: )
When supplying a POST parameter in the request object is optional, it can be useful to specify a default value if one is not supplied. In this case, use the following syntax to return an
optional String? :
let param = request.param(name: , defaultValue: )
File Uploads
A special case of using form data is handling file uploads.
There are two main form encoding types:
application/x-www-form-urlencoded (the default)
multipart/form-data
When you wish to include file upload elements, you must choose multipart/form-data as your form''s enctype (encoding) type.
All code used below can be seen in action as a complete example at https://github.com/iamjono/perfect-file-uploads.
An example HTML form containing the correct encoding and file input element might be represented like this:
Receiving the File on the Server Side
Because the form is a POST method, we will handle the route with a method: .post :
var routes = Routes()
routes.add(
method: .post,
uri: "/upload",
handler: handler)
server.addRoutes(routes)
Once the request has been offloaded to the handler we can:// Grab the fileUploads array and see what''s there
// If this POST was not multi-part, then this array will be empty
if let uploads = request.postFileUploads, uploads.count > 0 {
// Create an array of dictionaries which will show what was uploaded
var ary = [[String:Any]]()
for upload in uploads {
ary.append([
"fieldName": upload.fieldName,
"contentType": upload.contentType,
"fileName": upload.fileName,
"fileSize": upload.fileSize,
"tmpFileName": upload.tmpFileName
])
}
values["files"] = ary
values["count"] = ary.count
}
As demonstrated above, the file(s) uploaded are represented by the request.postFileUploads array, and the various properties such as fileName , fileSize and
tmpFileName can be accessed from each array component.
Note: The files uploaded are placed in a temporary directory. It is your responsibility to move them into the desired location.
So let''s create a directory to hold the uploaded files. This directory is outside of the webroot directory for security reasons:
// create uploads dir to store files
let fileDir = Dir(Dir.workingDir.path + "files")
do {
try fileDir.create()
} catch {
print(error)
}
Next, inside the for upload in uploads code block, we will create the action for the file to be moved:
// move file
let thisFile = File(upload.tmpFileName)
do {
let _ = try thisFile.moveTo(path: fileDir.path + upload.fileName, overWrite: true)
} catch {
print(error)
}
Now the uploaded files will move to the specified directory with the original filename restored.
For more information on file system manipulation, see the Directory Operations and File Operations chapters.
HTTPResponse
When handling a request, all client interaction is performed through the provided HTTPRequest and HTTPResponse objects.
The HTTPResponse object contains all outgoing response data. It consists of the HTTP status code and message, the HTTP headers, and any response body data. HTTPResponse
also contains the ability to stream or push chunks of response data to the client, and to complete or terminate the request.
In all of the sections below, unless otherwise noted, the properties and functions are part of the HTTPResponse protocol.
Relevant Examples
Perfect-Cookie-Demo
Perfect-HTTPRequestLogging
HTTP Status
The HTTP status indicates to the client whether or not the request was successful, if there was an error, or if it should take any other action. By default, the HTTPResponse object
contains a 200 OK status. The status can be set to any other value if needed. HTTP status codes are represented by the HTTPResponseStatus enumeration (enum). This enum
contains a case for each of the official status codes as well as a .custom(code: Int, message: String) case.The response status is set with the following property:
/// The HTTP response status.
var status: HTTPResponseStatus { get set }
Response Headers
Response headers can be retrieved, set, or iterated. Official/common header names are represented by the HTTPResponseHeader.Name enum. This also contains a case for
custom header names: .custom(name: String) .
/// Returns the requested outgoing header value.
func header(_ named: HTTPResponseHeader.Name) -> String?
/// Add a header to the outgoing response.
/// No check for duplicate or repeated headers will be made.
func addHeader(_ named: HTTPResponseHeader.Name, value: String)
/// Set the indicated header value.
/// If the header already exists then the existing value will be replaced.
func setHeader(_ named: HTTPResponseHeader.Name, value: String)
/// Provide access to all current header values.
var headers: AnyIterator<(HTTPResponseHeader.Name, String)> { get }
HTTPResponse provides higher level support for setting HTTP cookies. This is accomplished by creating a cookie object and adding it to the response object.
/// This bundles together the values which will be used to set a cookie in the outgoing response
public struct HTTPCookie {
/// Cookie public initializer
public init(name: String,
value: String,
domain: String?,
expires: Expiration?,
path: String?,
secure: Bool?,
httpOnly: Bool?)
}
Cookies are added to the HTTPResponse with the following function:
/// Add a cookie to the outgoing response.
func addCookie(_ cookie: HTTPCookie)
When a cookie is added it is properly formatted and added as a "Set-Cookie" header.
Body Data
The response''s current body data is exposed through the following property:
/// Body data waiting to be sent to the client.
/// This will be emptied after each chunk is sent.
var bodyBytes: [UInt8] { get set }
Data can be either directly added to this array, or can be added through one of the following convenience functions. These functions either set completely or append to the body bytes
using either raw UInt8 bytes or String data. String data will be converted to UTF-8. The last function permits a [String:Any] dictionary to be converted into a JSON string.
/// Append data to the bodyBytes member.
func appendBody(bytes: [UInt8])
/// Append String data to the outgoing response.
/// All such data will be converted to a UTF-8 encoded [UInt8]
func appendBody(string: String)
/// Set the bodyBytes member, clearing out any existing data.
func setBody(bytes: [UInt8])
/// Set the String data of the outgoing response, clearing out any existing data.
/// All such data will be converted to a UTF-8 encoded [UInt8]
func setBody(string: String)
/// Encodes the Dictionary as a JSON string and converts that to a UTF-8 encoded [UInt8]
func setBody(json: [String:Any]) throws
When responding to a client, it is vital that a content-length header be included. When the HTTPResponse object begins sending the accumulated data to the client, it will check to see if
a content length has been set. If it has not, the header will be set based on the count of bytes in the bodyBytes array.The following function will push all current response headers and any body data:
/// Push all currently available headers and body data to the client.
/// May be called multiple times.
func push(callback: (Bool) -> ())
During most common requests, it is not necessary to call this method as the system will automatically flush all pending outgoing data when the request is completed. However, in some
circumstances it may be desirable to have more direct control over this process.
For example, if one were to serve a very large file, it may not be practical to read the entire file content into memory and set the body bytes. Instead, one would set the response''s
content length to the size of the file and then, in chunks, read some of the file data, set the body bytes, and then call the push function repeatedly until all of the file has been sent. The
push function will call the provided callback function parameter with a Boolean. This bool will be true if the content was successfully sent to the client. If the bool is false, then the
request should be considered to have failed, and no further action should be taken.
Streaming
In some cases, the content length of the response cannot be easily determined. For example, if one were streaming live video or audio content, then it may not be possible to set a
content-length header. In such cases, set the HTTPResponse object into streaming mode. When using streaming mode, the response will be sent as HTTP-chunked encoding. When
streaming, it is not required that the content length be set. Instead, add content to the body as usual, and call the push function. If the push succeeds, then the body data will be empty
and ready for more data to be added. Continue calling push until the request has ended, or until push gives a false to the callback.
/// Indicate that the response should attempt to stream all outgoing data.
/// This is primarily used when the resulting content length can not be known.
var isStreaming: Bool { get set }
If streaming is to be used, it is required that isStreaming be set to true before any data is pushed to the client.
Request Completion
Important: When a request has completed, it is required that the HTTPResponse''s completed function be called. This will ensure that all pending data is delivered to the client, and
the underlying TCP connection will be either closed, or in the case of HTTP keep-alive, a new request will be read and processed.
/// Indicate that the request has completed.
/// Any currently available headers and body data will be pushed to the client.
/// No further request related activities should be performed after calling this.
func completed()
Request and Response Filters
In addition to the regular request/response handler system, the Perfect server also provides a request and response filtering system. Any filters which are added to the server are called
for each client request. When these filters run in turn, each are given a chance to modify either the request object before it is delivered to the handler, or the response object after the
request has been marked as complete. Filters also have the option to terminate the current request.
Filters are added to the server along with a priority indicator. Priority levels can be either high, medium, or low. High-priority filters are executed before medium and low. Medium priorities
are executed before any low-level filters.
Because filters are executed for every request, it is vital that they perform their tasks as quickly as possible so as to not hold up or delay request processing.
Relevant Examples
Perfect-HTTPRequestLogging
Request Filters
Request filters are called after the request has been fully read, but before the appropriate request handler has been located. This gives request filters an opportunity to modify the
request before it is handled.
Creating
Request filters must conform to the HTTPRequestFilter protocol:/// A filter which can be called to modify a HTTPRequest.
public protocol HTTPRequestFilter {
/// Called once after the request has been read but before any handler is executed.
func filter(request: HTTPRequest, response: HTTPResponse, callback: (HTTPRequestFilterResult) -> ())
}
When it comes time for the filter to run, its filter function will be called. The filter should perform any activities it needs, and then call the provided callback to indicate that it has
completed its processing. The callback takes a value which indicates what the next step should be. This can indicate that the system should either continue with processing filters, stop
processing request filters at the current priority level and proceed with delivering the request to a handler, or terminate the request entirely.
/// Result from one filter.
public enum HTTPRequestFilterResult {
/// Continue with filtering.
case `continue`(HTTPRequest, HTTPResponse)
/// Halt and finalize the request. Handler is not run.
case halt(HTTPRequest, HTTPResponse)
/// Stop filtering and execute the request.
/// No other filters at the current priority level will be executed.
case execute(HTTPRequest, HTTPResponse)
}
Because the filter receives both the request and response objects and then delivers request and response objects in its HTTPRequestFilterResult , it''s possible for a filter to
entirely replace these objects if desired.
Adding
Request filters are set directly on the server and given as an array of filter and priority tuples.
public class HTTPServer {
public func setRequestFilters(_ request: [(HTTPRequestFilter, HTTPFilterPriority)]) -> HTTPServer
}
Calling this function sets the server''s request filters. Each filter is provided along with its priority. The filters in the array parameter can be given in any order. The server will sort them
appropriately, putting high-priority filters above those with lower priorities. Filters of equal priority will maintain the given order.
Example
The following example is taken from a filter-related test case. It illustrates how to create and add filters, and shows how the filter priority levels interact.var oneSet = false
var twoSet = false
var threeSet = false
struct Filter1: HTTPRequestFilter {
func filter(request: HTTPRequest, response: HTTPResponse, callback: (HTTPRequestFilterResult) -> ()) {
oneSet = true
callback(.continue(request, response))
}
}
struct Filter2: HTTPRequestFilter {
func filter(request: HTTPRequest, response: HTTPResponse, callback: (HTTPRequestFilterResult) -> ()) {
XCTAssert(oneSet)
XCTAssert(!twoSet && !threeSet)
twoSet = true
callback(.execute(request, response))
}
}
struct Filter3: HTTPRequestFilter {
func filter(request: HTTPRequest, response: HTTPResponse, callback: (HTTPRequestFilterResult) -> ()) {
XCTAssert(false, "This filter should be skipped")
callback(.continue(request, response))
}
}
struct Filter4: HTTPRequestFilter {
func filter(request: HTTPRequest, response: HTTPResponse, callback: (HTTPRequestFilterResult) -> ()) {
XCTAssert(oneSet && twoSet)
XCTAssert(!threeSet)
threeSet = true
callback(.halt(request, response))
}
}
var routes = Routes()
routes.add(method: .get, uri: "/", handler: {
request, response in
XCTAssert(false, "This handler should not execute")
response.completed()
}
)
let requestFilters: [(HTTPRequestFilter, HTTPFilterPriority)] = [
(Filter1(), HTTPFilterPriority.high),
(Filter2(), HTTPFilterPriority.medium),
(Filter3(), HTTPFilterPriority.medium),
(Filter4(), HTTPFilterPriority.low)
]
let server = HTTPServer()
server.setRequestFilters(requestFilters)
server.serverPort = 8181
server.addRoutes(routes)
try server.start()
Response Filters
Each response filter is executed once before response header data is sent to the client, and again for any subsequent chunk of body data. These filters can modify the outgoing
response in any way they see fit, including adding or removing headers or rewriting body data.
Creating
Response filters must conform to the HTTPResponseFilter protocol.
/// A filter which can be called to modify a HTTPResponse.
public protocol HTTPResponseFilter {
/// Called once before headers are sent to the client.
func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ())
/// Called zero or more times for each bit of body data which is sent to the client.
func filterBody(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ())
}When it comes time to send response headers, the filterHeaders function is called. This function should perform whatever tasks it needs on the provided HTTPResponse
object, and then call the callback function. It should deliver unto the callback one of the HTTPResponseFilterResult values, which are defined as follows:
/// Response from one filter.
public enum HTTPResponseFilterResult {
/// Continue with filtering.
case `continue`
/// Stop executing filters until the next push.
case done
/// Halt and close the request.
case halt
}
These values indicate if the system should continue processing filters, stop executing filters until the next data push, or halt and terminate the request entirely.
When it comes time to send out one discrete chunk of data to the client, the filters'' filterBody function is called. This function can inspect the outgoing data in the
HTTPResponse.bodyBytes property, and potentially modify or replace the data. Since the headers have already been pushed out at this stage, any modifications to the header data
will be ignored. Once a filter''s body filtering has concluded, it should call the provided callback and deliver a HTTPResponseFilterResult . The meaning of these values is the
same as for the filterHeaders function.
Adding
Response filters are set directly on the server and given as an array of filter and priority tuples.
public class HTTPServer {
public func setResponseFilters(_ response: [(HTTPResponseFilter, HTTPFilterPriority)]) -> HTTPServer
}
Calling this function sets the server''s response filters. Each filter is provided along with its priority. The filters in the array parameter can be given in any order. The server will sort them
appropriately, putting high-priority filters above those with lower priorities. Filters of equal priority will maintain the given order.
Examples
The following example is taken from a filters test case. It illustrates how response filter priorities operate, and how response filters can modify outgoing headers and body data.struct Filter1: HTTPResponseFilter {
func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
response.setHeader(.custom(name: "X-Custom"), value: "Value")
callback(.continue)
}
func filterBody(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
callback(.continue)
}
}
struct Filter2: HTTPResponseFilter {
func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
callback(.continue)
}
func filterBody(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
var b = response.bodyBytes
b = b.map { $0 == 65 ? 97 : $0 }
response.bodyBytes = b
callback(.continue)
}
}
struct Filter3: HTTPResponseFilter {
func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
callback(.continue)
}
func filterBody(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
var b = response.bodyBytes
b = b.map { $0 == 66 ? 98 : $0 }
response.bodyBytes = b
callback(.done)
}
}
struct Filter4: HTTPResponseFilter {
func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
callback(.continue)
}
func filterBody(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
XCTAssert(false, "This should not execute")
callback(.done)
}
}
var routes = Routes()
routes.add(method: .get, uri: "/", handler: {
request, response in
response.addHeader(.contentType, value: "text/plain")
response.isStreaming = true
response.setBody(string: "ABZ")
response.push {
_ in
response.setBody(string: "ABZ")
response.completed()
}
})
let responseFilters: [(HTTPResponseFilter, HTTPFilterPriority)] = [
(Filter1(), HTTPFilterPriority.high),
(Filter2(), HTTPFilterPriority.medium),
(Filter3(), HTTPFilterPriority.low),
(Filter4(), HTTPFilterPriority.low)
]
let server = HTTPServer()
server.setResponseFilters(responseFilters)
server.serverPort = port
server.addRoutes(routes)
try server.start()
The example filters will add a X-Custom header and lowercase any A or B character in the body data. Note that the handler in this example sets the response to streaming mode,
meaning that chunked encoding is used, and the body data is sent out in two discrete chunks.
404 Response FilterA more useful example is posted below. This code will create and install a filter which monitors "404 not found" responses, and provides a custom message when it finds one.
struct Filter404: HTTPResponseFilter {
func filterBody(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
callback(.continue)
}
func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) {
if case .notFound = response.status {
response.setBody(string: "The file \(response.request.path) was not found.")
response.setHeader(.contentLength, value: "\(response.bodyBytes.count)")
callback(.done)
} else {
callback(.continue)
}
}
}
let server = HTTPServer()
server.setResponseFilters([(Filter404(), .high)])
server.serverPort = 8181
try server.start()
Web Redirects
The Perfect WebRedirects module will filter for specified routes (including trailing wildcard routes) and perform redirects as instructed if a match is found.
This can be important for maintaining SEO ranking in systems that have moved. For example, if moving from a static HTML site where /about.html is replaced with the new route
/about and no valid redirect is in place, the site or system will lose SEO ranking.
A demo showing the usage, and working of the Perfect WebRedirects module can be found at Perfect-WebRedirects-Demo.
Including in your project
Import the dependency into your project by specifying it in your project''s Package.swift file, or adding it via Perfect Assistant.
.Package(url: "https://github.com/PerfectlySoft/Perfect-WebRedirects", majorVersion: 1),
Then in your main.swift file where you configure your web server, add it as an import, and add the filter:
import PerfectWebRedirects
Adding the filter:
// Add to the "filters" section of the config:
[
"type":"request",
"priority":"high",
"name":WebRedirectsFilter.filterAPIRequest,
]
If you are also adding Request Logger filters, if the Web Redirects object is added second, directly after the RequestLogger filter, then both the original request (and its associated
redirect code) and the new request will be logged correctly.
Configuration file
The configuration for the routes is included in JSON files at /config/redirect-rules/*.json in the form:{
"/test/no": {
"code": 302,
"destination": "/test/yes"
},
"/test/no301": {
"code": 301,
"destination": "/test/yes"
},
"/test/wild/*": {
"code": 302,
"destination": "/test/wildyes"
},
"/test/wilder/*": {
"code": 302,
"destination": "/test/wilding/*"
}
}
Note that multiple JSON files can exist in this directory; all will be loaded the first time the filter is invoked.
The "key" is the matching route (the "old" file or route), and the "value" contains the HTTP code and new destination route to redirect to.# Sessions
Perfect includes session drivers for Redis, PostgreSQL, MySQL, SQLite3, CouchDB and MongoDB servers, as well as in-memory session storage for development purposes.
Session management is a foundational function for any web or application environment, and can provide linkage to authentication, transitional preference storage, and transactional data
such as for a traditional shopping cart.
The general principle is that when a user visits a web site or system with a browser, the server assigns the user a "token" or "session id" and passes this value back to the client/browser
in the form of a cookie or JSON data. This token is then included as either a cookie or Bearer Token with every subsequent request.
Sessions have an expiry time, usually in the form of an "idle timeout". This means that if a session has not been active for a set number of seconds, the session is considered expired
and invalid.
The Perfect Sessions implementation stores the date and time each was created, when it was last "touched", and an idle time. On each access by the client/browser the session is
"touched" and the idle time is reset. If the "last touched" plus "idle time" is less than the current date/time then the session has expired.
Each session has the following properties:
token - the session id
userid - an optionally stored user id string
created - an integer representing the date/time created, in seconds
updated - an integer representing the date/time last touched, in seconds
idle - an integer representing the number of seconds the session can be idle before being considered expired
data - a [String:Any] Array that is converted to JSON for storage. This is intended for storage of simple preference values.
ipaddress - the IP Address (v4 or v6) that the session was first used on. Used for optional session verification.
useragent - the User Agent string that the session was first used with. Used for optional session verification.
CSRF - the CSRF (Cross Site Request Forgery) security configuration.
CORS - the CORS (Cross Origin Resource Sharing) security configuration.
Examples
Each of the modules has an associated example/demo. Much of the functionality described in this document can be observed in each of these examples.
In-Memory Sessions
Redis Sessions
PostgreSQL Sessions
MySQL Sessions
SQLite Sessions
CouchDB Sessions
MongoDB SessionsInstallation
If using the in-memory driver, import the base module by including the following in your project''s Package.swift:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session.git", majorVersion: 1)
Database-Specific Drivers
Redis:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-Redis.git", majorVersion: 1)
PostgreSQL:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-PostgreSQL.git", majorVersion: 1)
MySQL:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-MySQL.git", majorVersion: 1)
SQLite3:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-SQLite.git", majorVersion: 1)
CouchDB:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-CouchDB.git", majorVersion: 1)
MongoDB:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-MongoDB.git", majorVersion: 1)
Configuration
The struct SessionConfig contains settings that can be customized to your own preference:
// The name of the session.
// This will also be the name of the cookie set in a browser.
SessionConfig.name = "PerfectSession"
// The "Idle" time for the session, in seconds.
// 86400 is one day.
SessionConfig.idle = 86400
// Optional cookie domain setting
SessionConfig.cookieDomain = "localhost"
// Optional setting to lock session to the initiating IP address. Default is false
SessionConfig.IPAddressLock = true
// Optional setting to lock session to the initiating user agent string. Default is false
SessionConfig.userAgentLock = true
// The interval at which stale sessions are purged from the database
SessionConfig.purgeInterval = 3600 // in seconds. Default is 1 hour.
// CouchDB-Specific
// The CouchDB database used to store sessions
SessionConfig.couchDatabase = "sessions"
// MongoDB-Specific
// The MongoDB collection used to store sessions
SessionConfig.mongoCollection = "sessions"
If you wish to change the SessionConfig values, you mush set these before the Session Driver is defined.The CSRF (Cross Site Request Forgery) security configuration and CORS (Cross Origin Resource Sharing) security configuration are discussed separately.
Note that for the Redis session driver there is no "timed event" via the SessionConfig.purgeInterval as the mechanism for expiry is handled directly via the Expires value
added at session creation or update.
IP Address and User Agent Locks
If the SessionConfig.IPAddressLock or SessionConfig.userAgentLock settings are true, then the session will be forcibly ended if the incoming information does not
match that which was sent when the session was initiated.
This is a security measure to assist in preventing man-in-the-middle / session hijacking attacks.
Be aware that if a user has logged on through a WiFi network and transitions to a mobile or wired connection while the SessionConfig.IPAddressLock setting has been set to
true , the user will be logged out of their session.
Defining the Session Driver
Each Session Driver has its own implementation which is optimized for the storage option. Therefore you must set the session driver and HTTP filters before executing "server.start()".
In main.swift, after any SessionConfig changes, set the following:
// Instantiate the HTTPServer
let server = HTTPServer()
// Define the Session Driver
let sessionDriver = SessionMemoryDriver()
// Add the filters so the pre- and post- route actions are executed.
server.setRequestFilters([sessionDriver.requestFilter])
server.setResponseFilters([sessionDriver.responseFilter])
In Perfect, filters are analogous in some ways to the concept of "middleware" in other frameworks. The "request" filter will intercept the incoming request and extract the session token,
attempt to load the session from storage, and make the session data available to the application. The response will be executed immediately before the response is returned to the
client/browser and saves the session, and re-sends the cookie to the client/browser.
See "Database-Specific Options" below for storage-appropriate settings and Driver syntax.
Accessing the Session Data
The token , userid and data properties of the session are exposed in the "request" object which is passed into all handlers. The userid and data properties can be
read and written to during the scope of the handler, and automatically saved to storage in the response filter.
A sample handler can be seen in the example systems.
// Defining an "Index" handler
open static func indexHandlerGet(request: HTTPRequest, _ response: HTTPResponse) {
// Random generator from TurnstileCrypto
let rand = URandom()
// Adding some random data to the session for demo purposes
request.session.data[rand.secureToken] = rand.secureToken
// For demo purposes, dumping all current session data as a JSON string.
let dump = try? request.session.data.jsonEncodedString()
// Some simple HTML that displays the session token/id, and the current data as JSON.
let body = "
Your Session ID is: \(request.session.token)
Session data: \(dump)
"
// Send the response back.
response.setBody(string: body)
response.completed()
}
To read the current session id:
request.session.token
To access the userid:// read:
request.session.userid
// write:
request.session.userid = "MyString"
To access the data stored in the session:
// read:
request.session.data
// write:
request.session.data["keyString"] = "Value"
request.session.data["keyInteger"] = 1
request.session.data["keyBool"] = true
// reading a specific value
if let val = request.session.data["keyString"] as? String {
let keyString = val
}
Database-Specific options
Redis
Importing the module, in Package.swift:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-Redis.git", majorVersion: 1)
Defining the connection to the PostgreSQL server:
RedisSessionConnector.host = "localhost"
RedisSessionConnector.port = 5432
RedisSessionConnector.password = "secret"
Defining the Session Driver:
let sessionDriver = SessionRedisDriver()
PostgreSQL
Importing the module, in Package.swift:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-PostgreSQL.git", majorVersion: 1)
Defining the connection to the PostgreSQL server:
PostgresSessionConnector.host = "localhost"
PostgresSessionConnector.port = 5432
PostgresSessionConnector.username = "username"
PostgresSessionConnector.password = "secret"
PostgresSessionConnector.database = "mydatabase"
PostgresSessionConnector.table = "sessions"
Defining the Session Driver:
let sessionDriver = SessionPostgresDriver()
MySQL
Importing the module, in Package.swift:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-MySQL.git", majorVersion: 1)
Defining the connection to the MySQL server:MySQLSessionConnector.host = "localhost"
MySQLSessionConnector.port = 3306
MySQLSessionConnector.username = "username"
MySQLSessionConnector.password = "secret"
MySQLSessionConnector.database = "mydatabase"
MySQLSessionConnector.table = "sessions"
Defining the Session Driver:
let sessionDriver = SessionMySQLDriver()
SQLite
Importing the module, in Package.swift:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-SQLite.git", majorVersion: 1)
Defining the connection to the SQLite server:
SQLiteConnector.db = "./SessionDB"
Defining the Session Driver:
let sessionDriver = SessionSQLiteDriver()
CouchDB
Importing the module, in Package.swift:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-CouchDB.git", majorVersion: 1)
Defining the CouchDB database to use for session storage:
SessionConfig.couchDatabase = "perfectsessions"
Defining the connection to the CouchDB server:
CouchDBConnection.host = "localhost"
CouchDBConnection.username = "username"
CouchDBConnection.password = "secret"
Defining the Session Driver:
let sessionDriver = SessionCouchDBDriver()
MongoDB
Importing the module, in Package.swift:
.Package(url:"https://github.com/PerfectlySoft/Perfect-Session-MongoDB.git", majorVersion: 1)
Defining the MongoDB database to use for session storage:
SessionConfig.mongoCollection = "perfectsessions"
Defining the connection to the MongoDB server:
MongoDBConnection.host = "localhost"
MongoDBConnection.database = "perfect_testing"
Defining the Session Driver:
let sessionDriver = SessionMongoDBDriver()CSRF (Cross Site Request Forgery) Security
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they''re currently authenticated. CSRF attacks
specifically target state-changing requests, not theft of data, since the attacker has no way to see the response to the forged request. With a little help of social engineering
(such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker''s choosing. If the victim is a normal user, a successful
CSRF attack can force the user to perform state changing requests like transferring funds, changing their email address, and so forth. If the victim is an administrative account, CSRF
can compromise the entire web application. [1] - (OWASP)
CSRF as an attack vector is often overlooked, and represents a significant "chaos" factor unless the validation is handled at the highest level: the framework. This allows web application
and API authors to have significant control over a vital layer of security.
The Perfect Sessions module includes support for CSRF configuration.
If you have included Perfect Sessions or any of its datasource-specific implementations in your Packages.swift file, you already have CSRF support.
Relevant Examples
Perfect-Session-Memory-Demo
Configuration
An example CSRF Configuration might look like this:
SessionConfig.CSRF.checkState = true
SessionConfig.CSRF.failAction = .fail
SessionConfig.CSRF.checkHeaders = true
SessionConfig.CSRF.acceptableHostnames.append("http://www.example.com")
SessionConfig.CSRF.requireToken = true
SessionConfig.CSRF.checkState
This is the "master switch". If enabled, CSRF will be enabled for all routes.
SessionConfig.CSRF.failAction
This specifies the action to take if the CSRF validation fails. The possible options are:
.fail - Execute an immediate halt. No further processing will be done, and an HTTP Status 406 Not Acceptable is generated.
.log - Processing will continue, however the event will be recorded in the log.
.none - Processing will continue, no action is taken.
SessionConfig.CSRF.acceptableHostnames
An array of host names that are compared in the following section for "origin" match acceptance.
SessionConfig.CSRF.checkHeaders
If the CORS.checkheader is configured as true , origin and host headers are checked for validity.
The Origin , Referrer or X-Forwarded-For headers must be populated ("origin").
If the "origin" is specified in SessionConfig.CSRF.acceptableHostnames , the CSRF check will continue to the next phase and the following checks are skipped.
The Host or X-Forwarded-Host header must be present ("host").
The "host" and "origin" values must match exactly.
SessionConfig.CSRF.requireToken
When set to true, this setting will enforce all POST requests to include a "_csrf" param, or if the content type header is "application/json" then an associated "X-CSRF-Token" header
must be sent with the request. The content of the header or parameter should match the request.session.data["csrf"] value. This value is set automatically at session start.
Session state
While not a configuration param, it is worth noting that if SessionConfig.CSRF.checkState is true, no POST request will be accepted if the session is "new". This is a deliberate
position supported by security recommendations.[1] - OWASP, Cross-Site Request Forgery (CSRF): https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)## CORS (Cross Origin Resource Sharing) Security
Cross-Origin Resource Sharing (CORS) is an important part of the "open web", and as such, no framework is complete without enabling support for CORS.
Monsur Hossain from html5rocks.com introduces CORS very effectively:
APIs are the threads that let you stitch together a rich web experience. But this experience has a hard time translating to the browser, where the options for cross-domain requests
are limited to techniques like JSON-P (which has limited use due to security concerns) or setting up a custom proxy (which can be a pain to set up and maintain).
Cross-Origin Resource Sharing (CORS) is a W3C spec that allows cross-domain communication from the browser. By building on top of the XMLHttpRequest object, CORS allows
developers to work with the same idioms as same-domain requests.
The use-case for CORS is simple. Imagine the site alice.com has some data that the site bob.com wants to access. This type of request traditionally wouldn’t be allowed under the
browser’s same origin policy. However, by supporting CORS requests, alice.com can add a few special response headers that allows bob.com to access the data.
As you can see from this example, CORS support requires coordination between both the server and client. Luckily, if you are a client-side developer you are shielded from most of
these details. The rest of this article shows how clients can make cross-origin requests, and how servers can configure themselves to support CORS.
The Perfect Sessions module includes support for CORS configuration, enabling your API and assets to be available or secured in the way you wish.
If you have included Perfect Sessions or any of its datasource-specific implementations in your Packages.swift file, you already have CORS support; however, it is off by default.
Relevant Examples
Perfect-Session-Memory-Demo
Configuration
// Enabled, true or false.
// Default is false.
SessionConfig.CORS.enabled = true
// Array of acceptable hostnames for incoming requests
// To enable CORS on all, have a single entry, *
SessionConfig.CORS.acceptableHostnames = ["*"]
// However if you wish to enable specific domains:
SessionConfig.CORS.acceptableHostnames.append("http://www.test-cors.org")
// Wildcards can also be used at the start or end of hosts
SessionConfig.CORS.acceptableHostnames.append("*.example.com")
SessionConfig.CORS.acceptableHostnames.append("http://www.domain.*")
// Array of acceptable methods
public var methods: [HTTPMethod] = [.get, .post, .put]
// An array of custom headers allowed
public var customHeaders = [String]()
// Access-Control-Allow-Credentials true/false.
// Standard CORS requests do not send or set any cookies by default.
// In order to include cookies as part of the request enable the client to do so by setting to true
public var withCredentials = false
// Max Age (seconds) of request / OPTION caching.
// Set to 0 for no caching (default)
public var maxAge = 3600
When a CORS request is submitted to the server, if there is no match then the CORS specific headers are not generated in the OPTIONS response, which will instruct the browser that it
cannot accept the resource.
If the server determines that CORS headers should be generated, the following headers are sent with the response:// An array of allowable HTTP Methods.
// In the case of the above configuration example:
Access-Control-Allow-Methods: GET, POST, PUT
// If the origin is acceptable the origin will be echoed back to the requester
// (even if configured with *)
Access-Control-Allow-Origin: http://www.test-cors.org
// If the server wishes cookies to be sent along with requests,
// this should return true
Access-Control-Allow-Credentials: true
// The length of time the OPTIONS request can be cached for.
Access-Control-Max-Age: 3600
An excellent resource for testing CORS entitlements and responses is available at http://www.test-cors.org# Perfect Local Authentication
This package provides Local Authentication libraries for projects that require locally stored and handled authentication.
Relevant Templates & Examples
A template application can be found at https://github.com/PerfectlySoft/Perfect-Local-Auth-PostgreSQL-Template, providing a fully functional starting point, as well as demonstrating the
usage of the system.
Adding to your project
Add this project as a dependency in your Package.swift file.
PostgreSQL Driver:
.Package(url: "https://github.com/PerfectlySoft/Perfect-LocalAuthentication-PostgreSQL.git", majorVersion: 1)
MySQL Driver:
.Package(url: "https://github.com/PerfectlySoft/Perfect-LocalAuthentication-MySQL.git", majorVersion: 1)
Configuration
It is important to configure the following in main.swift to set up database and session configuration:
Import the required modules:
import PerfectSession
import PerfectSessionPostgreSQL // Or PerfectSessionMySQL
import PerfectCrypto
import LocalAuthentication
Initialize PerfectCrypto:
let _ = PerfectCrypto.isInitialized
Now set some defaults:
// Used in email communications
// The Base link to your system, such as http://www.example.com/
var baseURL = ""
// Configuration of Session
SessionConfig.name = "perfectSession" // <-- change
SessionConfig.idle = 86400
SessionConfig.cookieDomain = "localhost" //<-- change
SessionConfig.IPAddressLock = false
SessionConfig.userAgentLock = false
SessionConfig.CSRF.checkState = true
SessionConfig.CORS.enabled = true
SessionConfig.cookieSameSite = .laxDetailed Session configuration documentation can be found at https://www.perfect.org/docs/sessions.html
The database and email configurations should be set as follows (if using JSON file config):
let opts = initializeSchema("./config/ApplicationConfiguration.json") // <-- loads base config like db and email configuration
httpPort = opts["httpPort"] as? Int ?? httpPort
baseURL = opts["baseURL"] as? String ?? baseURL
Otherwise, these will need to be set equivalent to one of these functions:
PostgreSQL: https://github.com/PerfectlySoft/Perfect-LocalAuthentication-PostgreSQL/blob/master/Sources/LocalAuthentication/Schema/InitializeSchema.swift
MySQL: https://github.com/PerfectlySoft/Perfect-LocalAuthentication-MySQL/blob/master/Sources/LocalAuthentication/Schema/InitializeSchema.swift
Set the session driver
PostgreSQL:
let sessionDriver = SessionPostgresDriver()
MySQL:
let sessionDriver = SessionMySQLDriver()
Request & Response Filters
The following two session filters need to be added to your server config:
PostgreSQL Driver:
// (where filter is a [[String: Any]] object)
filters.append(["type":"request","priority":"high","name":SessionPostgresFilter.filterAPIRequest])
filters.append(["type":"response","priority":"high","name":SessionPostgresFilter.filterAPIResponse])
MySQL Driver:
// (where filter is a [[String: Any]] object)
filters.append(["type":"request","priority":"high","name":SessionMySQLFilter.filterAPIRequest])
filters.append(["type":"response","priority":"high","name":SessionMySQLFilter.filterAPIResponse])
For example, see https://github.com/PerfectlySoft/Perfect-Local-Auth-PostgreSQL-Template/blob/master/Sources/PerfectLocalAuthPostgreSQLTemplate/configuration/Filters.swift
Add routes for login, register etc
The following routes can be added as needed or customized to add login, logout, register:
// Login
routes.append(["method":"get", "uri":"/login", "handler":Handlers.login]) // simply a serving of the login GET
routes.append(["method":"post", "uri":"/login", "handler":LocalAuthWebHandlers.login])
routes.append(["method":"get", "uri":"/logout", "handler":LocalAuthWebHandlers.logout])
// Register
routes.append(["method":"get", "uri":"/register", "handler":LocalAuthWebHandlers.register])
routes.append(["method":"post", "uri":"/register", "handler":LocalAuthWebHandlers.registerPost])
routes.append(["method":"get", "uri":"/verifyAccount/{passvalidation}", "handler":LocalAuthWebHandlers.registerVerify])
routes.append(["method":"post", "uri":"/registrationCompletion", "handler":LocalAuthWebHandlers.registerCompletion])
// JSON
routes.append(["method":"get", "uri":"/api/v1/session", "handler":LocalAuthJSONHandlers.session])
routes.append(["method":"get", "uri":"/api/v1/logout", "handler":LocalAuthJSONHandlers.logout])
routes.append(["method":"post", "uri":"/api/v1/register", "handler":LocalAuthJSONHandlers.register])
routes.append(["method":"login", "uri":"/api/v1/login", "handler":LocalAuthJSONHandlers.login])
An example can be found at https://github.com/PerfectlySoft/Perfect-Local-Auth-PostgreSQL-
Template/blob/master/Sources/PerfectLocalAuthPostgreSQLTemplate/configuration/Routes.swift
Testing for authentication:The user id can be accessed as follows:
request.session?.userid ?? ""
If a user id (i.e. logged in state) is required to access a page, code such as this could be used to detect and redirect:
let contextAuthenticated = !(request.session?.userid ?? "").isEmpty
if !contextAuthenticated { response.redirect(path: "/login") }
```# Perfect-SPNEGO [简体中⽂文](README.zh_CN.md)
This project provides a general server library which provides SPNEGO mechanism.
### Before Start
Perfect SPNEGO is aiming on a general application of Server Side Swift, so it could be plugged into *ANY* servers, such as HTTP / FTP / SSH, etc
.
Although it supports Perfect HTTP server natively, it could be applied to any other Swift servers as well.
Before attaching to any actual server applications, please make sure your server has been already configured with Kerberos V5.
### Xcode Build Note
If you would like to use Xcode to build this project, please make sure to pass proper linker flags to the Swift Package Manager:
$ swift package -Xlinker -framework -Xlinker GSS generate-xcodeproj ```
Linux Build Note
A special library called libkrb5-dev is required to build this project:
$ sudo apt-get install libkrb5-dev
If your server is a KDC, then you can skip this step, otherwise please install Kerberos V5 utilities:
$ sudo apt-get install krb5-user
KDC Configuration
Configure the application server''s /etc/krb5.conf to your KDC. The following sample configuration shows how to connect your application server to realm KRB5.CA under control of a
KDC named nut.krb5.ca :
[realms]
KRB5.CA = {
kdc = nut.krb5.ca
admin_server = nut.krb5.ca
}
[domain_realm]
.krb5.ca = KRB5.CA
krb5.ca = KRB5.CA
Prepare Kerberos Keys for Server
Contact to your KDC administrator to assign a keytab file to your application server.
Take example, SUPPOSE ALL HOSTS BELOW REGISTERED ON THE SAME DNS SERVER:
KDC server: nut.krb5.ca
Application server: coco.krb5.ca
Application server type: HTTP
In such a case, KDC administrator shall login on nut.krb5.ca then perform following operation:
kadmin.local: addprinc -randkey HTTP/coco.krb5.ca@KRB5.CA
kadmin.local: ktadd -k /tmp/krb5.keytab HTTP/coco.krb5.ca@KRB5.CA
Then please ship this krb5.keytab file securely and install on your application server coco.krb5.ca and move to folder /etc , then grant sufficient permissions to your swiftapplication to access it.
Quick Start
Add the following dependency to your project''s Package.swift file:
.Package(url: "https://github.com/PerfectlySoft/Perfect-SPNEGO.git", majorVersion: 1)
Then import Perfect-SPNEGO to your source code:
import PerfectSPNEGO
Connect to KDC
Use the key in your default keytab /etc/krb5.keytab file on application server to register your application server to the KDC.
let spnego = try Spnego("HTTP@coco.krb5.ca")
Please note that the host name and protocol type MUST match the records listed in the keytab file.
Respond to A Spnego Challenge
Once initialized, object spnego could respond to the challenges. Take example, if a user is trying to connect to the application server as:
$ kinit rocky@KRB5.CA
$ curl --negotiate -u : http://coco.krb5.ca
In this case, the curl command would possibly send a base64 encoded challenge in the HTTP header:
> Authorization: Negotiate YIICnQYGKwYBBQUCoIICkTCCAo2gJzAlBgkqhkiG9xIBAgI ...
Once received such a challenge, you could apply this base64 string to the spnego object:
let (user, response) = try spnego.accept(base64Token: "YIICnQYGKwYBBQUCoIICkTCCAo2gJzAlBgkqhkiG9xIBAgI...")
If succeeded, the user would be "rocky@KRB5.CA". The variable response might be nil which indicates nothing is required to reply such a token, otherwise you should send this
response back to the client.
Up till now, your application had already got the user information and request, then the application server might decide if this user could access the objective resource or not, according to
your ACL (access control list) configuration.
Relevant Examples
A good example to demonstrate how to use SPNEGO in a Perfect HTTP Server can be found from: * Perfect-Spnego-Demo# JSON Converter
Perfect includes basic JSON encoding and decoding functionality. JSON encoding is provided through a series of extensions on many of the built-in Swift data types. Decoding is
provided through an extension on the Swift String type.
It seems important to note that although Perfect provides this particular JSON encoding/decoding system, it is not required that your application uses it. Feel free to import your own
favourite JSON-related functionality.
To use this system, first ensure that PerfectLib is imported:
import PerfectLib
Encoding To JSON Data
You can convert any of the following types directly into JSON string data:
String
Int
UInt
Double
Bool
ArrayDictionary
Optional
Custom classes which inherit from JSONConvertibleObject
Note that only Optionals which contain any of the above types are directly convertible. Optionals which are nil will output as a JSON "null".
To encode any of these values, call the jsonEncodedString() function which is provided as an extension on the objects. This function may potentially throw a
JSONConversionError.notConvertible error.
Example:
let scoreArray: [String:Any] = ["1st Place": 300, "2nd Place": 230.45, "3rd Place": 150]
let encoded = try scoreArray.jsonEncodedString()
The result of the encoding would be the following String:
{"2nd Place":230.45,"1st Place":300,"3rd Place":150}
Decoding JSON Data
String objects which contain JSON string data can be decoded by using the jsonDecode() function. This function can throw a JSONConversionError.syntaxError error if
the String does not contain valid JSON data.
let encoded = "{\"2nd Place\":230.45,\"1st Place\":300,\"3rd Place\":150}"
let decoded = try encoded.jsonDecode() as? [String:Any]
Decoding the String will produce the following dictionary:
["2nd Place": 230.44999999999999, "1st Place": 300, "3rd Place": 150]
Though decoding a JSON string can produce any of the permitted values, it is most common to deal with JSON objects (dictionaries) or arrays. You will need to cast the resulting value
to the expected type.
Using the Decoded Data
Because decoded dictionaries or arrays are always of type [String:Any] or [Any], respectively, you will need to cast the contained values to usable types. For example:
var firstPlace = 0
var secondPlace = 0.0
var thirdPlace = 0
let encoded = "{\"2nd Place\":230.45,\"1st Place\":300,\"3rd Place\":150}"
guard let decoded = try encoded.jsonDecode() as? [String:Any] else {
return
}
for (key, value) in decoded {
switch key {
case "1st Place":
firstPlace = value as! Int
case "2nd Place":
secondPlace = value as! Double
case "3rd Place":
thirdPlace = value as! Int
default:
break
}
}
print("The top scores are: \r" + "First Place: " + "\(firstPlace)" + " Points\r" + "Second Place: " + "\(secondPlace)" + " Points\r" + "Third Pl
ace: " + "\(thirdPlace)" + " Points")
The output would be the following:
The top scores are:
First Place: 300 Points
Second Place: 230.45 Points
Third Place: 150 PointsDecoding Empty Values from JSON Data
As JSON null values are untyped, the system will substitute a JSONConvertibleNull in place of all JSON nulls. Example:
let jsonString = "{\"1st Place\":300,\"4th place\":null,\"2nd Place\":230.45,\"3rd Place\":150}"
if let decoded = try jsonString.jsonDecode() as? [String:Any] {
for (key, value) in decoded {
if let value as? JSONConvertibleNull {
print("The key \"\(key)\" had a null value")
}
}
}
The output would be:
The key "4th place" had a null value
JSON Convertible Object
Perfect''s JSON system provides the facilities for encoding and decoding custom classes. Any eligible class must inherit from the JSONConvertibleObject base class which is defined as
follows:
/// Base for a custom object which can be converted to and from JSON.
public class JSONConvertibleObject: JSONConvertible {
/// Default initializer.
public init() {}
/// Get the JSON keys/value.
public func setJSONValues(_ values:[String:Any]) {}
/// Set the object properties based on the JSON keys/values.
public func getJSONValues() -> [String:Any] { return [String:Any]() }
/// Encode the object into JSON text
public func jsonEncodedString() throws -> String {
return try self.getJSONValues().jsonEncodedString()
}
}
Any object wishing to be JSON encoded/decoded must first register itself with the system. This registration should take place once when your application starts up. Call the
JSONDecoding.registerJSONDecodable function to register your object. This function is defined as follows:
public class JSONDecoding {
/// Function which returns a new instance of a custom object which will have its members set based on the JSON data.
public typealias JSONConvertibleObjectCreator = () -> JSONConvertibleObject
static public func registerJSONDecodable(name: String, creator: JSONConvertibleObjectCreator)
}
Registering an object requires a unique name which can be any string, provided it is unique. It also requires a "creator" function which returns a new instance of the object in question.
When the system encodes a JSONConvertibleObject it calls the object''s getJSONValues function. This function should return a [String:Any] dictionary containing the names
and values for any properties which should be encoded into the resulting JSON string. This dictionary must also contain a value identifying the object type. The value must match the
name by which the object was originally registered. The dictionary key for the value is identified by the JSONDecoding.objectIdentifierKey property.
When the system decodes such an object, it will find the JSONDecoding.objectIdentifierKey value and look up the object creator which had been previously registered. It will
create a new instance of the type by calling that function, and will then call the new object''s setJSONValues(_ values:[String:Any]) function. It will pass in a dictionary
containing all of the deconverted values. These values will match those previously returned by the getJSONValues function when the object was first converted. Within the
setJSONValues function, the object should retrieve all properties which it wants to reinstate.
The following example defines a custom JSONConvertibleObject and converts it to a JSON string. It then decodes the object and compares it to the original. Note that this
example object calls the convenience function getJSONValue , which will pull a named value from the dictionary and permits providing a default value which will be returned if the
dictionary does not contain the indicated key.
This example is split up into several sections.
Define the class:class User: JSONConvertibleObject {
static let registerName = "user"
var firstName = ""
var lastName = ""
var age = 0
override func setJSONValues(_ values: [String : Any]) {
self.firstName = getJSONValue(named: "firstName", from: values, defaultValue: "")
self.lastName = getJSONValue(named: "lastName", from: values, defaultValue: "")
self.age = getJSONValue(named: "age", from: values, defaultValue: 0)
}
override func getJSONValues() -> [String : Any] {
return [
JSONDecoding.objectIdentifierKey:User.registerName,
"firstName":firstName,
"lastName":lastName,
"age":age
]
}
}
Register the class:
// do this once
JSONDecoding.registerJSONDecodable(name: User.registerName, creator: { return User() })
Encode the object:
let user = User()
user.firstName = "Donnie"
user.lastName = "Darko"
user.age = 17
let encoded = try user.jsonEncodedString()
The value of "encoded" will look as follows:
{"lastName":"Darko","age":17,"_jsonobjid":"user","firstName":"Donnie"}
Decode the object:
guard let user2 = try encoded.jsonDecode() as? User else {
return // error
}
// check the values
XCTAssert(user.firstName == user2.firstName)
XCTAssert(user.lastName == user2.lastName)
XCTAssert(user.age == user2.age)
Note that the JSONDecoding.objectIdentifierKey (the value of which is "_jsonobjid") key/value pair that identifies the Swift object to be decoded must be in the incoming JSON
string. If it is not present then the result of jsonDecode will be a regular Dictionary instead of the intended Swift object.
JSON Conversion Error
As an object is converted to or from a JSON string, the process may throw a JSONConversionError . This is defined as follows:
/// An error occurring during JSON conversion.
public enum JSONConversionError: ErrorProtocol {
/// The object did not suppport JSON conversion.
case notConvertible(Any)
/// A provided key was not a String.
case invalidKey(Any)
/// The JSON text contained a syntax error.
case syntaxError
}
Static File ContentAs seen in the Routing chapter, Perfect is capable of complex dynamic routing. It is also capable of serving static content including HTML, images, CSS, and JavaScript.
Static file content serving is handled through the StaticFileHandler object. Once a request is given to an instance of this object, it will handle finding the indicated file or returning
a 404 if it does not exist. StaticFileHandler also handles caching through use of the ETag header as well as byte range serving for very large files.
A StaticFileHandler object is initialized with a document root path parameter. This root path forms the prefix to which all file paths will be appended. The current HTTPRequest''s
path property will be used to indicate the path to the file which should be read and returned.
StaticFileHandler can be directly used by creating one in your web handler and calling its handleRequest method.
For example, a handler which simply returns the request file might look as follows:
{
request, response in
StaticFileHandler(documentRoot: request.documentRoot).handleRequest(request: request, response: response)
}
However, unless custom behaviour is required, it is not necessary to handle this manually. Setting the server''s documentRoot property will automatically install a handler which will
serve any files from the indicated directory. Setting the server''s document root is analogous to the following snippet:
let dir = Dir(documentRoot)
if !dir.exists {
try Dir(documentRoot).create()
}
routes.add(method: .get, uri: "/**", handler: {
request, response in
StaticFileHandler(documentRoot: request.documentRoot).handleRequest(request: request, response: response)
})
The handler that gets installed will serve any files from the root or any sub-directories contained therein.
An example of the documentRoot property usage is found in the PerfectTemplate:import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
// Create HTTP server.
let server = HTTPServer()
// Register your own routes and handlers
var routes = Routes()
routes.add(method: .get, uri: "/", handler: {
request, response in
response.appendBody(string: "...")
response.completed()
}
)
// Add the routes to the server.
server.addRoutes(routes)
// Set a listen port of 8181
server.serverPort = 8181
// Set a document root.
// This is optional. If you do not want to serve
// static content then do not set this.
// Setting the document root will automatically add a
// static file handler for the route
server.documentRoot = "./webroot"
// Gather command line options and further configure the server.
// Run the server with --help to see the list of supported arguments.
// Command line arguments will supplant any of the values set above.
configureServer(server)
do {
// Launch the HTTP server.
try server.start()
} catch PerfectError.networkError(let err, let msg) {
print("Network error thrown: \(err) \(msg)")
}
Note the server.documentRoot = "./webroot" line. It means that if there is a styles.css document in the specified webroot directory, then a request to the URI "/styles.css" will
return that file to the browser.
The following example establishes a virtual documents path, serving all URIs which begin with "/files" from the physical directory "/var/www/htdocs":
routes.add(method: .get, uri: "/files/**", handler: {
request, response in
// get the portion of the request path which was matched by the wildcard
request.path = request.urlVariables[routeTrailingWildcardKey]
// Initialize the StaticFileHandler with a documentRoot
let handler = StaticFileHandler(documentRoot: "/var/www/htdocs")
// trigger the handling of the request,
// with our documentRoot and modified path set
handler.handleRequest(request: request, response: response)
}
)
In the route example above, a request to "/files/foo.html" would return the corresponding file "/var/www/htdocs/foo.html".
Mustache Template Support
Mustache is a logic-less templating system. It permits you to use pre-written text files with placeholders that will be replaced at run-time with values particular to a given request.
For more general information on Mustache, consult the mustache specification.
To use this module, add this project as a dependency in your Package.swift file..Package(
url: "https://github.com/PerfectlySoft/Perfect-Mustache.git",
majorVersion: 2
)
Then import the Mustache Module in your source code before using:
import PerfectMustache
Mustache templates can be used in either an HTTP server handler or standalone with no server.
Mustache Server Handler
To use a mustache template as an HTTP response, you will need to create a handler object which conforms to MustachePageHandler . These handler objects generate the values
which the template processor will use to produce its content.
/// A mustache handler, which should be passed to `mustacheRequest`, generates values to fill a mustache template
/// Call `context.extendValues(with: values)` one or more times and then
/// `context.requestCompleted(withCollector collector)` to complete the request and output the resulting content to the client.
public protocol MustachePageHandler {
/// Called by the system when the handler needs to add values for the template.
func extendValuesForResponse(context contxt: MustacheWebEvaluationContext, collector: MustacheEvaluationOutputCollector)
}
The template page handler, which you would implement, might look like the following:
struct TestHandler: MustachePageHandler { // all template handlers must inherit from PageHandler
// This is the function which all handlers must impliment.
// It is called by the system to allow the handler to return the set of values which will be used when populating the template.
// - parameter context: The MustacheWebEvaluationContext which provides access to the HTTPRequest containing all the information pertaining
to the request
// - parameter collector: The MustacheEvaluationOutputCollector which can be used to adjust the template output. For example a `defaultEncod
ingFunc` could be installed to change how outgoing values are encoded.
func extendValuesForResponse(context contxt: MustacheWebEvaluationContext, collector: MustacheEvaluationOutputCollector) {
var values = MustacheEvaluationContext.MapType()
values["value"] = "hello"
/// etc.
contxt.extendValues(with: values)
do {
try contxt.requestCompleted(withCollector: collector)
} catch {
let response = contxt.webResponse
response.status = .internalServerError
response.appendBody(string: "\(error)")
response.completed()
}
}
}
To direct a web request to a Mustache template, call the mustacheRequest function. This function is defined as follows:
public func mustacheRequest(request req: HTTPRequest, response: HTTPResponse, handler: MustachePageHandler, templatePath: String)
Pass to this function the current request and response objects, your MustachePageHandler , and the path to the template file you wish to serve. mustacheRequest will perform
the initial steps such as creating the Mustache template parser, locating the template file, and calling your Mustache handler to generate the values which will be used when completing
the template content.
The following snippet illustrates how to use a Mustache template in your URL handler. In this example, the template named "test.html" would be located in your server''s web root
directory:
{
request, response in
let webRoot = request.documentRoot
mustacheRequest(request: request, response: response, handler: TestHandler(), templatePath: webRoot + "/test.html")
}
Look at the UploadEnumerator example for a more concrete example.Standalone Usage
It is possible to use this Mustache processor in a non-web, standalone manner. You can accomplish this by either providing the path to a template file, or by supplying the template data
as a string. In either case, the template content will be parsed, and any values that you supply will be filled in.
The first example uses raw template text as the source. The second example passes in a file path for the template:
let templateText = "TOP {\n{{#name}}\n{{name}}{{/name}}\n}\nBOTTOM"
let d = ["name":"The name"] as [String:Any]
let context = MustacheEvaluationContext(templateContent: templateText, map: d)
let collector = MustacheEvaluationOutputCollector()
let responseString = try context.formulateResponse(withCollector: collector)
XCTAssertEqual(responseString, "TOP {\n\nThe name\n}\nBOTTOM")
let templatePath = "path/to/template.mustache"
let d = ["name":"The name"] as [String:Any]
let context = MustacheEvaluationContext(templatePath: templatePath, map: d)
let collector = MustacheEvaluationOutputCollector()
let responseString = try context.formulateResponse(withCollector: collector)
Tag Support
This Mustache template processor supports:
{{regularTags}}
{{{unencodedTags}}}
{{# sections}} ... {{/sections}}
{{^ invertedSections}} ... {{/invertedSections}}
{{! comments}}
{{> partials}}
lambdas
Partials
All files used for partials must be located in the same directory as the calling template. Additionally, all partial files must have the file extension of .mustache, but this extension must not
be included in the partial tag itself. For example, to include the contents of the file foo.mustache, you would use the tag {{> foo }} .
Encoding
By default, all encoded tags (i.e. regular tags) are HTML-encoded, and < & > entities will be escaped. In your handler you can manually set the
MustacheEvaluationOutputCollector.defaultEncodingFunc function to perform whatever encoding you need. For example, when outputting JSON data you would want to
set this function to something like the following:
collector.defaultEncodingFunc = {
string in
return (try? string.jsonEncodedString()) ?? "bad string"
}
Lambdas
Functions can be added to the values dictionary. These will be executed and the results will be added to the template output. Such functions should have the following signature:
(tag: String, context: MustacheEvaluationContext) -> String
The tag parameter will be the tag name. For example, the tag {{name}} would give you the value "name" for the tag parameter.
Perfect-Markdown
This project provides a solution to convert markdown text into html presentations.
This package builds with Swift Package Manager and is part of the Perfect project but can also be used as an independent module.
Acknowledgement
Perfect-Markdown is directly building on GerHobbelt''s "upskirt" project.Swift Package Manager
Add dependencies to your Package.swift
.Package(url: "https://github.com/PerfectlySoft/Perfect-Markdown.git", majorVersion: 1)
Import Perfect Markdown Library
Add the following header to your swift source code:
import PerfectMarkdown
Get HTML from Markdown Text
Once imported, a new String extension markdownToHTML would be available:
let markdown = "# some blah blah blah markdown text \n\n## with mojo ! " "
guard let html = markdown.markdownToHTML else {
// conversion failed
}//end guard
print(html)
HTTP Request Logging
To log HTTP requests to a file, use the Perfect-RequestLogger module.
Relevant Examples
Perfect-HTTPRequestLogging
Perfect-Session-Memory-Demo
Usage
Add the following dependency to the Package.swift file:
.Package(url: "https://github.com/PerfectlySoft/Perfect-RequestLogger.git", majorVersion: 1)
In each file you wish to implement logging, import the module:
import PerfectRequestLogger
When using PerfectHTTP 2.1 or later
Add to main.swift after instantiating your server :// Instantiate a logger
let httplogger = RequestLogger()
// Configure Server
var confData: [String:[[String:Any]]] = [
"servers": [
[
"name":"localhost",
"port":8181,
"routes":[],
"filters":[
[
"type":"response",
"priority":"high",
"name":PerfectHTTPServer.HTTPFilter.contentCompression,
],
[
"type":"request",
"priority":"high",
"name":RequestLogger.filterAPIRequest,
],
[
"type":"response",
"priority":"low",
"name":RequestLogger.filterAPIResponse,
]
]
]
]
]
The important parts of the configuration spec to add for enabling the Request Logger are:
[
"type":"request",
"priority":"high",
"name":RequestLogger.filterAPIRequest,
],
[
"type":"response",
"priority":"low",
"name":RequestLogger.filterAPIResponse,
]
These request & response filters add the required hooks to mark the beginning and the completion of the HTTP request and response.
When using PerfectHTTP 2.0
Add to main.swift after instantiating your server :
// Instantiate a logger
let httplogger = RequestLogger()
// Add the filters
// Request filter at high priority to be executed first
server.setRequestFilters([(httplogger, .high)])
// Response filter at low priority to be executed last
server.setResponseFilters([(httplogger, .low)])
These request & response filters add the required hooks to mark the beginning and the completion of the HTTP request and response.
Setting a custom Logfile location
The default logfile location is /var/log/perfectLog.log . To set a custom logfile location, set the RequestLogFile.location property:
RequestLogFile.location = "/var/log/myLog.log"Example Log Output
[INFO] [62f940aa-f204-43ed-9934-166896eda21c] [servername/WuAyNIIU-1] 2016-10-07 21:49:04 +0000 "GET /one HTTP/1.1" from 127.0.0.1 - 200 64B in
0.000436007976531982s
[INFO] [ec6a9ca5-00b1-4656-9e4c-ddecae8dde02] [servername/WuAyNIIU-2] 2016-10-07 21:49:06 +0000 "GET /two HTTP/1.1" from 127.0.0.1 - 200 64B in
0.000207006931304932s
This module expands on earlier work by David Fleming.
WebSockets
WebSockets are designed as full-duplex communication channels over a single TCP connection. The WebSocket protocol facilitates the real-time data transfer from and to a server. This
is made possible by providing a standardized way for the server to send content to the browser without being solicited by the client, and allowing for messages to be passed back and
forth while keeping the connection open. In this way, a bi-directional (two-way) ongoing conversation can take place between browser and server. The communications are typically done
over standard TCP ports, such as 80 or 443.
The WebSocket protocol is currently supported in most major browsers including Google Chrome, Microsoft Edge, Internet Explorer, Firefox, Safari and Opera. WebSockets also require
support from web applications on the server.
Relevant Examples
Perfect-Chat-Demo
Perfect-WebSocketsServer
Getting started
Add the WebSocket dependency to your Package.swift file:
.Package(url:"https://github.com/PerfectlySoft/Perfect-WebSockets.git", majorVersion: 2)
Then import the WebSocket library into your Swift source code as needed:
import PerfectWebSockets
A typical scenario is communication inside a web page like a chat room where multiple users are interacting in near-real time.
This example sets up a WebSocket service handler interacting on the route /echo :
var routes = Routes()
routes.add(method: .get, uri: "/echo", handler: {
request, response in
// Provide your closure which will return the service handler.
WebSocketHandler(handlerProducer: {
(request: HTTPRequest, protocols: [String]) -> WebSocketSessionHandler? in
// Check to make sure the client is requesting our "echo" service.
guard protocols.contains("echo") else {
return nil
}
// Return our service handler.
return EchoHandler()
}).handleRequest(request: request, response: response)
}
)
Handling WebSocket Sessions
A WebSocket service handler must implement the WebSocketSessionHandler protocol.
This protocol requires the function handleSession(request: HTTPRequest, socket: WebSocket) . This function will be called once the WebSocket connection has been
established, at which point it is safe to begin reading and writing messages.The initial HTTPRequest object which instigated the session is provided for reference.
Messages are transmitted through the provided WebSocket object.
Call WebSocket.sendStringMessage or WebSocket.sendBinaryMessage to send data to the client.
Call WebSocket.readStringMessage or WebSocket.readBinaryMessage to read data from the client.
By default, reading will block indefinitely until a message arrives or a network error occurs.
A read timeout can be set with WebSocket.readTimeoutSeconds .
Close the session using WebSocket.close() .
The example EchoHandler consists of the following:
class EchoHandler: WebSocketSessionHandler {
// The name of the super-protocol we implement.
// This is optional, but it should match whatever the client-side WebSocket is initialized with.
let socketProtocol: String? = "echo"
// This function is called by the WebSocketHandler once the connection has been established.
func handleSession(request: HTTPRequest, socket: WebSocket) {
// Read a message from the client as a String.
// Alternatively we could call `WebSocket.readBytesMessage` to get the data as an array of bytes.
socket.readStringMessage {
// This callback is provided:
// the received data
// the message''s op-code
// a boolean indicating if the message is complete
// (as opposed to fragmented)
string, op, fin in
// The data parameter might be nil here if either a timeout
// or a network error, such as the client disconnecting, occurred.
// By default there is no timeout.
guard let string = string else {
// This block will be executed if, for example, the browser window is closed.
socket.close()
return
}
// Print some information to the console for informational purposes.
print("Read msg: \(string) op: \(op) fin: \(fin)")
// Echo the data received back to the client.
// Pass true for final. This will usually be the case, but WebSockets has
// the concept of fragmented messages.
// For example, if one were streaming a large file such as a video,
// one would pass false for final.
// This indicates to the receiver that there is more data to come in
// subsequent messages but that all the data is part of the same logical message.
// In such a scenario one would pass true for final only on the last bit of the video.
socket.sendStringMessage(string, final: true) {
// This callback is called once the message has been sent.
// Recurse to read and echo new message.
self.handleSession(request, socket: socket)
}
}
}
}
FastCGI Caveat
WebSockets serving is only supported with the stand-alone Perfect HTTP server. At this time, the WebSocket server does not operate with the Perfect FastCGI connector.
WebSocket Classenum OpcodeType
WebSocket messages can be various types: continuation, text, binary, close, ping or pong, or invalid types.
var readTimeoutSeconds
When trying to read a message from the current socket, this property helps the socket to read a message before timeout. If this property has been set to NetEvent.noTimeout (-1), it will
wait infinitely.
read message
There are two ways of reading messages: text or binary, which differ only in the data type returned, i.e. String and [UInt8] array respectively.
read text message:
public func readStringMessage(continuation: @escaping (String?, _ opcode: OpcodeType, _ final: Bool) -> ())
read binary message:
public func readBytesMessage(continuation: @escaping ([UInt8]?, _ opcode: OpcodeType, _ final: Bool) -> ())
There are three parameters when it calls back:
String / [UInt8]
readMessage will deliver the text / binary data sent from the client to your closure by this parameter.
opcode
Use opcode if you want more controls in the communication.
final
This parameter indicates whether the message is completed or fragmented.
send message
There are two ways of sending messages: text or binary, which differ only in the data type sent, i.e. String and [UInt8] array respectively.
send text message:
public func sendStringMessage(string: String, final: Bool, completion: @escaping () -> ())
send binary message:
public func sendBinaryMessage(bytes: [UInt8], final: Bool, completion: @escaping () -> ())
Parameter final indicates whether the message is completed or fragmented.
ping & pong
Perfect WebSocket also provides a convenient way of testing the connection. The ping method starts the test and expects a pong back.
Check out these two methods:
/// Send a "pong" message to the client.
public func sendPong(completion: @escaping () -> ())
/// Send a "ping" message to the client.
/// Expect a "pong" message to follow.
public func sendPing(completion: @escaping () -> ())
func close()
To close the WebSocket connection:socket.close()
For a Perfect WebSockets server example, visit the Perfect-WebSocketsServer demo.
Perfect Utilities
In addition to the core Web Service-related functionality, Perfect provides a range of fundamental building blocks with which to build server side and desktop applications.
Like the JSON library discussed in previous chapters, many of these functions can be achieved by Swift''s provided functions; however, Perfect''s APIs aim to increase the efficiency,
readability and simplicity of the end user''s code.
Consult the followings sections for detailed information on the utilities available in Perfect''s libraries:
Bytes
File
Dir
Threading
UUID
SysProcess
Log
CURL
Zip# Bytes
The bytes object provides simple streaming of common Swift values to and from a UInt8 array. It supports importing and exporting UInt8, UInt16, UInt32, and UInt64 values. When
importing these values, they are appended to the end of the contained array. When exporting, a repositionable marker is kept indicating the current export location. The bytes object is
included as part of PerfectLib. Make sure to import PerfectLib if you wish to use the Bytes object.
The primary purpose behind the bytes object is to enable binary network payloads to be easily assembled and decomposed. The resulting UInt8 array is available through the data
property.
The following example illustrates importing values of various sizes, and then exporting and validating the values. It also shows how to reposition the marker and how to determine how
many bytes remain for export.
let i8 = 254 as UInt8
let i16 = 54045 as UInt16
let i32 = 4160745471 as UInt32
let i64 = 17293541094125989887 as UInt64
let bytes = Bytes()
bytes.import64Bits(from: i64)
.import32Bits(from: i32)
.import16Bits(from: i16)
.import8Bits(from: i8)
let bytes2 = Bytes()
bytes2.importBytes(from: bytes)
XCTAssert(i64 == bytes2.export64Bits())
XCTAssert(i32 == bytes2.export32Bits())
XCTAssert(i16 == bytes2.export16Bits())
bytes2.position -= sizeof(UInt16.self)
XCTAssert(i16 == bytes2.export16Bits())
XCTAssert(bytes2.availableExportBytes == 1)
XCTAssert(i8 == bytes2.export8Bits())
File Operations
Perfect brings file system operations into your sever-side Swift environment to control how data is stored and retrieved in an accessible way.
First, ensure the PerfectLib is imported in your Swift file:
import PerfectLib
You are now able to use the File object to query and manipulate the file system.Setting Up a File Object Reference
Specify the absolute or relative path to the file:
let thisFile = File("/path/to/file/helloWorld.txt")
If you are not familiar with the file path, please read Directory Operations first.
Opening a File for Read or Write Access
Important: Before writing to a file — even if it is a new file — it must be opened with the appropriate permissions.
To open a file:
try thisFile.open(,permissions:)
For example, to write a file:
let thisFile = File("helloWorld.txt")
try thisFile.open(.readWrite)
try thisFile.write(string: "Hello, World!")
thisFile.close()
For full outlines of OpenMode and PermissionMode values, see their definitions later in this document.
Checking If a File Exists
Use the exists method to return a Boolean value.
thisFile.exists
Get the Modification Time for a File
Return the modification date for the file in the standard UNIX format of seconds since 1970/01/01 00:00:00 GMT, as an integer using:
thisFile.modificationTime
File Paths
Regardless of how a file reference was defined, both the absolute (real) and internal path can be returned.
Return the "internal reference" file path:
thisFile.path
Return the "real" file path. If the file is a symbolic link, the link will be resolved:
thisFile.realPath
Closing a File
Once a file has been opened for read or write, it is advisable to either close it at a specific place within the code, or by using defer :
let thisFile = File("/path/to/file/helloWorld.txt")
// Your processing here
thisFile.close()
Deleting a File
To remove a file from the file system, use the delete() method.
thisFile.delete()
This also closes the file, so there is no need to invoke an additional close() method.Returning the Size of a File
size returns the size of the file in bytes, as an integer.
thisFile.size
Determining if the File is a Symbolic Link
If the file is a symbolic link, the method will return Boolean true , otherwise false .
thisFile.isLink
Determining if the File Object is a Directory
If the file object refers instead to a directory, isDir will return either a Boolean true or false value.
thisFile.isDir
Returning the File''s UNIX Permissions
perms returns the UNIX style permissions for the file as a PermissionMode object.
thisFile.perms
For example:
print(thisFile.perms)
>> PermissionMode(rawValue: 29092)
Reading Contents of a File
readSomeBytes
Reads up to the indicated number of bytes from the file:
let thisFile = File("/path/to/file/helloWorld.txt")
let contents = try thisFile.readSomeBytes(count: )
Parameters
To read a specific byte range of a file''s contents, enter the number of bytes you wish to read. For example, to read the first 10 bytes of a file:
let thisFile = File("/path/to/file/helloWorld.txt")
let contents = try thisFile.readSomeBytes(count: 10)
print(contents)
>> [35, 32, 80, 101, 114, 102, 101, 99, 116, 84]
readString
readString reads the entire file as a string:
let thisFile = File("/path/to/file/helloWorld.txt")
let contents = try thisFile.readString()
Writing, Copying, and Moving Files
Important: Before writing to a file — even if it is a new file — it must be opened with the appropriate permissions.
Writing a String to a File
Use write to create or rewrite a string to the file using UTF-8 encoding. The method returns an integer which is the number of bytes written.
Note that this method uses the @discardableResult property, so it can be used without assignment if required.let bytesWritten = try thisFile.write(string: )
Writing a Bytes Array to a File
An array of bytes can also be written directly to a file. The method returns an integer which is the number of bytes written.
Note that this method uses the @discardableResult property, so it can be used without assignment if required.
let bytesWritten = try thisFile.write(
bytes: <[UInt8]>,
dataPosition: ,
length:
)
Parameters
bytes: The array of UInt8 to write
dataPosition: Optional. The offset within bytes at which to begin writing
length: Optional. The number of bytes to write
Moving a File
Once a file is defined, the moveto method can be used to relocate the file to a new location in the file system. This can also be used to rename a file if desired. The operation returns
a new file object representing the new location.
let newFile = thisFile.moveTo(path: , overWrite: )
Parameters
path: The path to move the file to
overWrite: Optional. Indicates that any existing file at the destination path should first be deleted. Default is false
Error Handling
The method throws PerfectError.FileError on error.
let thisFile = File("/path/to/file/helloWorld.txt")
let newFile = try thisFile.moveTo(path: "/path/to/file/goodbyeWorld.txt")
Copying a File
Similar to moveTo , copyTo copies the file to the new location, optionally overwriting any existing file. However, it does not delete the original file. A new file object is returned
representing the new location.
Note that this method uses the @discardableResult property, so it can be used without assignment if required.
let newFile = thisFile.copyTo(path: , overWrite: )
Parameters
path: The path to copy the file to
overWrite: Optional. Indicates that any existing file at the destination path should first be deleted. Default is false
Error Handling
The method throws PerfectError.FileError on error.
let thisFile = File("/path/to/file/helloWorld.txt")
let newFile = try thisFile.copyTo(path: "/path/to/file/goodbyeWorld.txt")
File Locking Functions
The file locking functions allow sections of a file to be locked with advisory-mode locks.All the locks for a file are removed when the file is closed or the process terminates.
Note: These are not file system locks, and do not prevent others from performing write operations on the affected files: The locks are "advisory-mode locks".
Locking a File
Attempts to place an advisory lock starting from the current position marker up to the indicated byte count. This function will block the current thread until the lock can be performed.
let result = try thisFile.lock(byteCount: )
Unlocking a File
Unlocks the number of bytes starting from the current position marker up to the indicated byte count.
let result = try thisFile.unlock(byteCount: )
Attempt to Lock a File
Attempts to place an advisory lock starting from the current position marker up to the indicated byte count. This function will throw an exception if the file is already locked, but will not
block the current thread.
let result = try thisFile.tryLock(byteCount: )
Testing a Lock
Tests if the indicated bytes are locked. Returns a Boolean true or false.
let isLocked = try thisFile.testLock(byteCount: )
File OpenMode
The OpenMode of a file is defined as an enum:
.read: Opens the file for read-only access
.write: Opens the file for write-only access, creating the file if it did not exist
.readWrite: Opens the file for read-write access, creating the file if it did not exist
.append: Opens the file for read-write access, creating the file if it did not exist and moving the file marker to the end
.truncate: Opens the file for read-write access, creating the file if it did not exist and setting the file''s size to zero
For example, to write a file:
let thisFile = File("helloWorld.txt")
try thisFile.open(.readWrite)
try thisFile.write(string: "Hello, World!")
thisFile.close()
File PermissionMode
The PermissionMode for a directory or file is provided as a single option or as an array of options.
For example, to create a directory with specific permissions:
let thisDir = Dir("/path/to/dir/")
do {
try thisDir.create(perms: [.rwxUser, .rxGroup, .rxOther])
} catch {
print("error")
}
//or
do {
try thisDir.create(perms: .rwxUser)
} catch {
print("error")
}PermissionMode Options
.readUser : Readable by user
.writeUser : Writable by user
.executeUser : Executable by user
.readGroup : Readable by group
.writeGroup : Writable by group
.executeGroup : Executable by group
.readOther : Readable by others
.writeOther : Writable by others
.executeOther : Executable by others
.rwxUser : Read, write, execute by user
.rwUserGroup : Read, write by user and group
.rxGroup : Read, execute by group
.rxOther : Read, execute by other
Directory Operations
Perfect brings file system operations into your sever-side Swift environments to control how data is stored and retrieved in an accessible way.
Relevant Examples
Perfect-Directory-Lister
Perfect-FileHandling
Usage
First, ensure the PerfectLib is imported in your Swift file:
import PerfectLib
You are now able to use the Dir object to query and manipulate the file system.
Setting Up a Directory Object Reference
Specify the absolute or relative path to the directory:
let thisDir = Dir("/path/to/directory/")
Checking If a Directory Exists
Use the exists method to return a Boolean value.
let thisDir = Dir("/path/to/directory/")
thisDir.exists
Returning the Current Directory Object''s Name
name returns the name of the object''s directory. Note that this is different from the "path".
thisDir.name
Returning the Parent Directory
parentDir returns a Dir object representing the current directory object''s parent. Returns nil if there is no parent.
let thisDir = Dir("/path/to/directory/")
let parent = thisDir.parentDir
Revealing the Directory Path
path returns the path to the current directory.let thisDir = Dir("/path/to/directory/")
let path = thisDir.path
Returning the Directory''s UNIX Permissions
perms returns the UNIX style permissions for the directory as a PermissionMode object.
thisDir.perms
For example:
print(thisDir.perms)
>> PermissionMode(rawValue: 29092)
Creating a Directory
create creates the directory using the provided permissions. All directories along the path will be created if needed.
The following will create a new directory with the default permissions (Owner: read-write-execute, Group and Everyone: read-execute.
let newDir = Dir("/path/to/directory/newDirectory")
try newDir.create()
To create a directory with specific permissions, specify the perms parameter:
let newDir = Dir("/path/to/directory/newDirectory")
try newDir.create(perms: [.rwxUser, .rxGroup, .rxOther])
The method throws PerfectError.FileError if an error creating the directory was encountered.
Deleting a Directory
Deleting a directory from the file system:
let newDir = Dir("/path/to/directory/newDirectory")
try newDir.delete()
The method throws PerfectError.FileError if an error deleting the directory was encountered.
Working Directories
Set the Working Directory to the Location of the Current Object
Use setAsWorkingDir to set the current working directory to the location of the object''s path.
let thisDir = Dir("/path/to/directory/")
try thisDir.setAsWorkingDir()
Return the Current Working Directory
Returns a new object containing the current working directory.
let workingDir = Dir.workingDir
Reading the Directory Structure
forEachEntry enumerates the contents of the directory, passing the name of each contained element to the provided callback.
try thisDir.forEachEntry(closure: {
n in
print(n)
})Threading
Perfect provides a core threading library in the PerfectThread package. This package is designed to provide support for the rest of the systems in Perfect. PerfectThread is abstracted
over the core operating system level threading package.
PerfectThread is imported by PerfectNet and so it is not generally required that one directly import it. However, if you need to do so you can import PerfectThread .
PerfectThread provides the following constructs:
Threading.Lock - Mutually exclusive thread lock, a.k.a. a mutex or critical section
Threading.RWLock - A many reader/single writer based thread lock
Threading.Event - Wait/signal/broadcast type synchronization
Threading.sleep - Block/pause; a single thread for a given period of time
Threading queue - Create either a serial or concurrent thread queue with a given name
Threading.dispatch - Dispatch a closure on a named queue
Promise - An API for executing one or more tasks on alternate threads and polling or waiting for return values or errors.
These systems provide internal concurrency for Perfect, and are heavily used in the PerfectNet package in particular.
Locks
PerfectThread provides both mutex and rwlock synchronization objects.
Mutex
Mutexes are provided through the Threading.Lock object. These are intended to protect shared resources from being accessed simultaneously by multiple threads at once. It provides
the following functions:
/// A wrapper around a variety of threading related functions and classes.
public extension Threading {
/// A mutex-type thread lock.
/// The lock can be held by only one thread.
/// Other threads attempting to secure the lock while it is held will block.
/// The lock is initialized as being recursive.
/// The locking thread may lock multiple times, but each lock should be accompanied by an unlock.
public class Lock {
/// Attempt to grab the lock.
/// Returns true if the lock was successful.
public func lock() -> Bool
/// Attempt to grab the lock.
/// Will only return true if the lock was not being held by any other thread.
/// Returns false if the lock is currently being held by another thread.
public func tryLock() -> Bool
/// Unlock. Returns true if the lock was held by the current thread and was successfully unlocked, or the lock count was decremented.
public func unlock() -> Bool
/// Acquire the lock, execute the closure, release the lock.
public func doWithLock(closure: () throws -> ()) rethrows
}
}
The general usage pattern as as follows:
Created a shared instance of Threading.Lock
When a shared resource needs to be accessed, call the lock() function
If another thread already has the lock then the calling thread will block until it is unlocked
Once the call to lock() returns the resource is safe to access
When finished, call unlock()
Other threads are now free to acquire the lock
Alternatively, you can pass a closure to the doWithLock function. This will lock, call the closure, and then unlock.
The tryLock function will lock and return true if no other thread currently holds the lock. If another thread holds the lock, it will return false.
Read/Write Lock
Read/Write Locks (RWLock) are provided through the Threading.RWLock object. RWLocks support many threads accessing a shared resource in a read-only capacity. Forexample, it could permit many threads at once to be accessing values in a shared Dictionary. When a thread needs to perform a modification (a write) to a shared object it acquires a
write lock. Only one thread can hold a write lock at a time, and all other threads attempting to read or write will block until the write lock is released. An attempt to lock for writing will block
until any other read or write locks have been released. When attempting to acquire a write lock, no other read locks will be permitted, and threads attempting to read or write will be
blocked until the write lock is held and then released.
RWLock is defined as follows:
/// A wrapper around a variety of threading related functions and classes.
public extension Threading {
/// A read-write thread lock.
/// Permits multiple readers to hold the while, while only allowing at most one writer to hold the lock.
/// For a writer to acquire the lock all readers must have unlocked.
/// For a reader to acquire the lock no writers must hold the lock.
public final class RWLock {
/// Attempt to acquire the lock for reading.
/// Returns false if an error occurs.
public func readLock() -> Bool
/// Attempts to acquire the lock for reading.
/// Returns false if the lock is held by a writer or an error occurs.
public func tryReadLock() -> Bool
/// Attempt to acquire the lock for writing.
/// Returns false if an error occurs.
public func writeLock() -> Bool
/// Attempt to acquire the lock for writing.
/// Returns false if the lock is held by readers or a writer or an error occurs.
public func tryWriteLock() -> Bool
/// Unlock a lock which is held for either reading or writing.
/// Returns false if an error occurs.
public func unlock() -> Bool
/// Acquire the read lock, execute the closure, release the lock.
public func doWithReadLock(closure: () throws -> ()) rethrows
/// Acquire the write lock, execute the closure, release the lock.
public func doWithWriteLock(closure: () throws -> ()) rethrows
}
}
RWLock supports tryReadLock and tryWriteLock , both of which will return false if the lock cannot be immediately acquired. It also supports doWithReadLock and
doWithWriteLock which will call the provided closure with the lock held and then release it when it has completed.
Events
The Threading.Event object provides a way to safely signal or communicate among threads about particular events. For instance, to signal worker threads that a task has entered
a queue. It''s important to note that Threading.Event inherits from Threading.Lock as using the lock and unlock methods provided therein are vital to understanding
thread event behaviour.
Threading.Event provides the following functions:
public extension Threading {
/// A thread event object. Inherits from `Threading.Lock`.
/// The event MUST be locked before `wait` or `signal` is called.
/// While inside the `wait` call, the event is automatically placed in the unlocked state.
/// After `wait` or `signal` return the event will be in the locked state and must be unlocked.
public final class Event: Lock {
/// Signal at most ONE thread which may be waiting on this event.
/// Has no effect if there is no waiting thread.
public func signal() -> Bool
/// Signal ALL threads which may be waiting on this event.
/// Has no effect if there is no waiting thread.
public func broadcast() -> Bool
/// Wait on this event for another thread to call signal.
/// Blocks the calling thread until a signal is received or the timeout occurs.
/// Returns true only if the signal was received.
/// Returns false upon timeout or error.
public func wait(seconds secs: Double = Threading.noTimeout) -> Bool
}
}
The general usage pattern is illustrated by using a producer/consumer metaphor:
Producer ThreadProducer thread wants to produce a resource and alert other threads about the occurrence
Call the lock function
Produce the resource
Call the signal or broadcast function
Call the unlock function
Consumer Thread
Call the lock function
If a resource is available for consumption:
Consume the resource and call the unlock function
If a resource is not available for consumption:
Call the wait function
When wait returns true:
If a resource is available then consume the resource
Call the unlock function
These producer/consumer threads generally operate in a loop performing these steps repeatedly during the life of the program.
The wait function accepts an optional timeout parameter. If the timeout expires then wait will return false. By default, wait does not timeout.
The functions signal and broadcast differ in that signal will alert at most one waiting thread while broadcast will alert all currently waiting threads.
Queues
The PerfectThread package provides an abstracted thread queue system. It is based loosely on Grand Central Dispatch (GCD), but is designed to mask the actual threading primitives,
and as such, operate with a variety of underlying systems.
This queue system provides the following features:
Named serial queues - one thread operating, removing and executing tasks
Named concurrent queues - multiple threads operating, the count varying depending on the number of available CPUs, removing and executing tasks simultaneously
Anonymous serial or concurrent queues which are not shared and can be explicitly destroyed.
A default concurrent queue
This system provides the following functions:/// A thread queue which can dispatch a closure according to the queue type.
public protocol ThreadQueue {
/// The queue name.
var name: String { get }
/// The queue type.
var type: Threading.QueueType { get }
/// Execute the given closure within the queue''s thread.
func dispatch(_ closure: Threading.ThreadClosure)
}
public extension Threading {
/// The function type which can be given to `Threading.dispatch`.
public typealias ThreadClosure = () -> ()
/// Queue type indicator.
public enum QueueType {
/// A queue which operates on only one thread.
case serial
/// A queue which operates on a number of threads, usually equal to the number of logical CPUs.
case concurrent
}
/// Find or create a queue indicated by name and type.
public static func getQueue(name nam: String, type: QueueType) -> ThreadQueue
/// Returns an anonymous queue of the indicated type.
/// This queue can not be utilized without the returned ThreadQueue object.
/// The queue should be destroyed when no longer needed.
public static func getQueue(type: QueueType) -> ThreadQueue
/// Return the default queue
public static func getDefaultQueue() -> ThreadQueue
/// Terminate and remove a thread queue.
public static func destroyQueue(_ queue: ThreadQueue)
/// Call the given closure on the "default" concurrent queue
/// Returns immediately.
public static func dispatch(closure: Threading.ThreadClosure)
}
Calling Threading.getQueue will create the queue if it does not already exist. Once the queue object has been returned call, its dispatch function and pass it the closure which
will be executed on that queue.
The system will automatically create a queue called "default". Calling the static Threading.dispatch function will always dispatch the closure on this queue.
Promise
A Promise is an object which is shared between one or more threads. A promise will execute the closure/function given to it on a new thread. When the thread produces its return value a
consumer thread will be able to obtain the value or handle the error if one occurred.
This object is generally used in one of two ways:
By passing a closure/function which accepts zero parameters and returns some arbitrary type, followed by zero or more calls to .then
Example: Count to three on another thread
let v = try Promise { 1 }
.then { try $0() + 1 }
.then { try $0() + 1 }
.wait()
XCTAssert(v == 3, "\(v)")
Note that the closure/function given to .then accepts a function which must be called to either throw the error produced by the previous call or return its value.
By passing a closure/function which is executed on another thread and accepts the Promise as a parameter. The promise can at some later point be .set or .fail ''ed, with a
return value or error object, respectively. The Promise creator can periodically .get or .wait for the value or error. This provides the most flexible usage as the Promise can
be .set at any point, for example after a series of asynchronous API calls.
Example: Pause then set a Bool valuelet prom = Promise {
(p: Promise) in
Threading.sleep(seconds: 2.0)
p.set(true)
}
XCTAssert(try prom.get() == nil) // not fulfilled yet
XCTAssert(try prom.wait(seconds: 3.0) == true)
Regardless of which method is used, the Promise''s closure will immediately begin executing on a new thread.
The full Promise API is as follows:
public class Promise {
/// Initialize a Promise with a closure. The closure is passed the promise object on which the
/// return value or error can be later set.
/// The closure will be executed on a new serial thread queue and will begin
/// executing immediately.
public init(closure: @escaping (Promise) throws -> ())
/// Initialize a Promise with a closure. The closure will return a single value type which will
/// fulfill the promise.
/// The closure will be executed on a new serial thread queue and will begin
/// executing immediately.
public init(closure: @escaping () throws -> ReturnType)
/// Chain a new Promise to an existing. The provided closure will receive the previous promise''s
/// value once it is available and should return a new value.
public func then(closure: @escaping (() throws -> ReturnType) throws -> NewType) -> Promise
}
public extension Promise {
/// Get the return value if it is available.
/// Returns nil if the return value is not available.
/// If a failure has occurred then the Error will be thrown.
/// This is called by the consumer thread.
public func get() throws -> ReturnType?
/// Get the return value if it is available.
/// Returns nil if the return value is not available.
/// If a failure has occurred then the Error will be thrown.
/// Will block and wait up to the indicated number of seconds for the return value to be produced.
/// This is called by the consumer thread.
public func wait(seconds: Double = Threading.noTimeout) throws -> ReturnType?
}
public extension Promise {
/// Set the Promise''s return value, enabling the consumer to retrieve it.
/// This is called by the producer thread.
public func set(_ value: ReturnType)
/// Fail the Promise and set its error value.
/// This is called by the producer thread.
public func fail(_ error: Error)
}
Networking
...
OAuth2
The Perfect Authentication OAuth2 provides OAuth2 libraries and OAuth2 provider drivers for Facebook, Google, and GitHub.
A demo application can be found at https://github.com/PerfectExamples/Perfect-Authentication-Demo that shows the usage of the libraries and providers.
Adding to your project
Add this project as a dependency in your Package.swift file.
.Package(url: "https://github.com/PerfectlySoft/Perfect-Authentication.git", majorVersion: 1)To then use the OAuth2 module in your code:
import OAuth2
Configuration
Each provider needs an "appid", also known as a "key", and a "secret". These are usually generated by the OAuth Host, such as Facebook, GitHub and Google developer consoles.
These values, as well as an "endpointAfterAuth" and "redirectAfterAuth" value must be set for each provider you wish to use.
To configure Facebook as a provider:
FacebookConfig.appid = "yourAppID"
FacebookConfig.secret = "yourSecret"
FacebookConfig.endpointAfterAuth = "http://localhost:8181/auth/response/facebook"
FacebookConfig.redirectAfterAuth = "http://localhost:8181/"
To configure Google as a provider:
GoogleConfig.appid = "yourAppID"
GoogleConfig.secret = "yourSecret"
GoogleConfig.endpointAfterAuth = "http://localhost:8181/auth/response/google"
GoogleConfig.redirectAfterAuth = "http://localhost:8181/"
To configure GitHub as a provider:
GitHubConfig.appid = "yourAppID"
GitHubConfig.secret = "yourSecret"
GitHubConfig.endpointAfterAuth = "http://localhost:8181/auth/response/github"
GitHubConfig.redirectAfterAuth = "http://localhost:8181/"
Adding Routes
The OAuth2 system relies on an authentication / exchange system, which requires a URL to be specially assembled that the user is redirected to, and a URL that the user is returned to
after the user has committed the authorization action.
The first set of routes below are the action URLs that will redirect to the OAuth2 provider''s system. They can be anything you wish them to be. The user will never see anything on them
as they will be immediately redirected to the correct location.
The second set of routes below are where the OAuth2 provider should return the user to. Note that this is the same as the "endpointAfterAuth" configuration option. Once the
"authResponse" function has been completed the user is automatically forwarded to the URL in the "redirectAfterAuth" option.
var routes: [[String: Any]] = [[String: Any]]()
routes.append(["method":"get", "uri":"/to/facebook", "handler":Facebook.sendToProvider])
routes.append(["method":"get", "uri":"/to/github", "handler":GitHub.sendToProvider])
routes.append(["method":"get", "uri":"/to/google", "handler":Google.sendToProvider])
routes.append(["method":"get", "uri":"/auth/response/facebook", "handler":Facebook.authResponse])
routes.append(["method":"get", "uri":"/auth/response/github", "handler":GitHub.authResponse])
routes.append(["method":"get", "uri":"/auth/response/google", "handler":Google.authResponse])
Information returned and made available
After the user has been authenticated, certain information is gleaned from the OAuth2 provider.
Note that the session ID can be retrieved using:
request.session?.token
The user-specific information can be accessed as part of the session info:// The UserID as defined by the provider
request.session?.userid
// designates the OAuth2 source - useful if you are allowing multiple OAuth providers
request.session?.data["loginType"]
// The access token obtained in the process
request.session?.data["accessToken"]
// The user''s first name as supplied by the provider
request.session?.data["firstName"]
// The user''s last name as supplied by the provider
request.session?.data["lastName"]
// The user''s profile picture as supplied by the provider
request.session?.data["picture"]
With access to this information, you can now save to the database of your choice.
UUID
Also known as a Globally Unique Identifier (GUID), a Universal Unique Identifier (UUID) is a 128-bit number used to uniquely identify some object or entity. The UUID relies upon a
combination of components to ensure uniqueness.
Create a New UUID object
A new UUID object can either be randomly generated, or assigned.
To randomly generate a v4 UUID:
let u = UUID()
To assign a v4 UUID from a string:
let u = UUID()
If the string is invalid, the object is assigned the following UUID instead: 00000000-0000-0000-0000-000000000000
To return the string value of a UUID:
let u1 = UUID()
print(u1.string)
SysProcess
Perfect provides the ability to execute local processes or shell commands through the SysProcess type. This type allows local processes to be launched with an array of parameters
and shell variables. Some processes will execute and return a result immediately. Other processes can be left open for interactive read/write operations.
Relevant Examples
Perfect-System
Setup
Add the "Perfect" project as a dependency in your Package.swift file:
.Package(
url: "https://github.com/PerfectlySoft/Perfect.git",
majorVersion: 2
)
In your file where you wish to use SysProcess, import the PerfectLib and add either SwiftGlibc or Darwin:import PerfectLib
#if os(Linux)
import SwiftGlibc
#else
import Darwin
#endif
Executing a SysProcess Command
The following function runProc accepts a command, an array of arguments, and optionally outputs the response from the command.
func runProc(cmd: String, args: [String], read: Bool = false) throws -> String? {
let envs = [("PATH", "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin")]
let proc = try SysProcess(cmd, args: args, env: envs)
var ret: String?
if read {
var ary = [UInt8]()
while true {
do {
guard let s = try proc.stdout?.readSomeBytes(count: 1024) where s.count > 0 else {
break
}
ary.append(contentsOf: s)
} catch PerfectLib.PerfectError.fileError(let code, _) {
if code != EINTR {
break
}
}
}
ret = UTF8Encoding.encode(bytes: ary)
}
let res = try proc.wait(hang: true)
if res != 0 {
let s = try proc.stderr?.readString()
throw PerfectError.systemError(Int32(res), s!)
}
return ret
}
let output = try runProc(cmd: "ls", args: ["-la"], read: true)
print(output)
Note that the SysProcess command is executed in this example with a hardcoded environment variable.
SysProcess Members
stdin
stdin is the standard input file stream.
stdout
stdout is the standard output file stream.
stderr
stderr is the standard error file stream.
pid
pid is the process identifier.
SysProcess Methods
isOpen
Returns true if the process was opened and was running at some point.Note that the process may not be currently running. Use wait(false) to check if the process is currently running.
myProcess.isOpen()
close
close terminates the process and cleans up.
myProcess.close()
detach
Detach from the process such that it will not be manually terminated when this object is uninitialized.
myProcess.detach()
wait
Determine if the process has completed running and retrieve its result code.
myProcess.wait(hang: )
kill
Terminate the process and return its result code.
myProcess.kill(signal: )
Response is an Int32 result code.
Log
Perfect has a built-in error logging system that allows messages to be logged at several different levels. Each log level can be routed to either the console or the system log.
The built-in log levels, in order of increasing severity:
debug: Log lines are preceded by [DBG]
info: Log lines are preceded by [INFO]
warning: Log lines are preceded by [WARN]
error: Log lines are preceded by [ERR]
critical: Log lines are preceded by [CRIT]
terminal: Log lines are preceded by [TERM]
To Log Information to the Console:
Log.debug(message: "Line 123: value \(myVar)")
Log.info(message: "At Line 123")
Log.warning(message: "Entered error handler")
Log.error(message: "Error condition: \(errorMessage)")
Log.critical(message: "Exception Caught: \(exceptionVar)")
Log.terminal(message: "Uncaught exception, terminating. \(infoVar)")
To Log Information to the System Log:
If you wish to pipe all log entries to the system log, set the Log.logger property to SysLogger() early in the application setup. Once this has been executed, all output will be
logged to the System Log file, and echoed to the console.
Log.logger = SysLogger()
If you wish to change the logger process back to only the console at any point, set the property back to ConsoleLogger()
Log.logger = ConsoleLogger()File Logging
Using the PerfectLogger module, events can be logged to a specified file, in addition to the console.
Usage
Add the dependency to your project''s Package.swift file:
.Package(url: "https://github.com/PerfectlySoft/Perfect-Logger.git", majorVersion: 0),
Now add the import directive to the file you wish to use the logging in:
import PerfectLogger
To log events to the local console as well as a file:
LogFile.debug("debug message", logFile: "test.txt")
LogFile.info("info message", logFile: "test.txt")
LogFile.warning("warning message", logFile: "test.txt")
LogFile.error("error message", logFile: "test.txt")
LogFile.critical("critical message", logFile: "test.txt")
LogFile.terminal("terminal message", logFile: "test.txt")
To log to the default file, omit the file name parameter.
Linking events with "eventid"
Each log event returns an event id string. If an eventid string is supplied to the directive then it will use the supplied eventid in the log file instead. This makes it easy to link together
related events.
let eid = LogFile.warning("test 1")
LogFile.critical("test 2", eventid: eid)
returns:
[WARNING] [62f940aa-f204-43ed-9934-166896eda21c] [2016-11-16 15:18:02 GMT-05:00] test 1
[CRITICAL] [62f940aa-f204-43ed-9934-166896eda21c] [2016-11-16 15:18:02 GMT-05:00] test 2
The returned eventid is marked @discardableResult and therefore can be safely ignored if not required for re-use.
Setting a custom Logfile location
The default logfile location is ./log.log . To set a custom logfile location, set the LogFile.location variable:
LogFile.location = "/var/log/myLog.log"
Messages can now be logged directly to the file as set by using:
LogFile.debug("debug message")
LogFile.info("info message")
LogFile.warning("warning message")
LogFile.error("error message")
LogFile.critical("critical message")
LogFile.terminal("terminal message")
Sample output
[DEBUG] [ec6a9ca5-00b1-4656-9e4c-ddecae8dde02] [2016-11-16 15:18:02 GMT-05:00] a debug message
[INFO] [ec6a9ca5-00b1-4656-9e4c-ddecae8dde02] [2016-11-16 15:18:02 GMT-05:00] an informational message
[WARNING] [ec6a9ca5-00b1-4656-9e4c-ddecae8dde02] [2016-11-16 15:18:02 GMT-05:00] a warning message
[ERROR] [62f940aa-f204-43ed-9934-166896eda21c] [2016-11-16 15:18:02 GMT-05:00] an error message
[CRITICAL] [62f940aa-f204-43ed-9934-166896eda21c] [2016-11-16 15:18:02 GMT-05:00] a critical message
[EMERG] [ec6a9ca5-00b1-4656-9e4c-ddecae8dde02] [2016-11-16 15:18:02 GMT-05:00] an emergency messageRemote Logging
Using the PerfectLogger module, events can be logged to a specified remote Perfect Log Server, in addition to the console.
The Perfect Log Server is a stand-alone project that can be deployed on your own servers.
Using in your project
To include the dependency in your project, add the following to your project''s Package.swift file:
.Package(url: "https://github.com/PerfectlySoft/Perfect-Logger.git", majorVersion: 1),
Now add the import directive to the file you wish to use the logging in:
import PerfectLogger
Configuration
Three configuration parameters are required:
// Your token
RemoteLogger.token = ""
// App ID (Optional)
RemoteLogger.appid = ""
// URL to access the log server.
// Note, this is not the full API path, just the host and port.
RemoteLogger.logServer = "http://localhost:8181"
Usage
To log events to the log server:
var obj = [String: Any]()
obj["one"] = "donkey"
RemoteLogger.critical(obj)
Linking events with "eventid"
Each log event returns an event id string. If an eventid string is supplied to the directive then it will use the supplied eventid in the log directive instead. This makes it easy to link together
related events.
let eid = RemoteLogger.critical(obj)
RemoteLogger.info(obj, eventid: eid)
The returned eventid is marked @discardableResult and therefore can be safely ignored if not required for re-use.
Network Requests with Perfect-CURL
The Perfect-CURL package provides support for curl in Swift. This package builds with Swift Package Manager and is part of the Perfect project.
Building
Ensure you have installed and activated the latest Swift 3.1+ tool chain.
Add this package as a dependency in your Package.swift file.
.Package(url: "https://github.com/PerfectlySoft/Perfect-CURL.git", majorVersion: 2)
Linux Build NotesEnsure that you have installed libcurl.
sudo apt-get install libcurl4-openssl-dev
Usage
import PerfectCURL
This package uses a simple request/response model to access URL contents. Start by creating a CURLRequest object and configure it according to your needs, then ask it to perform
the request and return a response. Responses are represented by CURLResponse objects.
Requests can be executed either synchronously - blocking the calling thread until the request completes, asynchronously - delivering the response to a callback, or through a Promise
object - performing the request on a background thread giving you a means to chain additional tasks, poll, or wait for completion.
Creating Requests
CURLRequest objects can be created with a URL and a series of options, or they can be created blank and then fully configured. CURLRequest provides the following initializers:
open class CURLRequest {
// Init with a url and options array.
public convenience init(_ url: String, options: [Option] = [])
/// Init with url and one or more options.
public convenience init(_ url: String, _ option1: Option, _ options: Option...)
/// Init with array of options.
public init(options: [Option] = [])
}
Options can be provided using either Array
- Set up new projects easily or download existing project templates
- Create simultaneous macOS and Ubuntu builds on your local machine
- Deploy your projects to your own ECS servers