23

Getting Started With HTTP Middleware in Kitura [FREE]

 4 years ago
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:

kituraHelloWorld-650x476.png

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?

  1. Any HTTP request must have an address , which contains:

    a. A domain or host (e.g., www.raywenderlich.com , localhost:8080 , or even 127.0.0.1:8080 )

    b. A path (e.g., /users )

    c. Query parameters (e.g., ?username=ray&id=42 )

  2. A method must be specified, (e.g., GET , POST , or OPTIONS ).
  3. All requests can have headers , which you can think of as metadata for your request (e.g., {"Origin": "www.raywenderlich.com"} ).
  4. 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:

  1. A status code between 200 and 599 that indicates the result of the request.
  2. A set of headers as response metadata.
  3. 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:

  1. You define the GET route you registered on /cors by specifying a request , a response , and a next handler, which tells your router to continue searching for things to do according to the request that was made.
  2. 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.
  3. 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:

  1. The method signature you write to add this middleware as a convenience to your HTTP route.
  2. 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 of www.raywenderlich.com to pass through. The maxAge parameter is a value that specifies how long you want this value to be cached for future requests.
  3. Here, you are creating a CORS middleware with your options for use on your HTTP route.
  4. Finally, you register your CORS middleware for all HTTP methods that hit /cors on your router. Even though you listed GET as the only method in your options 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:

  1. Kitura requires that your middleware class or struct conforms to the RouterMiddleware protocol.
  2. 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.
  3. The single requirement of the RouterMiddleware protocol is the handle method. It should look familiar, as it takes a RouterRequest , a RouterResponse 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!
Note : If your console ever tells you that another server is already listening on port 8080, follow these steps to clean up your port:
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:

  1. The main requirement of your middleware is that it must conform to the TypeSafeHTTPBasic protocol.
  2. The first required implementation in the TypeSafeBasicHTTP protocol is the id property, to be able to identify an authenticated user.
  3. 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!
  4. The other required implementation for the TypeSafeBasicHTTP protocol is the verifyPassword method. After you have confirmed that the username and password match expected values, you can create a RazeAuth object with the proper username, and pass it on in the callback. Since you registered the route with a non-optional RazeAuth object, this means that calling callback() with nil 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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK