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 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-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
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
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
return storedAddresses.flatMap { addresses in let data = ["addresses": addresses] return try req.view().render("email", data) } }
func addEMail(_ req: Request) throws -> Future
import Vapor
/// Register your application's routes here. public func routes(_ router: Router) throws { // "It works" page router.get("emails") { req -> Future
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
return storedAddresses.flatMap { addresses in let data = ["addresses": addresses] return try req.view().render("email", data) } } Here
func addEMail(_ req: Request) throws -> Future
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
[{ "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
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
return storedAddresses.flatMap { addresses in let accountList = try addresses.map {address -> Future
func getAccountEntry(req: Request, address: String) throws -> Future
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]