Steve Goodrich Server Side Swift With Vapor

https://github.com/zepedebo/PwnedWatcher

Server Side Swift With Vapor

https://github.com/zepedebo/PwnedWatcher

The Project: Build a database of email addresses and check them against haveibeenpwned.com

https://github.com/zepedebo/PwnedWatcher The Technology

https://github.com/zepedebo/PwnedWatcher Swift https://swift.org/ Swift Provides

Modern approachable language

Reasonably good tools

Cross platform development

Swift Playgrounds https://swift.org/ Vapor https://vapor.codes/ Vapor Provides: Vapor https://vapor.codes/ HTTP/HTTPS Endpoint Web Sockets

Middleware Crypto Authentication Validation

Session Multipart Data Management Routing

Data Serialization Caching Templates Database Logging Async / Deserialization SQLite3 https://www.sqlite.org SQLite Provides:

Ubiquitous Data Store

SQLite3 Flexible https://www.sqlite.org No setup required Requirements (for macOS)

Homebrew Xcode 9.3 or greater (https://brew.sh/) Check Your Xcode

slc-steveg-orm:~ steveg$ xcode-select -p /Applications/Xcode-beta.app/Contents/Developer slc-steveg-orm:~ steveg$ sudo xcode-select -s /Applications/Xcode.app Install Vapor slc-steveg-orm:~ steveg$ brew install vapor/tap/vapor https://github.com/zepedebo/PwnedWatcher Pwned Watcher

• Create a list of e-mail addresses • Check them against “';--have i been pwned?” • Display how many sites are a problem for me Demonstrates

• Simple URL routing

• Get and Post HTTP commands

• MVC Architecture

• Vapor Client calls to a RESTful API

• Async Structure of a Vapor Project

Generated with: vapor new --web WebHello Simplest Routing Structure of an Address

The scheme The computer The application The service / api call

https://haveibeenpwned.com:443/api/v2/breaches

We are mainly concerned with this part Adding a route

All routes start in routes.swift Simplest Route

http://localhost/hello

// routes.swift import Vapor

public func routes(_ router: Router) throws { router. get("hello") { req in return "Hello" } } Managing Complexity Model View Controller

Controllers are the Glue

http://localhost/hello

// routes.swift

import Vapor

public func routes(_ router: Router) throws { router. get("hello") { req in return "Hello" } } Model Add a Model with Fluent Fluent gives you

• Support for: • An ORM • SQLite • Migrations • MySQL • Relations • PostgreSQL • Transactions Interesting Files package.swift - Which libraries are needed configure.swift - Configure services

Models folder - Define our data models Add Package Information import PackageDescription Download this let package = Package( name: "URLWatcher", dependencies: [ // � A server-side Swift . .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),

// Load the driver for sqlite3 .package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0"),

// � An expressive, performant, and extensible templating language built for Swift. .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"), ], targets: [ .target(name: "App", dependencies: ["Leaf", "Vapor", "FluentSQLite"]), .target(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: ["App"]) ] ) Compile this Add a Class in Models

import FluentSQLite import Vapor final class MonitoredEMail: SQLiteModel { var id: Int? var address: String

init(id: Int? = nil, address: String) { self.id = id; self.address = address; } } extension MonitoredEMail: Content {} extension MonitoredEMail: Migration {} Add Configuration import Leaf import Vapor import FluentSQLite

/// Called before your application initializes. public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { : try services.register(FluentSQLiteProvider())

: var databases = DatabasesConfig() try databases.add(database: SQLiteDatabase(storage: .memory), as: .sqlite) services.register(databases)

:

var migrations = MigrationConfig() migrations.add(model: MonitoredEMail.self, database: .sqlite) services.register(migrations) } Change Package Information import PackageDescription let package = Package( name: "URLWatcher", dependencies: [ // � A server-side Swift web framework. .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),

// Load the driver for sqlite3 .package(url: "https://github.com/vapor/fluent-postgresql.git""https://github.com/vapor/fluent-sqlite.git", from:, from: "3.0.0" "1.0.0"), )

// � An expressive, performant, and extensible templating language built for Swift. .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"), ], targets: [ ..targettarget(name:(name: "App""App",, dependencies:dependencies: [["Leaf""Leaf",, "Vapor""Vapor",, "FluentPostgreSQL""FluentSQLite"]), ]), .target(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: ["App"]) ] ) Change the Class in Models

import FluentSQLiteFluentPostgreSQL import Vapor

final class MonitoredEMail: SQLiteModelPostgreSQLModel { { var id: Int? var address: String

init(id: Int? = nil, address: String) { self.id = id; self.address = address; } }

extension MonitoredEMail: Content {} extension MonitoredEMail: Migration {} Change Configuration import Leaf import Vapor import FluentSQLite

/// Called before your application initializes. public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { : trytry services.services.registerregister((FluentPostgreSQLProviderFluentSQLiteProvider()) ())

: var databases = DatabasesConfig() trylet databases.pgconfig =add PostgreSQLDatabaseConfig(database: SQLiteDatabase(hostname:(storage: "localhost" .memory), ,as: port: .sqlite 32768), username: "postgres", services. register (databases) database: "email", password: nil, transport: .cleartext) let postgres = PostgreSQLDatabase(config: pgconfig) databases.add(database: postgres, as: .psql) services.register(databases)

:

var migrations = MigrationConfig() migrations.migrations.addadd(model:(model: MonitoredEMailMonitoredEMail..selfself,, database:database: ..sqlitepsql) ) services.register(migrations) } Model Model View Controller View Add a View with Leaf Add a View Add Package Information (package.swift)

import PackageDescription let package = Package( name: "WebHello", Download this dependencies: [ // � A server-side Swift web framework. .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),

// � An expressive, performant, and extensible templating language built for Swift. .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"), ], targets: [ .target(name: "App", dependencies: ["Leaf", "Vapor"]), .target(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: ["App"]) ] Compile this ) Add Configuration (configure.swift)

import Leaf import Vapor

/// Called before your application initializes. public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { // Register providers first try services.register(LeafProvider())

: : : Add a .leaf File (email.leaf)

#set("title") { Monitored Emails }

#set("body") {

Monitored E-Mail addresses

#for(email in addresses) { }
E-Mail Address
#(email.address)
}

#embed("base") Render the View (in our controller)

import Vapor

/// Register your application's routes here. public func routes(_ router: Router) throws { // "It works" page router.get("emails") { req -> Future in let storedAddresses = MonitoredEMail.query(on: req).all()

return storedAddresses.flatMap { addresses in let data = ["addresses": addresses]

return try req.view().render("email", data) } } } Model View Controller Controller Add a Controller with Swift Add GET and POST Handlers (Email.swift)

import Vapor

final class EmailController {

func getEMails(_ req: Request) throws -> Future { let storedAddresses = MonitoredEMail.query(on: req).all()

return storedAddresses.flatMap { addresses in let data = ["addresses": addresses] return try req.view().render("email", data) } }

func addEMail(_ req: Request) throws -> Future { return try req.content.decode(MonitoredEMail.self).flatMap { url in return url.save(on: req).map { _ in return req.redirect(to: "/emails") } } } } Connect to Routes (Routes.swift)

import Vapor

/// Register your application's routes here. public func routes(_ router: Router) throws { // "It works" page router.get("emails") { req -> Future in let storedAddresses = MonitoredEMail.query(on: req).all()

return storedAddresses.flatMap { addresses in let data = ["addresses": addresses]

return try req.view().render("email", data) } } } Connect to Routes (Routes.swift)

import Vapor

/// Register your application's routes here. public func routes(_ router: Router) throws {

let emailController = EmailController() router.get("emails", use:emailController.getEMails) router.post("emails", use:emailController.addEMail) Closures I do not think it runs when you think it runs Quick Closure Review add1 = lambda a: a + 1 let add1 = a => a + 1 let add1 = {(a: Int) -> Int in return a + 1} let add1 = {a in return a + 1} let add1 = {$0 + 1}

These are VARIABLES. Trailing Closures

[1,2,3].reduce(0, {$0 + $1}) == [1,2,3].reduce(0){$0+$1}

[1,2,3].map{$0 * $0} Spot the Closures import Vapor Here final class EmailController {

func getEMails(_ req: Request) throws -> Future { let storedAddresses = MonitoredEMail.query(on: req).all()

return storedAddresses.flatMap { addresses in let data = ["addresses": addresses] return try req.view().render("email", data) } } Here

func addEMail(_ req: Request) throws -> Future { return try req.content.decode(MonitoredEMail.self).flatMap { url in return url.save(on: req).map { _ in return req.redirect(to: "/emails") } } } Here Here } Demo 1

What we have so far: A list of e-mail addresses Vapor does clients too The API we will use

https://haveibeenpwned.com/ Making a Request

Someday we will get a response.…

func callPWNDService(req: Request, address: String) throws -> Future { let client = try req.make(Client.self) let query = "https://haveibeenpwned.com/api/v2/breachedaccount/" + address let response = client.get(query, headers: ["User-Agent":"account checker"]) return response } Model the Response Data What We Get

[{ "Name":"Adobe", "Title":"Adobe", "Domain":"adobe.com", "BreachDate":"2013-10-04", "AddedDate":"2013-12-04T00:00Z", "ModifiedDate":"2013-12-04T00:00Z", "PwnCount":152445165, "Description":"In October 2013, 153 million Adobe accounts were breached …”, "DataClasses":["Email addresses","Password hints","Passwords","Usernames"], "IsVerified":True, "IsSensitive":False, "IsRetired":False, "IsSpamList":False }] Our Data

import Foundation struct PwndEntry: Codable { var Name, Title, Domain: String var BreachDate, AddedDate, ModifiedDate : String var PwnCount, Description : String var LogoType : String? var LogoPath: String var DataClasses : [String] var IsVerified, IsFabricated, IsSensitive : Bool var IsRetired, IsSpamList : Bool } Decode the Result

func decodePWNDResponse(response: Future) throws -> Future<[PwndEntry]> { let pwndData = response.flatMap(to: [PwndEntry].self) { entryResponse -> Future<[PwndEntry]> in guard entryResponse.http.status == HTTPResponseStatus.ok else { return response.map(to: [PwndEntry].self, {_ in return [PwndEntry]()}); } return try entryResponse.content.decode([PwndEntry].self) } return pwndData } Account Information

What we need What we have (potentially) struct AccountEntry: Codable { struct AccountEntry: Codable { var Address: String; var Address: String; var Pwnd: [PwndEntry]; var Pwnd: Future<[PwndEntry]>; } }

This causes problems for the Leaf Chaining Two Kinds of Queries

Safari Vapor SQLite3 haveibeenpwned Leaf

Get page Get accounts

Accounts

Get the breaches

Breaches Get view

Page View Async Building a chain of actions that will take place later. Brute Force

Create an array of futures, each containing a promise that we will have an address to look up Create an Array Of Futures

func getPWND(_ req: Request) throws -> Future { let storedAddresses = MonitoredEMail.query(on: req).all()

return storedAddresses.flatMap { addresses in let accountList = try addresses.map {address -> Future in return try self.getAccountEntry(req: req, address: address.address) } return try req.view().render("pwnd", ["addresses": accountList]) } } Containing a Promise (or Created from a Promise)

func getAccountEntry(req: Request, address: String) throws -> Future { let promise = req.eventLoop.newPromise(AccountEntry.self) DispatchQueue.global().async { do { // Make the call let pwndData = try self.callPWNDService(req: req, address: address).wait() promise.succeed(result: AccountEntry(Address: address, Pwnd: pwndData)) } catch { promise.fail(error: error) } } return promise.futureResult; } Magic Happens Demo 2 Summary

178 lines of code (a good portion generated by Vapor)

• Web applications / interfaces are cool. • Swift is a great language for building them • Vapor gives you some great tools to make building them relatively easy Steve Goodrich [email protected]