🎯 Ejemplos recomendados
Balanced sample collections from various categories for you to explore
Funcionalidades Web macOS Swift - Ejemplos
Ejemplos de funcionalidades web macOS Swift incluyendo enrutamiento URL, pipeline de middleware y servicio de archivos estáticos
💻 Enrutamiento de Solicitudes HTTP swift
🟡 intermediate
⭐⭐⭐
Implementar enrutamiento flexible de solicitudes HTTP con coincidencia de patrones, extracción de parámetros y soporte RESTful API
⏱️ 30 min
🏷️ swift, macos, web, routing, http
Prerequisites:
Swift basics, HTTP protocol, Regular expressions
// macOS Swift Web Routing Examples
// Using Foundation and Vapor-inspired routing patterns
import Foundation
// MARK: - HTTP Models
// HTTP Request representation
struct HTTPRequest {
let method: HTTPMethod
let path: String
let headers: [String: String]
let body: Data?
var queryParameters: [String: String] = [:]
var pathParameters: [String: String] = [:]
}
enum HTTPMethod: String {
case GET = "GET"
case POST = "POST"
case PUT = "PUT"
case DELETE = "DELETE"
case PATCH = "PATCH"
case HEAD = "HEAD"
case OPTIONS = "OPTIONS"
case ANY = "*"
}
// HTTP Response representation
struct HTTPResponse {
var statusCode: HTTPStatusCode = .ok
var headers: [String: String] = [:]
var body: Data?
init(statusCode: HTTPStatusCode = .ok, headers: [String: String] = [:], body: Data? = nil) {
self.statusCode = statusCode
self.headers = headers
self.body = body
}
func setContentType(_ contentType: String) -> HTTPResponse {
var response = self
response.headers["Content-Type"] = contentType
return response
}
func setJSON(_ json: String) -> HTTPResponse {
var response = self
response.headers["Content-Type"] = "application/json"
response.body = json.data(using: .utf8)
return response
}
func setHTML(_ html: String) -> HTTPResponse {
var response = self
response.headers["Content-Type"] = "text/html"
response.body = html.data(using: .utf8)
return response
}
func setText(_ text: String) -> HTTPResponse {
var response = self
response.headers["Content-Type"] = "text/plain"
response.body = text.data(using: .utf8)
return response
}
}
enum HTTPStatusCode: Int {
case ok = 200
case created = 201
case accepted = 202
case noContent = 204
case movedPermanently = 301
case found = 302
case notModified = 304
case badRequest = 400
case unauthorized = 401
case forbidden = 403
case notFound = 404
case methodNotAllowed = 405
case conflict = 409
case internalServerError = 500
case notImplemented = 501
case badGateway = 502
case serviceUnavailable = 503
}
// MARK: - Route Handler
typealias RouteHandler = (HTTPRequest) -> HTTPResponse
// MARK: - Route
class Route {
let method: HTTPMethod
let pattern: String
let handler: RouteHandler
let regex: NSRegularExpression
let parameterNames: [String]
init(method: HTTPMethod, pattern: String, handler: @escaping RouteHandler) {
self.method = method
self.pattern = pattern
self.handler = handler
// Convert route pattern to regex
let (regexString, params) = Route.compilePattern(pattern)
self.regex = try! NSRegularExpression(pattern: regexString, options: [])
self.parameterNames = params
}
// Check if route matches request
func matches(request: HTTPRequest) -> Bool {
if method != .ANY && method != request.method {
return false
}
let range = NSRange(location: 0, length: request.path.utf16.count)
return regex.firstMatch(in: request.path, options: [], range: range) != nil
}
// Extract path parameters from request
func extractParameters(from request: HTTPRequest) -> [String: String] {
var parameters: [String: String] = [:]
let range = NSRange(location: 0, length: request.path.utf16.count)
if let match = regex.firstMatch(in: request.path, options: [], range: range) {
for (index, name) in parameterNames.enumerated() {
let captureRange = match.range(at: index + 1)
if captureRange.location != NSNotFound,
let range = Range(captureRange, in: request.path) {
parameters[name] = String(request.path[range])
}
}
}
return parameters
}
// Compile route pattern to regex
private static func compilePattern(_ pattern: String) -> (String, [String]) {
var regexPattern = "^"
var params: [String] = []
var current = pattern.startIndex
while current < pattern.endIndex {
if pattern[current] == ":" {
// Parameter found
let paramStart = pattern.index(after: current)
if let paramEnd = pattern[paramStart...].firstIndex(where: { $0 == "/" || $0 == "." }) {
let paramName = String(pattern[paramStart..<paramEnd])
params.append(paramName)
regexPattern += "([^/\.]+)"
current = paramEnd
} else {
let paramName = String(pattern[paramStart...])
params.append(paramName)
regexPattern += "([^/]+)"
current = pattern.endIndex
}
} else if pattern[current] == "*" {
// Wildcard
regexPattern += ".*"
current = pattern.index(after: current)
} else {
// Regular character
regexPattern += NSRegularExpression.escapedPattern(for: String(pattern[current]))
current = pattern.index(after: current)
}
}
regexPattern += "$"
return (regexPattern, params)
}
}
// MARK: - Router
class Router {
private var routes: [Route] = []
// Add route
@discardableResult
func add(method: HTTPMethod, path: String, handler: @escaping RouteHandler) -> Router {
let route = Route(method: method, pattern: path, handler: handler)
routes.append(route)
print("Added route: \(method.rawValue) \(path)")
return self
}
// Convenience methods
func get(_ path: String, handler: @escaping RouteHandler) -> Router {
add(method: .GET, path: path, handler: handler)
}
func post(_ path: String, handler: @escaping RouteHandler) -> Router {
add(method: .POST, path: path, handler: handler)
}
func put(_ path: String, handler: @escaping RouteHandler) -> Router {
add(method: .PUT, path: path, handler: handler)
}
func delete(_ path: String, handler: @escaping RouteHandler) -> Router {
add(method: .DELETE, path: path, handler: handler)
}
func patch(_ path: String, handler: @escaping RouteHandler) -> Router {
add(method: .PATCH, path: path, handler: handler)
}
func any(_ path: String, handler: @escaping RouteHandler) -> Router {
add(method: .ANY, path: path, handler: handler)
}
// Route request
func route(request: HTTPRequest) -> HTTPResponse {
for route in routes {
if route.matches(request: request) {
var routedRequest = request
routedRequest.pathParameters = route.extractParameters(from: request)
print("Routing: \(request.method.rawValue) \(request.path) -> \(route.pattern)")
do {
return route.handler(routedRequest)
} catch {
return HTTPResponse(statusCode: .internalServerError, body: "Internal Server Error".data(using: .utf8))
}
}
}
// No matching route
return HTTPResponse(statusCode: .notFound).setText("404 Not Found")
}
// Group routes with prefix
func group(prefix: String, configure: (Router) -> Void) -> Router {
let groupRouter = Router()
configure(groupRouter)
// Add all routes from group with prefix
for route in groupRouter.routes {
let fullPath = prefix + route.pattern
let newRoute = Route(method: route.method, pattern: fullPath, handler: route.handler)
routes.append(newRoute)
}
return self
}
}
// MARK: - RESTful API Builder
class RESTfulAPI {
let router = Router()
private let resourceName: String
init(resourceName: String) {
self.resourceName = resourceName
setupDefaultRoutes()
}
private func setupDefaultRoutes() {
let path = "/\(resourceName)"
// List all resources
router.get(path) { request in
let response = HTTPResponse()
let json = """
{
"resource": "\(self.resourceName)",
"items": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"},
{"id": 3, "name": "Item 3"}
],
"count": 3
}
"""
return response.setJSON(json)
}
// Get specific resource
router.get("\(path)/:id") { request in
let id = request.pathParameters["id"] ?? "0"
let response = HTTPResponse()
let json = """
{
"id": \(id),
"name": "Item \(id)",
"description": "This is item \(id)"
}
"""
return response.setJSON(json)
}
// Create new resource
router.post(path) { request in
let response = HTTPResponse(statusCode: .created)
let json = """
{
"id": 4,
"status": "created",
"message": "Resource created successfully"
}
"""
return response.setJSON(json)
}
// Update resource
router.put("\(path)/:id") { request in
let id = request.pathParameters["id"] ?? "0"
let response = HTTPResponse()
let json = """
{
"id": \(id),
"status": "updated",
"message": "Resource \(id) updated successfully"
}
"""
return response.setJSON(json)
}
// Delete resource
router.delete("\(path)/:id") { request in
let id = request.pathParameters["id"] ?? "0"
let response = HTTPResponse()
let json = """
{
"id": \(id),
"status": "deleted",
"message": "Resource \(id) deleted successfully"
}
"""
return response.setJSON(json)
}
}
// Customize routes
func customize(using builder: (Router) -> Void) -> RESTfulAPI {
builder(router)
return self
}
// Get router
func build() -> Router {
return router
}
}
// MARK: - Route Middleware
class RouteMiddleware {
let handler: (HTTPRequest, @escaping (HTTPRequest) -> HTTPResponse) -> HTTPResponse
init(handler: @escaping (HTTPRequest, @escaping (HTTPRequest) -> HTTPResponse) -> HTTPResponse) {
self.handler = handler
}
func process(request: HTTPRequest, next: @escaping (HTTPRequest) -> HTTPResponse) -> HTTPResponse {
return handler(request, next)
}
}
// Middleware implementations
extension RouteMiddleware {
// Logging middleware
static func logging() -> RouteMiddleware {
return RouteMiddleware { request, next in
print("[\(Date())] \(request.method.rawValue) \(request.path)")
let response = next(request)
print("[Response] \(response.statusCode.rawValue)")
return response
}
}
// Authentication middleware
static func authenticate() -> RouteMiddleware {
return RouteMiddleware { request, next in
if let auth = request.headers["Authorization"], auth.hasPrefix("Bearer ") {
return next(request)
} else {
return HTTPResponse(statusCode: .unauthorized).setJSON("{"error": "Unauthorized"}")
}
}
}
// CORS middleware
static func cors() -> RouteMiddleware {
return RouteMiddleware { request, next in
var response = next(request)
if request.method == .OPTIONS {
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
return response
}
response.headers["Access-Control-Allow-Origin"] = "*"
return response
}
}
}
// MARK: - Demonstration
func demonstrateWebRouting() {
print("=== macOS Swift Web Routing Examples ===\n")
// 1. Basic router setup
print("--- 1. Basic Router Setup ---")
let router = Router()
// Home page
router.get("/") { request in
let html = """
<!DOCTYPE html>
<html>
<head><title>Swift Routing Demo</title></head>
<body>
<h1>Welcome to Swift Routing!</h1>
<p>Try these routes:</p>
<ul>
<li><a href="/api/hello">GET /api/hello</a></li>
<li><a href="/api/hello/John">GET /api/hello/John</a></li>
<li><a href="/api/users">GET /api/users</a></li>
<li><a href="/api/users/123">GET /api/users/123</a></li>
<li><a href="/api/search?q=swift">GET /api/search?q=swift</a></li>
</ul>
</body>
</html>
"""
return HTTPResponse().setHTML(html)
}
// Simple API endpoint
router.get("/api/hello") { request in
return HTTPResponse().setJSON("{"message": "Hello, World!"}")
}
// Parameterized route
router.get("/api/hello/:name") { request in
let name = request.pathParameters["name"] ?? "Guest"
return HTTPResponse().setJSON("{"message": "Hello, \(name)!"}")
}
// Multiple parameters
router.get("/api/users/:userId/posts/:postId") { request in
let userId = request.pathParameters["userId"] ?? "0"
let postId = request.pathParameters["postId"] ?? "0"
return HTTPResponse().setJSON("{"userId": \(userId), "postId": \(postId)}")
}
// Wildcard route
router.get("/api/files/*") { request in
return HTTPResponse().setJSON("{"path": "\(request.path)"}")
}
// 2. RESTful API
print("\n--- 2. RESTful API ---")
let userAPI = RESTfulAPI(resourceName: "users")
// Customize with additional routes
userAPI.customize { router in
router.get("/users/:id/posts") { request in
let id = request.pathParameters["id"] ?? "0"
return HTTPResponse().setJSON("{"userId": \(id), "posts": []}")
}
}
// Add RESTful routes to main router
let apiRouter = userAPI.build()
for route in apiRouter.routes {
router.routes.append(route)
}
// 3. Route groups
print("\n--- 3. Route Groups ---")
router.group(prefix: "/api/v1") { group in
group.get("/status") { request in
return HTTPResponse().setJSON("{"version": "v1", "status": "ok"}")
}
group.get("/info") { request in
return HTTPResponse().setJSON("{"version": "v1", "info": "API v1"}")
}
}
// 4. Test routing
print("\n--- 4. Testing Routes ---")
let testRequests = [
HTTPRequest(method: .GET, path: "/", headers: [:], body: nil),
HTTPRequest(method: .GET, path: "/api/hello", headers: [:], body: nil),
HTTPRequest(method: .GET, path: "/api/hello/John", headers: [:], body: nil),
HTTPRequest(method: .GET, path: "/api/users/123/posts/456", headers: [:], body: nil),
HTTPRequest(method: .GET, path: "/api/files/documents/report.pdf", headers: [:], body: nil),
HTTPRequest(method: .GET, path: "/api/users", headers: [:], body: nil),
HTTPRequest(method: .GET, path: "/api/users/42", headers: [:], body: nil),
HTTPRequest(method: .GET, path: "/api/v1/status", headers: [:], body: nil),
HTTPRequest(method: .GET, path: "/nonexistent", headers: [:], body: nil)
]
for request in testRequests {
print("\nRequest: \(request.method.rawValue) \(request.path)")
let response = router.route(request: request)
print("Response: \(response.statusCode.rawValue)")
if let body = response.body,
let bodyString = String(data: body, encoding: .utf8),
bodyString.count < 200 {
print("Body: \(bodyString)")
}
}
print("\n=== Web Routing Demo Completed ===")
}
// Run demonstration
demonstrateWebRouting()
💻 Middleware de Procesamiento de Solicitudes swift
🟡 intermediate
⭐⭐⭐⭐
Implementar pipeline de middleware para registro, autenticación, CORS y manejo de errores
⏱️ 35 min
🏷️ swift, macos, web, middleware, pipeline
Prerequisites:
Swift basics, HTTP protocol, Functional programming
// macOS Swift Middleware Pipeline Examples
// Using Foundation and async/await patterns
import Foundation
// MARK: - HTTP Context
struct HTTPContext {
var request: HTTPRequest
var response: HTTPResponse
var metadata: [String: Any] = [:]
init(request: HTTPRequest) {
self.request = request
self.response = HTTPResponse()
}
}
// MARK: - Middleware Protocol
protocol Middleware {
func process(context: HTTPContext, next: @escaping (HTTPContext) -> HTTPContext) -> HTTPContext
}
// MARK: - Middleware Pipeline
class MiddlewarePipeline {
private var middlewares: [Middleware] = []
// Add middleware to pipeline
@discardableResult
func use(_ middleware: Middleware) -> MiddlewarePipeline {
middlewares.append(middleware)
print("Added middleware: \(type(of: middleware))")
return self
}
// Process request through pipeline
func process(request: HTTPRequest, finalHandler: @escaping (HTTPContext) -> HTTPContext) -> HTTPContext {
var context = HTTPContext(request: request)
// Build middleware chain
let chain = buildChain(index: 0, finalHandler: finalHandler)
return chain(context)
}
// Build middleware chain recursively
private func buildChain(index: Int, finalHandler: @escaping (HTTPContext) -> HTTPContext) -> (HTTPContext) -> HTTPContext {
if index < middlewares.count {
let middleware = middlewares[index]
return { context in
let next = self.buildChain(index: index + 1, finalHandler: finalHandler)
return middleware.process(context: context, next: next)
}
} else {
return finalHandler
}
}
}
// MARK: - Middleware Implementations
// 1. Logging Middleware
class LoggingMiddleware: Middleware {
let logLevel: LogLevel
init(logLevel: LogLevel = .info) {
self.logLevel = logLevel
}
func process(context: HTTPContext, next: @escaping (HTTPContext) -> HTTPContext) -> HTTPContext {
let requestId = UUID().uuidString.prefix(8)
let startTime = Date()
log(.info, "[\(requestId]) \(context.request.method.rawValue) \(context.request.path) -> Started")
// Store metadata
context.metadata["requestId"] = String(requestId)
context.metadata["startTime"] = startTime
// Process next middleware
let result = next(context)
// Log completion
let duration = Date().timeIntervalSince(startTime)
log(.info, "[\(requestId)] \(context.request.method.rawValue) \(context.request.path) -> \(result.response.statusCode.rawValue) (\(String(format: "%.0f", duration * 1000))ms)")
return result
}
private func log(_ level: LogLevel, _ message: String) {
if level.rawValue >= logLevel.rawValue {
print("[\(level)] \(message)")
}
}
}
enum LogLevel: Int, Comparable {
case debug = 0
case info = 1
case warning = 2
case error = 3
static func < (lhs: LogLevel, rhs: LogLevel) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
// 2. Authentication Middleware
class AuthenticationMiddleware: Middleware {
let authPaths: Set<String>
let excludePaths: Set<String>
init(authPaths: Set<String>, excludePaths: Set<String> = []) {
self.authPaths = authPaths
self.excludePaths = excludePaths
}
func process(context: HTTPContext, next: @escaping (HTTPContext) -> HTTPContext) -> HTTPContext {
let path = context.request.path
// Check if path requires authentication
if authPaths.contains(where: { path.hasPrefix($0) }) &&
!excludePaths.contains(where: { path.hasPrefix($0) }) {
// Check for Authorization header
guard let authHeader = context.request.headers["Authorization"],
authHeader.hasPrefix("Bearer ") else {
log(.warning, "Authentication required for: \(path)")
var unauthorizedContext = context
unauthorizedContext.response = HTTPResponse(statusCode: .unauthorized)
.setJSON("{"error": "Authentication required", "code": 401}")
return unauthorizedContext
}
// Validate token (simplified)
let token = String(authHeader.dropFirst(7))
if !isValidToken(token) {
log(.warning, "Invalid token for: \(path)")
var forbiddenContext = context
forbiddenContext.response = HTTPResponse(statusCode: .forbidden)
.setJSON("{"error": "Invalid token", "code": 403}")
return forbiddenContext
}
// Store user info in context
context.metadata["authenticated"] = true
context.metadata["userId"] = extractUserId(from: token)
log(.info, "User authenticated: \(context.metadata["userId"] ?? "unknown")")
}
return next(context)
}
private func isValidToken(_ token: String) -> Bool {
// Simplified token validation
return token.count > 20
}
private func extractUserId(from token: String) -> String {
// Extract user ID from token (simplified)
return "user_\(token.prefix(8))"
}
private func log(_ level: LogLevel, _ message: String) {
print("[\(level)] \(message)")
}
}
// 3. CORS Middleware
class CORSMiddleware: Middleware {
let allowedOrigins: [String]
let allowedMethods: [String]
let allowedHeaders: [String]
init(allowedOrigins: [String] = ["*"],
allowedMethods: [String] = ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: [String] = ["Content-Type", "Authorization"]) {
self.allowedOrigins = allowedOrigins
self.allowedMethods = allowedMethods
self.allowedHeaders = allowedHeaders
}
func process(context: HTTPContext, next: @escaping (HTTPContext) -> HTTPContext) -> HTTPContext {
// Handle preflight request
if context.request.method == .OPTIONS {
var preflightContext = context
preflightContext.response.statusCode = .ok
preflightContext.response.headers["Access-Control-Allow-Origin"] = allowedOrigins.first ?? "*"
preflightContext.response.headers["Access-Control-Allow-Methods"] = allowedMethods.joined(separator: ", ")
preflightContext.response.headers["Access-Control-Allow-Headers"] = allowedHeaders.joined(separator: ", ")
preflightContext.response.headers["Access-Control-Max-Age"] = "86400"
return preflightContext
}
// Add CORS headers to response
let result = next(context)
result.response.headers["Access-Control-Allow-Origin"] = allowedOrigins.first ?? "*"
result.response.headers["Access-Control-Allow-Methods"] = allowedMethods.joined(separator: ", ")
result.response.headers["Access-Control-Allow-Headers"] = allowedHeaders.joined(separator: ", ")
if let origin = context.request.headers["Origin"] {
result.response.headers["Access-Control-Allow-Origin"] = origin
result.response.headers["Vary"] = "Origin"
}
return result
}
}
// 4. Rate Limiting Middleware
class RateLimitingMiddleware: Middleware {
private var requestCounts: [String: [(Date, Int)]] = [:]
private let maxRequests: Int
private let windowInSeconds: Int
private let cleanupInterval: Int
init(maxRequests: Int = 100, windowInSeconds: Int = 60, cleanupInterval: Int = 300) {
self.maxRequests = maxRequests
self.windowInSeconds = windowInSeconds
self.cleanupInterval = cleanupInterval
}
func process(context: HTTPContext, next: @escaping (HTTPContext) -> HTTPContext) -> HTTPContext {
let clientId = getClientId(from: context.request)
let now = Date()
// Clean up old entries
if Int.random(in: 1...100) == 1 { // Occasionally cleanup
cleanup()
}
// Check rate limit
if isRateLimited(clientId: clientId, at: now) {
log(.warning, "Rate limit exceeded for: \(clientId)")
var rateLimitedContext = context
rateLimitedContext.response = HTTPResponse(statusCode: .serviceUnavailable)
.setJSON("{"error": "Rate limit exceeded", "code": 429}")
rateLimitedContext.response.headers["Retry-After"] = String(windowInSeconds)
return rateLimitedContext
}
// Record request
recordRequest(clientId: clientId, at: now)
return next(context)
}
private func getClientId(from request: HTTPRequest) -> String {
// Use IP or User-Agent as client identifier
return request.headers["X-Forwarded-For"] ??
request.headers["X-Real-IP"] ??
request.headers["User-Agent"] ??
"unknown"
}
private func isRateLimited(clientId: String, at now: Date) -> Bool {
guard let requests = requestCounts[clientId] else {
return false
}
let cutoff = now.addingTimeInterval(-Double(windowInSeconds))
let recentRequests = requests.filter { $0.0 > cutoff }
return recentRequests.count >= maxRequests
}
private func recordRequest(clientId: String, at now: Date) {
if requestCounts[clientId] == nil {
requestCounts[clientId] = []
}
requestCounts[clientId]?.append((now, 1))
}
private func cleanup() {
let cutoff = Date().addingTimeInterval(-Double(cleanupInterval))
for clientId in requestCounts.keys {
requestCounts[clientId]?..removeAll { $0.0 < cutoff }
}
}
private func log(_ level: LogLevel, _ message: String) {
print("[\(level)] \(message)")
}
}
// 5. Request Body Parsing Middleware
class BodyParsingMiddleware: Middleware {
func process(context: HTTPContext, next: @escaping (HTTPContext) -> HTTPContext) -> HTTPContext {
guard let body = context.request.body else {
return next(context)
}
// Check content type
let contentType = context.request.headers["Content-Type"] ?? ""
if contentType.contains("application/json") {
// Parse JSON body
if let json = try? JSONSerialization.jsonObject(with: body, options: []) as? [String: Any] {
context.metadata["parsedBody"] = json
log(.info, "JSON body parsed: \(json.keys.count) fields")
}
} else if contentType.contains("application/x-www-form-urlencoded") {
// Parse form data
if let bodyString = String(data: body, encoding: .utf8) {
let formData = parseFormData(bodyString)
context.metadata["parsedBody"] = formData
log(.info, "Form data parsed: \(formData.keys.count) fields")
}
} else {
// Store raw body
context.metadata["rawBody"] = body
}
return next(context)
}
private func parseFormData(_ string: String) -> [String: String] {
var result: [String: String] = [:]
let pairs = string.components(separatedBy: "&")
for pair in pairs {
let components = pair.components(separatedBy: "=")
if components.count == 2,
let key = components[0].removingPercentEncoding,
let value = components[1].removingPercentEncoding {
result[key] = value
}
}
return result
}
private func log(_ level: LogLevel, _ message: String) {
print("[\(level)] \(message)")
}
}
// 6. Error Handling Middleware
class ErrorHandlingMiddleware: Middleware {
func process(context: HTTPContext, next: @escaping (HTTPContext) -> HTTPContext) -> HTTPContext {
do {
return next(context)
} catch {
log(.error, "Unhandled error: \(error)")
var errorContext = context
let requestId = context.metadata["requestId"] as? String ?? "unknown"
let errorJson = """
{
"error": "Internal Server Error",
"message": "\(error.localizedDescription)",
"requestId": "\(requestId)"
}
"""
errorContext.response = HTTPResponse(statusCode: .internalServerError)
.setJSON(errorJson)
return errorContext
}
}
private func log(_ level: LogLevel, _ message: String) {
print("[\(level)] \(message)")
}
}
// 7. Response Compression Middleware
class CompressionMiddleware: Middleware {
let minSize: Int
init(minSize: Int = 1024) {
self.minSize = minSize
}
func process(context: HTTPContext, next: @escaping (HTTPContext) -> HTTPContext) -> HTTPContext {
let result = next(context)
// Check if response should be compressed
guard let body = result.response.body,
body.count > minSize,
shouldCompress(for: context.request) else {
return result
}
// Compress body (simplified - just remove whitespace for JSON)
if let contentType = result.response.headers["Content-Type"],
contentType.contains("application/json"),
let bodyString = String(data: body, encoding: .utf8) {
let compressed = bodyString.split(separator: " ").joined()
if let compressedData = compressed.data(using: .utf8) {
result.response.body = compressedData
result.response.headers["Content-Encoding"] = "gzip"
result.response.headers["X-Original-Size"] = String(body.count)
result.response.headers["X-Compressed-Size"] = String(compressedData.count)
log(.info, "Compressed response: \(body.count) -> \(compressedData.count) bytes")
}
}
return result
}
private func shouldCompress(for request: HTTPRequest) -> Bool {
// Check Accept-Encoding header
if let acceptEncoding = request.headers["Accept-Encoding"] {
return acceptEncoding.contains("gzip") || acceptEncoding.contains("*")
}
return false
}
private func log(_ level: LogLevel, _ message: String) {
print("[\(level)] \(message)")
}
}
// 8. Timing Middleware
class TimingMiddleware: Middleware {
func process(context: HTTPContext, next: @escaping (HTTPContext) -> HTTPContext) -> HTTPContext {
let startTime = Date()
let result = next(context)
let duration = Date().timeIntervalSince(startTime)
result.response.headers["X-Response-Time"] = String(format: "%.3f", duration)
return result
}
}
// MARK: - HTTP Types (from previous example)
struct HTTPRequest {
let method: HTTPMethod
let path: String
let headers: [String: String]
let body: Data?
}
enum HTTPMethod: String {
case GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, ANY
}
struct HTTPResponse {
var statusCode: HTTPStatusCode = .ok
var headers: [String: String] = [:]
var body: Data?
func setJSON(_ json: String) -> HTTPResponse {
var response = self
response.headers["Content-Type"] = "application/json"
response.body = json.data(using: .utf8)
return response
}
}
enum HTTPStatusCode: Int {
case ok = 200
case unauthorized = 401
case forbidden = 403
case notFound = 404
case serviceUnavailable = 503
case internalServerError = 500
}
// MARK: - Demonstration
func demonstrateMiddleware() {
print("=== macOS Swift Middleware Pipeline Examples ===\n")
// 1. Create pipeline with multiple middlewares
print("--- 1. Creating Middleware Pipeline ---")
let pipeline = MiddlewarePipeline()
pipeline
.use(LoggingMiddleware(logLevel: .info))
.use(ErrorHandlingMiddleware())
.use(CORSMiddleware())
.use(TimingMiddleware())
// 2. Test public route (no authentication)
print("\n--- 2. Testing Public Route ---")
let publicRequest = HTTPRequest(
method: .GET,
path: "/api/status",
headers: ["User-Agent": "TestClient"],
body: nil
)
let publicContext = pipeline.process(request: publicRequest) { context in
var ctx = context
ctx.response = HTTPResponse().setJSON("{"status": "ok", "message": "Public access"}")
return ctx
}
print("Public route response: \(publicContext.response.statusCode.rawValue)")
// 3. Test authenticated route
print("\n--- 3. Testing Authenticated Route ---")
let authPipeline = MiddlewarePipeline()
authPipeline
.use(LoggingMiddleware())
.use(AuthenticationMiddleware(authPaths: ["/api/protected"]))
.use(ErrorHandlingMiddleware())
let unauthRequest = HTTPRequest(
method: .GET,
path: "/api/protected/data",
headers: ["User-Agent": "TestClient"],
body: nil
)
let unauthContext = authPipeline.process(request: unauthRequest) { context in
var ctx = context
ctx.response = HTTPResponse().setJSON("{"data": "sensitive"}")
return ctx
}
print("Unauthorized access: \(unauthContext.response.statusCode.rawValue)")
// 4. Test with valid token
let authRequest = HTTPRequest(
method: .GET,
path: "/api/protected/data",
headers: ["Authorization": "Bearer valid_token_here_12345678"],
body: nil
)
let authContext = authPipeline.process(request: authRequest) { context in
var ctx = context
let userId = context.metadata["userId"] as? String ?? "unknown"
ctx.response = HTTPResponse().setJSON("{"data": "sensitive", "user": "\(userId)"}")
return ctx
}
print("Authorized access: \(authContext.response.statusCode.rawValue)")
// 5. Test rate limiting
print("\n--- 4. Testing Rate Limiting ---")
let rateLimitPipeline = MiddlewarePipeline()
rateLimitPipeline
.use(RateLimitingMiddleware(maxRequests: 3, windowInSeconds: 60))
.use(LoggingMiddleware())
// Make multiple requests
for i in 1...5 {
let request = HTTPRequest(
method: .GET,
path: "/api/test",
headers: ["User-Agent": "TestClient"],
body: nil
)
let context = rateLimitPipeline.process(request: request) { context in
var ctx = context
ctx.response = HTTPResponse().setJSON("{"request": \(i)}")
return ctx
}
print("Request \(i): \(context.response.statusCode.rawValue)")
}
// 6. Test body parsing
print("\n--- 5. Testing Body Parsing ---")
let bodyPipeline = MiddlewarePipeline()
bodyPipeline
.use(BodyParsingMiddleware())
.use(LoggingMiddleware())
let jsonBody = """
{
"name": "John Doe",
"email": "[email protected]"
}
""".data(using: .utf8)
let postRequest = HTTPRequest(
method: .POST,
path: "/api/users",
headers: ["Content-Type": "application/json"],
body: jsonBody
)
let postContext = bodyPipeline.process(request: postRequest) { context in
var ctx = context
if let body = context.metadata["parsedBody"] as? [String: Any] {
ctx.response = HTTPResponse().setJSON("{"received": \(body.keys.count) fields}")
}
return ctx
}
print("POST request: \(postContext.response.statusCode.rawValue)")
print("\n=== Middleware Pipeline Demo Completed ===")
}
// Run demonstration
demonstrateMiddleware()
💻 Servicio de Archivos Estáticos swift
🟡 intermediate
⭐⭐⭐
Implementar servicio eficiente de archivos estáticos con detección de tipos MIME, encabezados de caché y características de seguridad
⏱️ 25 min
🏷️ swift, macos, web, static files, server
Prerequisites:
Swift basics, File system operations, HTTP protocol
// macOS Swift Static File Serving Examples
// Using Foundation and URL handling
import Foundation
// MARK: - Static File Server Configuration
struct StaticFileConfig {
var rootDirectory: String
var enableDirectoryBrowsing: Bool = false
var enableDefaultFiles: Bool = true
var defaultFileNames: [String] = ["index.html", "default.html", "home.html"]
var enableETag: Bool = true
var enableLastModified: Bool = true
var cacheMaxAge: Int = 3600 // 1 hour
var enableCompression: Bool = true
var allowedExtensions: [String] = [
"html", "htm", "css", "js", "json", "xml", "txt", "md",
"png", "jpg", "jpeg", "gif", "bmp", "svg", "ico",
"pdf", "zip", "woff", "woff2", "ttf"
]
var indexFileName: String {
return defaultFileNames.first ?? "index.html"
}
}
// MARK: - MIME Type Detector
class MIMETypeDetector {
static let shared = MIMETypeDetector()
private let mimeTypes: [String: String] = [
// Text files
"html": "text/html",
"htm": "text/html",
"css": "text/css",
"js": "application/javascript",
"json": "application/json",
"xml": "application/xml",
"txt": "text/plain",
"md": "text/markdown",
"csv": "text/csv",
// Images
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"gif": "image/gif",
"bmp": "image/bmp",
"svg": "image/svg+xml",
"ico": "image/x-icon",
"webp": "image/webp",
// Fonts
"woff": "font/woff",
"woff2": "font/woff2",
"ttf": "font/ttf",
"otf": "font/otf",
"eot": "application/vnd.ms-fontobject",
// Documents
"pdf": "application/pdf",
"zip": "application/zip",
"tar": "application/x-tar",
"gz": "application/gzip",
// Audio/Video
"mp3": "audio/mpeg",
"wav": "audio/wav",
"mp4": "video/mp4",
"webm": "video/webm"
]
func detectMIMEType(for extension: String) -> String {
let ext = extension.lowercased()
return mimeTypes[ext] ?? "application/octet-stream"
}
func detectMIMEType(for url: URL) -> String {
return detectMIMEType(for: url.pathExtension)
}
}
// MARK: - Static File Server
class StaticFileServer {
let config: StaticFileConfig
private let fileManager = FileManager.default
private let mimeDetector = MIMETypeDetector.shared
init(config: StaticFileConfig) {
self.config = config
}
convenience init(rootDirectory: String) {
var config = StaticFileConfig(rootDirectory: rootDirectory)
self.init(config: config)
}
// Serve file for request path
func serveFile(for path: String) -> HTTPResponse {
// Normalize and validate path
let normalizedPath = normalizePath(path)
// Security check: prevent path traversal
if isPathTraversal(normalizedPath) {
return HTTPResponse(statusCode: .forbidden)
.setText("Forbidden: Path traversal detected")
}
// Resolve full path
let fullPath = resolvePath(normalizedPath)
// Check if path exists
var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: fullPath, isDirectory: &isDirectory) else {
return HTTPResponse(statusCode: .notFound)
.setText("404 - File not found: \(path)")
}
// Handle directory
if isDirectory.boolValue {
return serveDirectory(at: fullPath, path: normalizedPath)
}
// Check file extension
let fileExtension = (fullPath as NSString).pathExtension.lowercased()
if !config.allowedExtensions.contains(fileExtension) {
return HTTPResponse(statusCode: .forbidden)
.setText("Forbidden: File type not allowed")
}
// Serve file
return serveFile(at: fullPath)
}
// Serve individual file
private func serveFile(at fullPath: String) -> HTTPResponse {
do {
// Get file attributes
let attributes = try fileManager.attributesOfItem(atPath: fullPath)
let fileSize = attributes[.size] as? UInt64 ?? 0
let modificationDate = attributes[.modificationDate] as? Date ?? Date()
// Read file content
let fileData = try Data(contentsOf: URL(fileURLWithPath: fullPath))
// Create response
var response = HTTPResponse()
response.body = fileData
// Set content type
let fileExtension = (fullPath as NSString).pathExtension.lowercased()
let mimeType = mimeDetector.detectMIMEType(for: fileExtension)
response.headers["Content-Type"] = mimeType
// Set content length
response.headers["Content-Length"] = String(fileSize)
// Set cache headers
if config.enableETag {
let eTag = generateETag(for: fullPath, size: fileSize, modifiedDate: modificationDate)
response.headers["ETag"] = eTag
response.headers["Cache-Control"] = "public, max-age=\(config.cacheMaxAge)"
}
if config.enableLastModified {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss z"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
response.headers["Last-Modified"] = dateFormatter.string(from: modificationDate)
}
// Set security headers
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
print("Served: \(fullPath) (\(fileSize) bytes, \(mimeType))")
return response
} catch {
print("Error serving file: \(error)")
return HTTPResponse(statusCode: .internalServerError)
.setText("Error reading file")
}
}
// Serve directory
private func serveDirectory(at fullPath: String, path: String) -> HTTPResponse {
// Try to serve default file
if config.enableDefaultFiles {
for defaultFile in config.defaultFileNames {
let defaultFilePath = (fullPath as NSString).appendingPathComponent(defaultFile)
if fileManager.fileExists(atPath: defaultFilePath) {
return serveFile(at: defaultFilePath)
}
}
}
// Directory browsing
if config.enableDirectoryBrowsing {
return serveDirectoryListing(at: fullPath, path: path)
}
// No default file and browsing disabled
return HTTPResponse(statusCode: .forbidden)
.setText("Forbidden: Directory browsing disabled")
}
// Serve directory listing
private func serveDirectoryListing(at fullPath: String, path: String) -> HTTPResponse {
do {
let contents = try fileManager.contentsOfDirectory(atPath: fullPath)
var directories: [String] = []
var files: [(name: String, size: UInt64, modified: Date)] = []
for item in contents {
let itemPath = (fullPath as NSString).appendingPathComponent(item)
var isDirectory: ObjCBool = false
fileManager.fileExists(atPath: itemPath, isDirectory: &isDirectory)
if isDirectory.boolValue {
directories.append(item)
} else {
if let attributes = try? fileManager.attributesOfItem(atPath: itemPath) {
let size = attributes[.size] as? UInt64 ?? 0
let modified = attributes[.modificationDate] as? Date ?? Date()
files.append((item, size, modified))
}
}
}
// Generate HTML listing
let html = generateDirectoryListingHTML(path: path, directories: directories, files: files)
var response = HTTPResponse()
response.headers["Content-Type"] = "text/html"
response.body = html.data(using: .utf8)
return response
} catch {
return HTTPResponse(statusCode: .internalServerError)
.setText("Error reading directory")
}
}
// Generate directory listing HTML
private func generateDirectoryListingHTML(path: String, directories: [String], files: [(name: String, size: UInt64, modified: Date)]) -> String {
var html = """
<!DOCTYPE html>
<html>
<head>
<title>Directory Listing: \(path)</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
table { border-collapse: collapse; width: 100%; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #f4f4f4; }
a { text-decoration: none; color: #0066cc; }
a:hover { text-decoration: underline; }
.icon { margin-right: 10px; }
</style>
</head>
<body>
<h1>Directory listing for \(path.isEmpty ? "/" : path)</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>Modified</th>
</tr>
</thead>
<tbody>
"""
// Parent directory link
if path != "/" && !path.isEmpty {
let parentPath = (path as NSString).deletingLastPathComponent
html += "\n<tr><td><span class=\"icon\">📁</span><a href=\"\(parentPath)\">../</a></td><td>-</td><td>-</td></tr>"
}
// Directories
for dir in directories.sorted() {
let dirPath = path.isEmpty ? "/\(dir)" : "\(path)/\(dir)"
html += "\n<tr><td><span class=\"icon\">📁</span><a href=\"\(dirPath)/\">\(dir)/</a></td><td>-</td><td>-</td></tr>"
}
// Files
for file in files.sorted(by: { $0.name < $1.name }) {
let filePath = path.isEmpty ? "/\(file.name)" : "\(path)/\(file.name)"
let icon = getFileIcon(for: file.name)
let size = formatBytes(file.size)
let modified = formatDate(file.modified)
html += "\n<tr><td><span class=\"icon\">\(icon)</span><a href=\"\(filePath)\">\(file.name)</a></td><td>\(size)</td><td>\(modified)</td></tr>"
}
html += """
</tbody>
</table>
<p>Total: \(directories.count) directories, \(files.count) files</p>
</body>
</html>
"""
return html
}
// Generate ETag
private func generateETag(for path: String, size: UInt64, modifiedDate: Date) -> String {
let hash = "\(modifiedDate.timeIntervalSince1970)-\(size)"
return "W/\"\(hash)\""
}
// Normalize path
private func normalizePath(_ path: String) -> String {
var normalized = path
// Remove leading slash
if normalized.hasPrefix("/") {
normalized = String(normalized.dropFirst())
}
// Replace backslashes with forward slashes
normalized = normalized.replacingOccurrences(of: "\\", with: "/")
// Remove query string
if let queryRange = normalized.range(of: "?") {
normalized = String(normalized[..<queryRange.lowerBound])
}
// Remove fragment
if let fragmentRange = normalized.range(of: "#") {
normalized = String(normalized[..<fragmentRange.lowerBound])
}
return normalized.isEmpty ? "/" : "/\(normalized)"
}
// Resolve full path
private func resolvePath(_ normalizedPath: String) -> String {
let relativePath = String(normalizedPath.dropFirst())
return (config.rootDirectory as NSString).appendingPathComponent(relativePath)
}
// Check for path traversal
private func isPathTraversal(_ path: String) -> Bool {
return path.contains("..") || path.contains("./") || path.contains("\\")
}
// Get file icon
private func getFileIcon(for fileName: String) -> String {
let ext = ((fileName as NSString).pathExtension).lowercased()
switch ext {
case "html", "htm": return "🌐"
case "css": return "🎨"
case "js": return "⚡"
case "json": return "📋"
case "png", "jpg", "jpeg", "gif", "svg", "webp": return "🖼️"
case "pdf": return "📕"
case "zip", "tar", "gz": return "📦"
case "woff", "woff2", "ttf": return "🔤"
default: return "📄"
}
}
// Format bytes
private func formatBytes(_ bytes: UInt64) -> String {
let kb = Double(bytes) / 1024
let mb = kb / 1024
let gb = mb / 1024
if gb >= 1 {
return String(format: "%.2f GB", gb)
} else if mb >= 1 {
return String(format: "%.2f MB", mb)
} else if kb >= 1 {
return String(format: "%.2f KB", kb)
} else {
return "\(bytes) B"
}
}
// Format date
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter.string(from: date)
}
}
// MARK: - HTTP Response
struct HTTPResponse {
var statusCode: HTTPStatusCode = .ok
var headers: [String: String] = [:]
var body: Data?
func setText(_ text: String) -> HTTPResponse {
var response = self
response.headers["Content-Type"] = "text/plain"
response.body = text.data(using: .utf8)
return response
}
}
enum HTTPStatusCode: Int {
case ok = 200
case forbidden = 403
case notFound = 404
case internalServerError = 500
}
// MARK: - File Cache
class FileCache {
private var cache: [String: CachedFile] = [:]
private let maxSize: Int
private let maxAge: TimeInterval
init(maxSize: Int = 100, maxAge: TimeInterval = 3600) {
self.maxSize = maxSize
self.maxAge = maxAge
}
func get(_ key: String) -> Data? {
guard let cached = cache[key] else { return nil }
// Check if expired
if Date().timeIntervalSince(cached.timestamp) > maxAge {
cache.removeValue(forKey: key)
return nil
}
return cached.data
}
func set(_ key: String, data: Data) {
// Evict oldest if necessary
if cache.count >= maxSize {
let oldest = cache.min(by: { $0.value.timestamp < $1.value.timestamp })
if let oldestKey = oldest?.key {
cache.removeValue(forKey: oldestKey)
}
}
cache[key] = CachedFile(data: data, timestamp: Date())
}
func clear() {
cache.removeAll()
}
private struct CachedFile {
let data: Data
let timestamp: Date
}
}
// MARK: - Demonstration
func demonstrateStaticFileServing() {
print("=== macOS Swift Static File Serving Examples ===\n")
// Create test directory structure
let testDirectory = "/tmp/static_file_test"
let fileManager = FileManager.default
try? fileManager.removeItem(atPath: testDirectory)
try? fileManager.createDirectory(atPath: testDirectory, withIntermediateDirectories: true)
// Create sample files
let indexPath = "\(testDirectory)/index.html"
let cssPath = "\(testDirectory)/style.css"
let jsPath = "\(testDirectory)/app.js"
let subDir = "\(testDirectory)/assets"
try? fileManager.createDirectory(atPath: subDir, withIntermediateDirectories: true)
try? "<html><body><h1>Test Page</h1></body></html>".write(toFile: indexPath, atomically: true, encoding: .utf8)
try? "body { margin: 0; }".write(toFile: cssPath, atomically: true, encoding: .utf8)
try? "console.log('Hello');".write(toFile: jsPath, atomically: true, encoding: .utf8)
try? "Test".write(toFile: "\(subDir)/test.txt", atomically: true, encoding: .utf8)
// 1. Basic file serving
print("--- 1. Basic File Serving ---")
var config = StaticFileConfig(rootDirectory: testDirectory)
config.enableDirectoryBrowsing = true
let server = StaticFileServer(config: config)
// Test serving HTML file
let response1 = server.serveFile(for: "/index.html")
print("Served /index.html: \(response1.statusCode.rawValue)")
if let contentType = response1.headers["Content-Type"] {
print(" Content-Type: \(contentType)")
}
// Test serving CSS file
let response2 = server.serveFile(for: "/style.css")
print("Served /style.css: \(response2.statusCode.rawValue)")
print(" Content-Type: \(response2.headers["Content-Type"] ?? "none")")
// 2. Default file serving
print("\n--- 2. Default File Serving ---")
let response3 = server.serveFile(for: "/")
print("Served / (default): \(response3.statusCode.rawValue)")
// 3. Directory listing
print("\n--- 3. Directory Listing ---")
let response4 = server.serveFile(for: "/assets/")
print("Served /assets/ (listing): \(response4.statusCode.rawValue)")
// 4. 404 handling
print("\n--- 4. 404 Handling ---")
let response5 = server.serveFile(for: "/nonexistent.html")
print("Served /nonexistent.html: \(response5.statusCode.rawValue)")
// 5. Path traversal protection
print("\n--- 5. Path Traversal Protection ---")
let response6 = server.serveFile(for: "/../../../etc/passwd")
print("Served ../../../etc/passwd: \(response6.statusCode.rawValue)")
// 6. File type restriction
print("\n--- 6. File Type Restriction ---")
try? "test".write(toFile: "\(testDirectory)/test.exe", atomically: true, encoding: .utf8)
let response7 = server.serveFile(for: "/test.exe")
print("Served /test.exe: \(response7.statusCode.rawValue)")
// 7. ETag generation
print("\n--- 7. ETag Generation ---")
config.enableETag = true
let serverWithETag = StaticFileServer(config: config)
let response8 = serverWithETag.serveFile(for: "/index.html")
print("ETag: \(response8.headers["ETag"] ?? "none")")
print("Cache-Control: \(response8.headers["Cache-Control"] ?? "none")")
// Cleanup
try? fileManager.removeItem(atPath: testDirectory)
print("\nCleanup completed")
print("\n=== Static File Serving Demo Completed ===")
}
// Run demonstration
demonstrateStaticFileServing()