Getting Started With HTTP Middleware in Kitura [FREE]
source link: https://www.tuicool.com/articles/nInMjuZ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
As Swift on the server continues to mature, many popular frameworks are adopting industry standards to handle incoming requests and outgoing responses. One of the most popular ways to handle traffic is by implementing middleware , which intercepts requests and performs logic that you define on specific routes.
In this tutorial, you’ll take a hands-on approach to using middleware in a REST API. You will:
- Enable cross-origin traffic by adding CORS to your router.
- Use middleware to authenticate a HTTP(s) request.
- Ensure that your API properly understands the meaning of life!
You’ll follow test-driven development principles throughout this tutorial by starting with routes that behave incorrectly, and adding the needed HTTP Route and middleware logic that you need to resolve the issues.
This tutorial uses Kitura. However, the concepts of middleware and HTTP routing work in the same manner in Vapor, as well as many other Swift server frameworks. From a technical standpoint, here’s what you’ll use throughout this tutorial:
- Kitura 2.7 or higher
- macOS 10.14 or higher
- Swift 5.0 or higher
- Xcode 10.2 or higher
- Terminal
Getting Started
To start, click on the Download Materials button at the top or bottom of this page to download the projects that you’ll use throughout this tutorial. Open Terminal and navigate the starter project folder.
You’ll notice the lack of a .xcodeproj
file in this folder, and that’s a-OK! Enter the following commands in Terminal:
swift package generate-xcodeproj xed .
This will pull all the dependencies needed to run the project and open Xcode.
Note : If you are looking for a great tutorial on how to use Swift Package Manager, check outhere.
In Xcode, build and run your project by pressing Command-R . Check the Xcode console and you should see a server listening on port 8080. Open a web browser, and navigate to http://localhost:8080 . You should see Kitura’s “Hello, World!” page:
You’re ready to dive right into the middle of this tutorial!
First, revisit some core concepts of HTTP.
The Request/Response Model
The server you’ll build throughout this tutorial is responsible for handling a request made from a client, then responding appropriately based on the content of that request. In this tutorial, you will only use cURL
commands from Terminal to interact with your server, but these commands could come just as easily from an iOS or Android app. :]
What makes up a request?
-
Any HTTP request must have an address
, which contains:
a. A domain or host (e.g.,
www.raywenderlich.com
,localhost:8080
, or even127.0.0.1:8080
)b. A path (e.g.,
/users
)c. Query parameters (e.g.,
?username=ray&id=42
) -
A method
must be specified, (e.g.,
GET
,POST
, orOPTIONS
). -
All requests can have headers
, which you can think of as metadata
for your request (e.g.,
{"Origin": "www.raywenderlich.com"}
). - For certain methods, a body must be specified, which you usually serialize into JSON.
When your server creates a response, it needs to specify the following:
- A status code between 200 and 599 that indicates the result of the request.
- A set of headers as response metadata.
- A body, which is usually in text, JSON, or another data type.
Later in this tutorial, you are going to write a server that responds to a request validating your interpretation of the meaning of life. Using cURL
from Terminal, your request will look like this:
curl -X POST \ http://localhost:8080/raze \ -H 'content-type: application/json' \ -H 'origin: www.raywenderlich.com' \ -d '{"meaningOfLife": 42}'
When you write your handler for this request at first, this request won’t quite work out of the box. However, you’ll use middleware to inspect the headers and body of the request to make it work, until you get this response:
Yes, the meaning of life is indeed 42!
Before you start writing code, review what happens when you send off your request from your client.
HTTP Routing
Consider two components in your server: the HTTP router and the HTTP route. The router is the object responsible for handling incoming requests and routing them to the appropriate route . When you’ve set up a route on your server, the router will hand it off to the handler function that is responsible for sending the response back to the client.
Assume that you are working on a team of developers who have already put a lot of work into their existing routes. You join the team and your first task is to validate data coming into the /test
route — all requests that are routed to /test
must
have the Access-Control-Allow-Origin
header specified as true
. This is what the route looks like:
func corsHandler(request: RouterRequest, response: RouterResponse, next: () -> Void) { if response.headers["Access-Control-Allow-Origin"] == "false" { response.status(.badRequest).send("Bad origin") } else { response.status(.OK).send("Good origin") } }
One idea would be to ask everyone who connects to this API very nicely to specify this header in their request, but what if someone doesn’t get the message? Furthermore, what if you could get your hands on the request without having to mess with any of the existing code in the route handler? This is the crux of this tutorial — middleware to the rescue.
Middleware
Simply put, middleware is code that runs in the middle of your router and your route handler. Whenever you write middleware, you can intercept a request on its way to its specified route, and you can do whatever you need to do with the request at that point. You can even choose to send back a response early if the request doesn’t meet your needs, ignoring the route handler altogether.
In Kitura, middleware is achieved through writing a class or struct that adheres to a protocol, and then by registering the middleware on specific routes.
The example read above, in terms of middleware, is a common requirement called CORS , an acronym for C ross O rigin R esource S haring. If you wanted to roughly write your own code to fulfill this need, you could write middleware that looks like this:
class RazeMiddleWare: RouterMiddleware { public func handle(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) throws { request.headers.append("Access-Control-Allow-Origin", value: "true") next() } }
And then you could add it to the /test
route, as required previously, like so:
let middleware = RazeMiddleware() router.all("/test", middleware: middleware)
Note
: Remember how you were required to handle POST
requests to /test
? Instead of specifying only this method, you are choosing to handle all methods used on this route. The next section is going to explain why.
Prepping Your Kitura Server for Middleware
In Xcode, navigate to your Package.swift
and the following code to the end of the dependencies
array:
.package(url: "https://github.com/IBM-Swift/Kitura-CORS.git", .upToNextMinor(from: "2.1.0")), .package(url: "https://github.com/IBM-Swift/Kitura-CredentialsHTTP.git", .upToNextMinor(from: "2.1.3"))
Next, scroll down to targets
and add the following two dependencies to your Application
target:
"KituraCORS", "CredentialsHTTP"
Close your Xcode project. Open Terminal and navigate to the root directory of your project. Run two commands:
swift package generate-xcodeproj xed .
You have now updated your project to add CORS middleware and some authentication capabilities to your project.
Next, you’ll write three HTTP routes that fail or give you an undesirable result at first, and then you will write middleware to make each of them work correctly!
In Xcode, open the Sources/Application/Routes
directory in the Project Navigator on the left, and right-click on the directory. Click on New File…
, and add a new Swift file called RazeRoutes.swift
. Make sure you select the Application
target.
Replace the contents of the file with the following import statements and initialization function:
import LoggerAPI import KituraContracts import Kitura func initializeRazeRoutes(app: App) { }
Before you start adding some more code to this file, go back to Application.swift
and add the following line to the end of postInit()
:
initializeRazeRoutes(app: self)
Every HTTP route you now add in RazeRoutes.swift will register with your router every time you start your server. Now, you’ll add a place to put all of your middleware.
Right-click Application.swift
and click New File…
again. This file should be named Middleware.swift
, and should also be targeted to Application
.
Replace the file’s contents with the following:
import Foundation import Kitura import KituraCORS class Middleware { }
Alright, the stage is set — time for you to enable cross-origin requests on your server and get your first taste of middleware!
Enabling CORS
Open RazeRoutes.swift
and add this route registration to initalizeRazeRoutes
:
app.router.get("/cors", handler: corsHandler)
Xcode should show you an error at this point because you have not yet declared a function called corsHandler
. Fix that by adding the following code at the very bottom of the file outside of your function:
// 1 func corsHandler(request: RouterRequest, response: RouterResponse, next: () -> Void) { // 2 guard response.headers["Access-Control-Allow-Origin"] == "www.raywenderlich.com" else { response.status(.badRequest).send("Bad origin") return } // 3 response.status(.OK).send("Good origin") }
Here’s what you just wrote:
-
You define the
GET
route you registered on/cors
by specifying arequest
, aresponse
, and anext
handler, which tells your router to continue searching for things to do according to the request that was made. -
Next, you validate the value of the
Access-Control-Allow-Origin
header in your response. If you’re wondering why you’d be checking a response without having previously set anything to it, you’re spot on! This is what you will have to fix with middleware. - This is the “happy path.” If everything looks good, simply return a successful response.
Build and run your server, and then confirm that your server is running on port 8080. Open Terminal and execute the following command:
curl -H "Origin: www.raywenderlich.com" localhost:8080/cors
In Terminal, you should see the string "Bad Origin"
sent as a response. This might not be the desired response, but you can trust that it’s expected for now!
You’re going to implement middleware to fix this. In Xcode, open Middleware.swift
, and add the following method to the Middleware
class:
// 1 static func initializeCORS(app: App) { // 2 let options = Options(allowedOrigin: .origin("www.raywenderlich.com"), methods: ["GET"], maxAge: 5) // 3 let cors = CORS(options: options) // 4 app.router.all("/cors", middleware: cors) }
Here’s what you just added, step by step:
- The method signature you write to add this middleware as a convenience to your HTTP route.
-
You create and set an object of options to enable CORS, most notably that you will only allow
GET
to be an acceptable method on the/cors
route, and that you will only allow requests that specify an origin ofwww.raywenderlich.com
to pass through. ThemaxAge
parameter is a value that specifies how long you want this value to be cached for future requests. -
Here, you are creating a
CORS
middleware with your options for use on your HTTP route. -
Finally, you register your CORS middleware for all HTTP methods that hit
/cors
on your router. Even though you listedGET
as the only method in youroptions
map, any method should still be able to access this middleware.
Hold down the Command
button and click on the CORS
text in your constructor. This will open the CORS
class in Xcode. Scroll to the definition of the handle
method, and add on its first line. Finally, go back to RazeRoutes.swift
and at the top of the initializeRazeRoutes
function, add the following to register your middleware:
Middleware.initializeCORS(app: app)
Build and run your server again. Once your server is running on port 8080, execute the same cURL
command in Terminal:
curl -H "Origin: www.raywenderlich.com" localhost:8080/cors
Go back to Xcode, where you’ve hit the breakpoint you’ve just added. Inspect the request
and response
objects as you step through the code. When you finally let the program continue, you should see Good origin
in your response!
Good work! The CORS
middleware took care of ensuring that your response was marked appropriately and allowed a cross-origin resource to access the GET
method on /cors
, all thanks to your middleware! Now let’s do something from scratch.
Middleware From Scratch
Open RazeRoutes.swift
again, and at the end of your initializeRazeRoutes
function, register a new GET
route like so:
app.router.get("/raze", handler: razeHandler)
Below your corsHandler
function, add the following code to handle any GET
requests that come in for /raze
:
func razeHandler(request: RouterRequest, response: RouterResponse, next: () -> Void) { guard let meaning = request.queryParameters["meaningOfLife"] else { return } response.status(.OK).send("Yes, the meaning of life is indeed \(meaning)!") }
Here’s the drill: You’ve been asked to make a route that simply echoes back the “meaning of life” to any client that makes a GET
request to /raze
, and you want to have control over what that value is. Build and run your server, and execute the following command in Terminal:
curl "localhost:8080/raze?meaningOfLife=42"
You should get a response that says, “Yes, the meaning of life is indeed 42!”.
While truer words may have never been spoken, this route is built on the assumption that all clients know to include this parameter both as a query parameter in the GET
request and to ensure that it is an integer and not a string. You might have good direction, but not every client that consumes this API might remember to include this!
To see what happens if you forget this parameter, execute the following command in Terminal:
curl -v localhost:8080/raze
You should get a response returning a 503 code, meaning that the server is unable to handle the request. What gives? You registered the route and everything, right?
Since you didn’t include the query parameter meaningOfLife
in your request, and you didn’t write code to send back a user-friendly response, it makes sense that you’re going to get a less-than-ideal response in this case. Guess what? You can write middleware to make sure that this parameter is handled correctly in all of your requests to this route!
Further, you can make sure that malformed requests are responded to correctly, so that you can ensure a good developer experience for consumers of this API and not have to worry about touching the original HTTP route code!
In Xcode, open Middleware.swift . Scroll to the very bottom of this file, and add the following code:
// 1 public class RazeMiddleware: RouterMiddleware { private var meaning: Int // 2 public init(meaning: Int) { self.meaning = meaning } // 3 public func handle(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) throws { } }
What you’ve added, here:
-
Kitura requires that your middleware class or struct conforms to the
RouterMiddleware
protocol. -
It’s generally a good idea to set up a constructor for your middleware instance. This allows you to handle stored properties that are relevant to your middleware — similar to the options you handled with
CORS
in the previous example. -
The single requirement of the
RouterMiddleware
protocol is thehandle
method. It should look familiar, as it takes aRouterRequest
, aRouterResponse
and a closure to tell the router to continue on.
Note
: In the last route you implement, you’ll use a different version of this type of middleware called TypeSafeMiddleware
that allows you to use a strongly typed object instead of “raw middleware” as you’ve done here.
When you implemented CORS
, you elected to add some headers to your response if and only if your request included certain headers. Inside the handle()
method in your new middleware, add the following code:
guard let parsedMeaning = request.queryParameters["meaningOfLife"] else { response.status(.badRequest).send("You must include the meaning of life in your request!") return } guard let castMeaning = Int(parsedMeaning) else { response.status(.badRequest).send("You sent an invalid meaning of life.") return } guard castMeaning == meaning else { response.status(.badRequest).send("Your meaning of life is incorrect") return } next()
After you register this middleware with the appropriate route, you’ll ensure that you can do the following with incoming requests. All you do in the above code is make sure a meaningOfLife
parameter exists in your requests, make sure it’s a valid number, and finally make sure it’s equal to the correct meaning. If any of these is wrong, you simply respond with an erroneous response. Otherwise, you call next()
to signal this middleware is done with its work.
This might be a fairly contrived example, but consider for a moment that you were able to intervene on all requests made to the /raze
route this way without touching a single line of code on the existing route! This perfectly illustrates the power of middleware. Scroll up to the Middleware
class and the following method to it:
static func initializeRazeMiddleware(app: App) { let meaning = RazeMiddleware(meaning: 42) app.router.get("/raze", middleware: meaning) }
By parameterizing the meaning
value, you can let developers who want to use this middleware set whatever value they want! However, you’re a well-read developer and you understand the true meaning of life, so you set it to 42
here.
Lastly, open up RazeRoutes.swift
in Xcode, and inside the initializeRazeRoutes()
function, but above your route registrations, add this line of code:
Middleware.initializeRazeMiddleware(app: app)
Build and run your server, and ensure that it is live on port 8080. Open Terminal, and run the following commands:
curl "localhost:8080/raze" curl "localhost:8080/raze?meaningOfLife=43" curl "localhost:8080/raze?meaningOfLife=42"
You should see the following output:
$ curl "localhost:8080/raze" You must include the meaning of life in your request! $ curl "localhost:8080/raze?meaningOfLife=43" Your meaning of life is incorrect $ curl "localhost:8080/raze?meaningOfLife=42" Yes, the meaning of life is indeed 42!
lsof -i tcp:8080 PID kill -9 **PID**
Feel free to put breakpoints on your middleware class to observe how it handles each request, but you’ll notice that only the properly formed request gets through to your route handler now. Here’s a reminder: You ensured that this route yields a safe experience no matter what the request is, and you did it all without touching the existing route handler code! Nice work!
Your last example is going to deal with authentication — this might seem scary at first, but the principles are the exact same!
Authentication Middleware
Whenever you’re browsing your favorite social media website, it would make sense that you could only see your personal content if you’re logged in, right? Why would you even want to waste time performing an operation in a route handler if the request is unauthenticated? You’re going to implement a route handler that uses Codable Routing in Kitura with type-safe middleware to ensure that the request is authenticated.
Note : Both Server-Side Swift books,Kitura andVapor, go into the details of HTTP authentication. HTTP basic is often used as a means of demonstration authentication on a server, and it is generally not recommended for production.
First, open RazeRoutes.swift
and register your route in your initalizeRazeRoutes()
function:
app.router.get("/auth", handler: authHandler)
Next, scroll to the bottom of this file and add the following handler:
func authHandler(profile: RazeAuth, completion: (RazeAuth?, RequestError?) -> Void) { completion(profile, nil) }
Your server should not compile properly at this point, because you have not yet defined RazeAuth
. Open Middleware.swift
and import the following module at the top of your file underneath your import of KituraCORS
:
import CredentialsHTTP
Next, scroll to the bottom of this file and add the following code to define your middleware instance:
// 1 public struct RazeAuth: TypeSafeHTTPBasic { // 2 public var id: String // 3 static let database = ["David": "12345", "Tim": "54321"] // 4 public static func verifyPassword(username: String, password: String, callback: @escaping (RazeAuth?) -> Void) { } }
Take a moment to examine what you’ve added:
-
The main requirement of your middleware is that it must conform to the
TypeSafeHTTPBasic
protocol. -
The first required implementation in the
TypeSafeBasicHTTP
protocol is theid
property, to be able to identify an authenticated user. - In this example, you are setting up a very small and simple database of usernames and passwords — this is here to demonstrate that you could use any existing database module to query by username!
-
The other required implementation for the
TypeSafeBasicHTTP
protocol is theverifyPassword
method. After you have confirmed that the username and password match expected values, you can create aRazeAuth
object with the proper username, and pass it on in the callback. Since you registered the route with a non-optionalRazeAuth
object, this means that callingcallback()
withnil
will instead send a 401 Unauthorized response to the client.
Next, add this code inside verifyPassword()
to verify if the given username and password are valid according to your super secure database of usernames and passwords:
guard let storedPassword = database[username], password == storedPassword else { return callback(nil) } return callback(RazeAuth(id: username))
Lastly, go to RazeRoutes.swift
and put a breakpoint inside your /auth
route handler. Build and run your server, and ensure your server is listening on port 8080. Open Terminal, and run the three following commands:
curl -u "Ray":"12345" localhost:8080/auth curl -u "David":"12345" localhost:8080/auth curl -u "Tim":"54321" localhost:8080/auth
For the commands that are properly authenticated (David and Tim’s), you should trigger your breakpoint, and your server should respond with the username that you sent over! Now, your server only has to do the work its authenticated to do!
Where to Go From Here?
Middleware opens up a large realm of possibilities for developers to enhance routes that might already exist on a server. This tutorial showed you how easy it is to both implement existing middleware libraries, and how you can roll your own library to add custom behavior to your server, like the koba library written by Caleb Kinney.
Both our Server Side Swift with Kitura and Server Side Swift with Vapor books have plenty of information about implementing middleware and authentication, and you can work on them in a real-life scenario!
If you want to learn more about how Kitura handles HTTP routing and works in general, read thisbeginner’s tutorial about it.
Please leave a comment below if you know about any other middleware libraries or if you have any questions!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK