Skip to content

atom2ueki/SwiftWebServer

Repository files navigation

SwiftWebServer

A lightweight, Swift-based HTTP web server with middleware support.

Features

  • πŸš€ Lightweight & Fast: Minimal overhead with efficient request handling
  • πŸ”§ Middleware System: Extensible middleware architecture for request/response processing
  • πŸ›£οΈ Route Handling: Support for path parameters (/users/{id}) and multiple HTTP methods
  • 🦾 Body Parsing: JSON and form data parsing middleware
  • πŸ“ Static File Serving: Built-in static file serving with automatic MIME type detection
  • πŸͺ Cookie Support: Full cookie parsing and setting capabilities with secure attributes
  • πŸ”’ Authentication: Bearer token authentication middleware with JWT support
  • 🌐 CORS Support: Cross-Origin Resource Sharing middleware with configurable options
  • πŸ“ Logging: Configurable request/response logging with detailed output options
  • 🏷️ ETag Support: Conditional requests with 304 Not Modified responses for caching
  • πŸ”„ HTTP Redirects: Support for temporary and permanent redirects with proper status codes
  • 🎯 Error Handling: Comprehensive error responses with proper HTTP status codes
  • πŸ“± SwiftUI Integration: Native iOS/macOS integration with example application

Demo (Blog WebApp)

SwiftWebServer Demo

Quick Start

Installation

Add SwiftWebServer to your Package.swift:

dependencies: [
    .package(url: "https://github.com/atom2ueki/SwiftWebServer.git", from: "1.0.0")
]

Basic Usage

import SwiftWebServer

let server = SwiftWebServer()

// Add middleware
server.use(LoggerMiddleware())
server.use(CORSMiddleware())

// Define routes
server.get("/hello") { req, res in
    res.send("Hello, World!")
}

server.get("/users/{id}") { req, res in
    let userId = req.pathParameters["id"] ?? "unknown"
    res.json("""
    {
        "userId": "\(userId)",
        "message": "User details"
    }
    """)
}

// Start server
try server.start(port: 8080)
print("Server running on http://localhost:8080")

Architecture Overview

SwiftWebServer follows a middleware-based architecture where requests flow through a chain of middleware functions before reaching route handlers, and responses flow back through the same chain.

Request/Response Workflow

The diagram above shows how requests flow through SwiftWebServer:

  1. Client Request: HTTP request arrives at the server
  2. Connection Handler: Accepts and manages the connection
  3. Request Parsing: Parses HTTP headers, method, path, and body
  4. Middleware Chain: Request flows through registered middleware in order
  5. Route Matching: Attempts to find a matching route handler
  6. Static Files: If no route matches, checks for static files
  7. Response Processing: Generates response through middleware chain
  8. Client Response: Sends final HTTP response back to client

Middleware Architecture

Middleware functions are the core of SwiftWebServer's extensibility. Each middleware can:

  • Inspect and modify incoming requests
  • Add functionality like authentication, logging, or parsing
  • Short-circuit the request chain (e.g., for authentication failures)
  • Process responses on the way back to the client

Middleware Execution Order

Request  β†’  [Middleware 1]  β†’  [Middleware 2]  β†’  [Route Handler]
Response ←  [Middleware 1]  ←  [Middleware 2]  ←  [Route Handler]

Middleware executes in the order it's registered using server.use(), and response processing happens in reverse order.

Built-in Middleware

SwiftWebServer comes with several built-in middleware components:

BodyParser

Parses JSON and form data from request bodies.

server.use(BodyParser())

server.post("/api/users") { req, res in
    if let jsonBody = req.jsonBody {
        // Handle JSON data
        let userData = jsonBody
    } else if let formBody = req.formBody {
        // Handle form data
        let name = formBody["name"]
    }
}

LoggerMiddleware

Logs incoming requests and outgoing responses with configurable detail levels.

// Basic logging
server.use(LoggerMiddleware())

// Detailed logging with headers
server.use(LoggerMiddleware(options: LoggerOptions(
    level: .detailed,
    includeHeaders: true
)))

CORSMiddleware

Handles Cross-Origin Resource Sharing (CORS) headers for web applications.

// Default CORS settings
server.use(CORSMiddleware())

// Custom CORS configuration
server.use(CORSMiddleware(options: CORSOptions(
    allowedOrigins: ["https://myapp.com"],
    allowedMethods: [.get, .post, .put],
    allowedHeaders: [.contentType, .authorization],
    allowCredentials: true
)))

CookieMiddleware

Parses incoming cookies and provides methods for setting response cookies.

server.use(CookieMiddleware())

// In route handlers
server.get("/login") { req, res in
    // Read cookies
    let sessionId = req.cookie("sessionId")

    // Set cookies
    res.cookie("sessionId", "abc123", attributes: CookieAttributes(
        expires: Date().addingTimeInterval(3600),
        httpOnly: true,
        secure: true
    ))
}

BearerTokenMiddleware

Provides Bearer token authentication for protected routes with JWT support.

let authMiddleware = BearerTokenMiddleware(options: BearerTokenOptions(
    validator: { token in
        // Validate token against your auth system
        return token == "valid-api-key" || validateJWT(token)
    }
))

// Apply to specific routes
server.get("/protected", middleware: [authMiddleware]) { req, res in
    // Access authenticated user info
    if let authToken = req.middlewareStorage["authToken"] as? String {
        res.json("""{"message": "Access granted", "token": "\(authToken)"}""")
    } else {
        res.json("""{"message": "Access granted"}""")
    }
}

ETagMiddleware

Implements conditional requests with ETag support for caching.

server.use(ETagMiddleware(options: ETagOptions(
    strategy: .strong  // or .weak
)))

// In route handlers
server.get("/data") { req, res in
    let content = generateDynamicContent()
    res.sendWithETag(content, contentType: .applicationJson)
}

Creating Custom Middleware

The middleware system is designed to be easily extensible. You can create custom middleware by implementing the BaseMiddleware class or the ConfigurableMiddleware protocol.

Simple Middleware Example

import SwiftWebServer

/// Custom middleware that adds a request timestamp
class TimestampMiddleware: BaseMiddleware {

    override func execute(request: Request, response: Response, next: @escaping NextFunction) throws {
        // Add timestamp to request
        let timestamp = Date().timeIntervalSince1970
        request.middlewareStorage["timestamp"] = timestamp

        // Add custom header to response
        response.header("X-Request-Timestamp", "\(timestamp)")

        // Continue to next middleware
        try next()

        // Post-processing (after route handler)
        print("Request processed in \(Date().timeIntervalSince1970 - timestamp) seconds")
    }
}

// Usage
server.use(TimestampMiddleware())

Middleware Data Sharing

Middleware can share data through the request's middlewareStorage dictionary:

// In authentication middleware
class AuthMiddleware: BaseMiddleware {
    override func execute(request: Request, response: Response, next: @escaping NextFunction) throws {
        // Validate token and store user info
        if let user = validateAndGetUser(from: request) {
            request.middlewareStorage["currentUser"] = user
            request.middlewareStorage["isAuthenticated"] = true
        }
        try next()
    }
}

// In route handler
server.get("/profile") { req, res in
    if let user = req.middlewareStorage["currentUser"] as? User {
        res.json(user.toJSON())
    } else {
        res.status(.unauthorized).json(["error": "Not authenticated"])
    }
}

Configurable Middleware Example

/// Configuration options for rate limiting
public struct RateLimitOptions {
    public let maxRequests: Int
    public let windowSeconds: Int
    public let message: String

    public init(maxRequests: Int = 100, windowSeconds: Int = 60, message: String = "Rate limit exceeded") {
        self.maxRequests = maxRequests
        self.windowSeconds = windowSeconds
        self.message = message
    }

    public static let `default` = RateLimitOptions()
}

/// Rate limiting middleware
public class RateLimitMiddleware: BaseMiddleware, ConfigurableMiddleware {
    public typealias Options = RateLimitOptions

    private let options: RateLimitOptions
    private var requestCounts: [String: (count: Int, resetTime: Date)] = [:]
    private let queue = DispatchQueue(label: "rateLimit", attributes: .concurrent)

    public required init(options: RateLimitOptions = .default) {
        self.options = options
        super.init()
    }

    public convenience override init() {
        self.init(options: .default)
    }

    public override func execute(request: Request, response: Response, next: @escaping NextFunction) throws {
        let clientIP = request.clientIP ?? "unknown"
        let now = Date()

        let shouldAllow = queue.sync {
            if let entry = requestCounts[clientIP] {
                if now > entry.resetTime {
                    // Reset window
                    requestCounts[clientIP] = (count: 1, resetTime: now.addingTimeInterval(TimeInterval(options.windowSeconds)))
                    return true
                } else if entry.count < options.maxRequests {
                    // Increment count
                    requestCounts[clientIP] = (count: entry.count + 1, resetTime: entry.resetTime)
                    return true
                } else {
                    // Rate limit exceeded
                    return false
                }
            } else {
                // First request from this IP
                requestCounts[clientIP] = (count: 1, resetTime: now.addingTimeInterval(TimeInterval(options.windowSeconds)))
                return true
            }
        }

        if shouldAllow {
            try next()
        } else {
            response.status(.tooManyRequests).send(options.message)
        }
    }
}

// Usage
server.use(RateLimitMiddleware(options: RateLimitOptions(
    maxRequests: 50,
    windowSeconds: 60,
    message: "Too many requests. Please try again later."
)))

Routing

SwiftWebServer supports flexible routing with path parameters and multiple HTTP methods.

Basic Routes

// HTTP Methods
server.get("/users") { req, res in res.send("Get all users") }
server.post("/users") { req, res in res.send("Create user") }
server.put("/users/{id}") { req, res in res.send("Update user") }
server.delete("/users/{id}") { req, res in res.send("Delete user") }

Path Parameters

// Single parameter
server.get("/users/{id}") { req, res in
    let userId = req.pathParameters["id"] ?? "unknown"
    res.send("User ID: \(userId)")
}

// Multiple parameters
server.get("/users/{userId}/posts/{postId}") { req, res in
    let userId = req.pathParameters["userId"] ?? "unknown"
    let postId = req.pathParameters["postId"] ?? "unknown"
    res.json("""
    {
        "userId": "\(userId)",
        "postId": "\(postId)"
    }
    """)
}

Query Parameters

server.get("/search") { req, res in
    let query = req.queryParameters["q"] ?? ""
    let page = Int(req.queryParameters["page"] ?? "1") ?? 1

    res.json("""
    {
        "query": "\(query)",
        "page": \(page),
        "results": []
    }
    """)
}

Static File Serving

// Serve files from a directory
server.use(staticDirectory: "./public")

// Multiple static directories
server.use(staticDirectory: "./assets")
server.use(staticDirectory: "./uploads")

API Reference

SwiftWebServer Class

Initialization

let server = SwiftWebServer()           // Basic initialization
let server = SwiftWebServer(port: 8080) // With default port

Server Control

try server.start(port: 8080)    // Start server on specified port
server.stop()                   // Stop the server
server.status                   // Get current server status (.stopped, .starting, .running, .stopping)
server.currentPort              // Get current port (0 if not running)
server.isRunning                // Check if server is running

Middleware

server.use(middleware)                    // Add middleware
server.use(staticDirectory: "./public")   // Serve static files

Route Definition

server.get(pattern, handler)      // GET route
server.post(pattern, handler)     // POST route
server.put(pattern, handler)      // PUT route
server.delete(pattern, handler)   // DELETE route

Request Object

The Request object provides access to all incoming request data:

// Basic properties
req.method                    // HTTPMethod (.get, .post, etc.)
req.path                      // Request path ("/users/123")
req.httpVersion              // HTTP version ("HTTP/1.1")
req.headers                  // HTTPHeaders object
req.body                     // Raw request body as Data?
req.bodyString               // Request body as String?

// Parsed data
req.pathParameters           // Path parameters ["id": "123"]
req.queryParameters          // Query parameters ["page": "1"]
req.cookies                  // Parsed cookies ["session": "abc123"]
req.jsonBody                 // Parsed JSON body (if BodyParser middleware is used)
req.formBody                 // Parsed form data (if BodyParser middleware is used)
req.middlewareStorage        // Generic storage for middleware data sharing

// Convenience methods
req.header("Content-Type")   // Get header by name
req.header(.contentType)     // Get header by enum
req.cookie("sessionId")      // Get cookie by name
req.contentType              // Parsed content type
req.contentLength            // Content length as Int?
req.userAgent                // User-Agent header
req.host                     // Host header
req.clientIP                 // Client IP address
req.isSecure                 // Whether request is HTTPS
req.accepts(.applicationJson) // Check if client accepts content type

Response Object

The Response object provides methods for sending responses:

// Status codes
res.status(.ok)                    // Set status code
res.status(200)                    // Set status code by number

// Headers
res.header("Content-Type", "application/json")  // Set header
res.header(.contentType, "application/json")    // Set header by enum

// Response methods
res.send("Hello World")            // Send text response
res.json("""{"key": "value"}""")   // Send JSON response
res.html("<h1>Hello</h1>")         // Send HTML response
res.file("./public/index.html")    // Send file

// Cookies
res.cookie("name", "value")        // Set cookie
res.cookie("session", "abc123", attributes: CookieAttributes(
    expires: Date().addingTimeInterval(3600),
    httpOnly: true,
    secure: true
))
res.clearCookie("session")         // Clear cookie

// ETag support (with ETagMiddleware)
res.sendWithETag(content, contentType: .applicationJson)
res.notModified()                  // Send 304 Not Modified

// Redirects
res.redirect("/new-path")          // Temporary redirect (302)
res.redirect("/new-path", permanent: true)  // Permanent redirect (301)
res.redirectPermanent("/new-path") // Permanent redirect (301)
res.redirectTemporary("/new-path") // Temporary redirect (302)
res.redirectTemporaryPreserveMethod("/new-path")  // 307 redirect
res.redirectPermanentPreserveMethod("/new-path")  // 308 redirect

// Error responses with messages
res.badRequest("Invalid input data")
res.notFound("Resource not found")
res.internalServerError("Something went wrong")

// Method chaining
res.status(.ok)
   .header(.contentType, "application/json")
   .json("""{"message": "Success"}""")

Complete Example

Here's a comprehensive example showing a REST API with authentication, logging, and error handling:

import SwiftWebServer

let server = SwiftWebServer()

// Add middleware in order
server.use(LoggerMiddleware(options: LoggerOptions(level: .detailed)))
server.use(CORSMiddleware())
server.use(CookieMiddleware())
server.use(BodyParser())
server.use(ETagMiddleware())

// Authentication middleware for protected routes
let authMiddleware = BearerTokenMiddleware(options: BearerTokenOptions(
    validator: { token in
        // Validate token against your auth system
        return validateJWT(token) || validateDatabaseToken(token)
    }
))

// Public routes
server.get("/") { req, res in
    res.html("""
    <h1>Welcome to SwiftWebServer</h1>
    <p>A lightweight HTTP server for Swift</p>
    """)
}

server.get("/api/status") { req, res in
    res.sendWithETag("""
    {
        "status": "healthy",
        "timestamp": "\(Date().iso8601Formatted())",
        "version": "1.0.0"
    }
    """, contentType: .applicationJson)
}

// Protected routes
server.get("/api/users", middleware: [authMiddleware]) { req, res in
    let page = Int(req.queryParameters["page"] ?? "1") ?? 1
    let limit = Int(req.queryParameters["limit"] ?? "10") ?? 10

    res.json("""
    {
        "users": [],
        "pagination": {
            "page": \(page),
            "limit": \(limit),
            "total": 0
        }
    }
    """)
}

server.post("/api/users", middleware: [authMiddleware]) { req, res in
    guard let jsonBody = req.jsonBody,
          let userData = jsonBody as? [String: Any],
          let name = userData["name"] as? String else {
        res.status(.badRequest).json("""{"error": "Invalid user data"}""")
        return
    }

    // Create user logic here
    let userId = UUID().uuidString

    res.status(.created).json("""
    {
        "id": "\(userId)",
        "name": "\(name)",
        "created": "\(Date().iso8601Formatted())"
    }
    """)
}

// Error handling
server.get("/api/error") { req, res in
    res.status(.internalServerError).json("""
    {
        "error": "Something went wrong",
        "timestamp": "\(Date().iso8601Formatted())"
    }
    """)
}

// Serve static files
server.use(staticDirectory: "./public")

// Start server
do {
    try server.start(port: 8080)
    print("πŸš€ Server running on http://localhost:8080")
    print("πŸ“Š Status endpoint: http://localhost:8080/api/status")
    print("πŸ”’ Protected endpoint: http://localhost:8080/api/users (requires Bearer token)")

    // Keep the server running
    RunLoop.current.run()
} catch {
    print("❌ Failed to start server: \(error)")
}

Example Application

The SwiftWebServerExample project demonstrates a comprehensive blog application with both frontend and backend servers, featuring a native SwiftUI interface:

Architecture

  • Backend Server (Port 8080): REST API with JWT authentication, user management, and blog posts
  • Frontend Server (Port 3000): Serves static HTML/CSS/JS files with responsive design
  • SwiftUI Dashboard: Native iOS interface with server controls and data management
  • SwiftData Integration: Modern data persistence with automatic relationship management

Key Features

  • Blog Interface: Public blog page with responsive design and post details
  • Admin Login: Secure login with JWT token authentication
  • Admin Dashboard: Clean blog management interface for authenticated users
  • Session Management: Automatic token cleanup and session tracking
  • Real-time Logging: Request/response logging with filtering and haptic feedback
  • Data Management: Native SwiftUI interface for managing users, posts, and comments
  • Dual Server Setup: Separate frontend and backend servers for realistic deployment

SwiftUI Console Features

  • Dashboard Layout: Card-based interface with server status and data management
  • Server Controls: Start/stop servers with real-time status updates
  • Data Management: Create, edit, and delete users, posts, and comments
  • Session Monitoring: View and manage active authentication tokens
  • Console Logging: Real-time request/response logs with filtering options
  • Haptic Feedback: Enhanced user experience with tactile feedback

Authentication Flow

  1. Users access the blog at http://localhost:3000/
  2. Admin login is available at http://localhost:3000/login.html
  3. JWT tokens are issued upon successful authentication
  4. Admin dashboard at http://localhost:3000/admin.html validates tokens
  5. Automatic logout when tokens expire
  6. Session management through SwiftUI interface

Running the Example

  1. Open SwiftWebServerExample.xcodeproj in Xcode
  2. Run the project on iOS Simulator or device (iOS 17.0+)
  3. Start both servers using the dashboard controls
  4. Access the blog at http://localhost:3000/
  5. Use demo credentials: johndoe / password123
  6. Manage data through the native SwiftUI interface

Development Setup

  1. Clone the repository
  2. Open in Xcode or use Swift Package Manager
  3. Run tests: swift test
  4. Build: swift build

Requirements

  • iOS: 17.0+ (for SwiftUI example app)
  • macOS: 14.0+ (for command-line usage)
  • Xcode: 15.0+
  • Swift: 5.9+

Recent Updates

Version 1.0.0 Features

  • βœ… Session Management: Automatic JWT token cleanup and session tracking
  • βœ… Enhanced Authentication: Improved Bearer token middleware with detailed error responses
  • βœ… SwiftUI Integration: Native iOS dashboard with haptic feedback and real-time updates
  • βœ… SwiftData Support: Modern data persistence with automatic relationship management
  • βœ… Responsive Design: Mobile-first web interface with adaptive layouts
  • βœ… Error Handling: Comprehensive error responses with proper HTTP status codes
  • βœ… Middleware Improvements: Enhanced logging, CORS, and cookie handling
  • βœ… HTTP Redirects: Full support for temporary and permanent redirects

Testing

SwiftWebServer includes comprehensive unit tests for all middleware and core functionality:

# Run all tests
swift test

# Run specific test suite
swift test --filter SwiftWebServerTests

# Run with verbose output
swift test --verbose

Test Coverage

  • βœ… Core Server: Server lifecycle, routing, and request handling
  • βœ… Middleware: All built-in middleware components
  • βœ… HTTP Methods: GET, POST, PUT, DELETE, and other HTTP methods
  • βœ… Path Parameters: Route matching and parameter extraction
  • βœ… Authentication: Bearer token validation and error handling
  • βœ… CORS: Cross-origin request handling
  • βœ… Cookie Management: Cookie parsing and setting
  • βœ… Error Handling: Proper error responses and status codes

Contributing

We welcome contributions! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development Guidelines

  1. Code Style: Follow Swift conventions and use SwiftLint
  2. Testing: Add tests for new features and bug fixes
  3. Documentation: Update README and inline documentation
  4. Compatibility: Maintain backward compatibility when possible

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

swift based web server

Resources

License

Stars

Watchers

Forks

Packages

No packages published