-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathToyHttpServer.swift
More file actions
192 lines (164 loc) · 7.15 KB
/
Copy pathToyHttpServer.swift
File metadata and controls
192 lines (164 loc) · 7.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import Foundation
#if canImport(Darwin)
import Darwin
#elseif canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#endif
/// A basic HTTP server with keep-alive support and Server-Sent Events for local development and testing purposes only.
///
/// Features:
/// - HTTP keep-alive connections (default behavior)
/// - 30-second connection timeout
/// - Maximum 100 requests per connection
/// - Basic GET request support for static files
/// - Server-Sent Events for live reload functionality
/// - Connection closes only on timeout, error, or explicit "Connection: close" header
///
/// WARNING: This is NOT a production-ready HTTP server and should never be used
/// in production environments. It lacks many essential features including:
/// - Security measures and input validation
/// - Comprehensive error handling and recovery
/// - Full HTTP/1.1 compliance beyond basic GET requests
/// - Support for POST/PUT requests, cookies, authentication
/// - Advanced connection pooling and resource management
/// - Performance optimizations for high load
///
/// This server is intended solely for serving static files during local development
/// of the Tuzuru static blog generator.
public class ToyHttpServer: @unchecked Sendable {
private let port: Int
private let servePath: String
private let beforeResponseHook: RequestHook?
private let afterResponseHook: ResponseHook?
public init(
port: Int,
servePath: String,
beforeResponseHook: RequestHook? = nil,
afterResponseHook: ResponseHook? = nil
) {
self.port = port
self.servePath = servePath
self.beforeResponseHook = beforeResponseHook
self.afterResponseHook = afterResponseHook
}
public func start() async throws {
let serverSocket = try Socket.createServerSocket(port: port)
print("⚠️ This is a basic HTTP server that might have issues. Report me any issues at: https://github.com/ainame/Tuzuru/issues")
print("")
print("🚀 Starting server on http://localhost:\(port)")
print("📂 Serving directory: \(servePath)")
print("🛑 Press Ctrl+C to stop")
signal(SIGINT) { _ in exit(0) }
await withTaskGroup(of: Void.self) { group in
while true {
guard let clientSocket = Socket.accept(serverSocket) else { continue }
group.addTask { @Sendable [
servePath = self.servePath,
beforeHook = self.beforeResponseHook,
afterHook = self.afterResponseHook,
] in
await ToyHttpServer.handleClientInstance(
clientSocket,
servePath: servePath,
beforeHook: beforeHook,
afterHook: afterHook,
)
}
}
}
}
private static func handleClientInstance(
_ clientSocket: Socket,
servePath: String,
beforeHook: RequestHook?,
afterHook: ResponseHook?,
) async {
defer { clientSocket.close() }
var requestCount = 0
let maxRequests = 100
// Keep-alive connection loop
while requestCount < maxRequests {
requestCount += 1
// Read request with timeout
guard let httpRequest = await HttpParser.readHttpRequestWithTimeout(clientSocket, timeout: 30) else {
// Timeout or connection closed by client
break
}
let requestContext = HttpRequestContext(
method: httpRequest.method,
path: httpRequest.path,
fullPath: httpRequest.fullPath,
timestamp: Date()
)
do {
try await beforeHook?(requestContext)
} catch {
print("Error in beforeResponseHook: \(error)")
}
let response: HttpResponse
if httpRequest.method != "GET" {
response = HttpResponse(statusCode: 405, contentType: "text/plain",
data: "405 Method Not Allowed".data(using: .utf8) ?? Data())
} else {
response = serveFile(path: httpRequest.path, servePath: servePath)
}
// Send response
clientSocket.send(response.generateResponseString())
clientSocket.send(response.data)
logRequestStatic(httpRequest.method, httpRequest.fullPath, response.statusCode)
await afterHook?(requestContext, response.statusCode)
// Check if client wants to close connection
if httpRequest.shouldClose {
break
}
}
}
private static func logRequestStatic(_ method: String, _ path: String, _ statusCode: Int) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let timestamp = formatter.string(from: Date())
print("\(timestamp) \(method) \(path) \(statusCode)")
}
private static func serveFile(path: String, servePath: String) -> HttpResponse {
var filePath = path == "/" ?
servePath + "/index.html" :
path.hasSuffix("/") ? servePath + path + "index.html" :
servePath + path
// If not a regular file, try directory index serving
var isDirectory: ObjCBool = false
if !FileManager.default.fileExists(atPath: filePath, isDirectory: &isDirectory) || isDirectory.boolValue {
let indexPath = filePath + "/index.html"
if FileManager.default.fileExists(atPath: indexPath) {
filePath = indexPath
}
}
guard filePath.hasPrefix(servePath),
FileManager.default.fileExists(atPath: filePath),
let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
return HttpResponse(statusCode: 404, contentType: "text/plain",
data: "404 Not Found".data(using: .utf8) ?? Data())
}
let contentType = determineContentType(for: filePath)
return HttpResponse(statusCode: 200, contentType: contentType, data: data)
}
private static func determineContentType(for filePath: String) -> String {
return filePath.hasSuffix(".html") ? "text/html; charset=utf-8" :
filePath.hasSuffix(".css") ? "text/css" :
filePath.hasSuffix(".js") ? "application/javascript" :
filePath.hasSuffix(".json") ? "application/json" :
filePath.hasSuffix(".png") ? "image/png" :
filePath.hasSuffix(".jpg") || filePath.hasSuffix(".jpeg") ? "image/jpeg" :
filePath.hasSuffix(".gif") ? "image/gif" :
filePath.hasSuffix(".webp") ? "image/webp" :
filePath.hasSuffix(".svg") ? "image/svg+xml" :
filePath.hasSuffix(".ico") ? "image/x-icon" :
filePath.hasSuffix(".txt") ? "text/plain" :
filePath.hasSuffix(".xml") ? "application/xml" :
"application/octet-stream"
}
}
public enum TinyHttpServerError: Error {
case socketCreationFailed, bindFailed, listenFailed
}