@cappitolian/http-local-server 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -20,104 +20,75 @@ import Foundation
|
|
|
20
20
|
import GCDWebServer
|
|
21
21
|
import Capacitor
|
|
22
22
|
|
|
23
|
+
// MARK: - Protocol
|
|
23
24
|
public protocol HttpLocalServerDelegate: AnyObject {
|
|
24
25
|
func httpLocalServerDidReceiveRequest(_ data: [String: Any])
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
// MARK: - HttpLocalServer
|
|
27
29
|
@objc public class HttpLocalServer: NSObject {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// MARK: - Properties
|
|
31
|
+
private var webServer: GCDWebServer?
|
|
32
|
+
private weak var delegate: HttpLocalServerDelegate?
|
|
33
|
+
|
|
34
|
+
private static var pendingResponses = [String: (String) -> Void]()
|
|
35
|
+
private static let queue = DispatchQueue(label: "com.cappitolian.HttpLocalServer.pendingResponses", qos: .userInitiated)
|
|
36
|
+
|
|
37
|
+
private let defaultTimeout: TimeInterval = 5.0
|
|
38
|
+
private let defaultPort: UInt = 8080
|
|
39
|
+
|
|
40
|
+
// MARK: - Initialization
|
|
33
41
|
public init(delegate: HttpLocalServerDelegate) {
|
|
34
42
|
self.delegate = delegate
|
|
43
|
+
super.init()
|
|
35
44
|
}
|
|
36
|
-
|
|
45
|
+
|
|
46
|
+
deinit {
|
|
47
|
+
disconnect()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// MARK: - Public Methods
|
|
37
51
|
@objc public func connect(_ call: CAPPluginCall) {
|
|
38
52
|
DispatchQueue.main.async { [weak self] in
|
|
39
|
-
guard let self = self else {
|
|
40
|
-
|
|
53
|
+
guard let self = self else {
|
|
54
|
+
call.reject("Server instance deallocated")
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Stop existing server if running
|
|
59
|
+
if self.webServer?.isRunning == true {
|
|
60
|
+
self.webServer?.stop()
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
self.webServer = GCDWebServer()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
match: { method, url, headers, path, query in
|
|
45
|
-
GCDWebServerRequest(method: method, url: url, headers: headers, path: path, query: query)
|
|
46
|
-
},
|
|
47
|
-
processBlock: { request in
|
|
48
|
-
let method = request.method
|
|
49
|
-
let path = request.url.path
|
|
50
|
-
var body: String? = nil
|
|
51
|
-
|
|
52
|
-
if let dataRequest = request as? GCDWebServerDataRequest, let text = String(data: dataRequest.data, encoding: .utf8) {
|
|
53
|
-
body = text
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
let requestId = UUID().uuidString
|
|
57
|
-
var responseString: String? = nil
|
|
58
|
-
|
|
59
|
-
// Set up a semaphore so we can block until JS responds or timeout (3s)
|
|
60
|
-
let semaphore = DispatchSemaphore(value: 0)
|
|
61
|
-
Self.queue.async {
|
|
62
|
-
Self.pendingResponses[requestId] = { responseBody in
|
|
63
|
-
responseString = responseBody
|
|
64
|
-
semaphore.signal()
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Notify delegate (plugin) with the request info
|
|
69
|
-
let req: [String: Any?] = [
|
|
70
|
-
"requestId": requestId,
|
|
71
|
-
"method": method,
|
|
72
|
-
"path": path,
|
|
73
|
-
"body": body
|
|
74
|
-
]
|
|
75
|
-
self.delegate?.httpLocalServerDidReceiveRequest(req.compactMapValues { $0 })
|
|
76
|
-
|
|
77
|
-
// Wait for JS response or timeout
|
|
78
|
-
_ = semaphore.wait(timeout: .now() + 3.0)
|
|
79
|
-
Self.queue.async {
|
|
80
|
-
Self.pendingResponses.removeValue(forKey: requestId)
|
|
81
|
-
}
|
|
82
|
-
let reply = responseString ?? "{\"error\":\"Timeout waiting for JS response\"}"
|
|
83
|
-
|
|
84
|
-
let response = GCDWebServerDataResponse(text: reply)
|
|
85
|
-
response?.setValue("*", forAdditionalHeader: "Access-Control-Allow-Origin")
|
|
86
|
-
response?.setValue("GET,POST,OPTIONS", forAdditionalHeader: "Access-Control-Allow-Methods")
|
|
87
|
-
response?.setValue("origin, content-type, accept, authorization", forAdditionalHeader: "Access-Control-Allow-Headers")
|
|
88
|
-
response?.setValue("3600", forAdditionalHeader: "Access-Control-Max-Age")
|
|
89
|
-
response?.contentType = "application/json"
|
|
90
|
-
return response!
|
|
91
|
-
}
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
let port: UInt = 8080
|
|
64
|
+
self.setupHandlers()
|
|
65
|
+
|
|
95
66
|
do {
|
|
96
|
-
try self.
|
|
97
|
-
GCDWebServerOption_Port: port,
|
|
98
|
-
GCDWebServerOption_BonjourName: "",
|
|
99
|
-
GCDWebServerOption_BindToLocalhost: false
|
|
100
|
-
])
|
|
67
|
+
try self.startServer()
|
|
101
68
|
let ip = Self.getWiFiAddress() ?? "127.0.0.1"
|
|
102
69
|
call.resolve([
|
|
103
70
|
"ip": ip,
|
|
104
|
-
"port":
|
|
71
|
+
"port": self.defaultPort
|
|
105
72
|
])
|
|
106
73
|
} catch {
|
|
107
74
|
call.reject("Failed to start server: \(error.localizedDescription)")
|
|
108
75
|
}
|
|
109
76
|
}
|
|
110
77
|
}
|
|
111
|
-
|
|
112
|
-
@objc public func disconnect(_ call: CAPPluginCall) {
|
|
78
|
+
|
|
79
|
+
@objc public func disconnect(_ call: CAPPluginCall? = nil) {
|
|
113
80
|
DispatchQueue.main.async { [weak self] in
|
|
114
|
-
self
|
|
115
|
-
|
|
116
|
-
|
|
81
|
+
guard let self = self else {
|
|
82
|
+
call?.reject("Server instance deallocated")
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
self.disconnect()
|
|
87
|
+
call?.resolve()
|
|
117
88
|
}
|
|
118
89
|
}
|
|
119
|
-
|
|
120
|
-
//
|
|
90
|
+
|
|
91
|
+
// MARK: - Static Methods
|
|
121
92
|
static func handleJsResponse(requestId: String, body: String) {
|
|
122
93
|
queue.async {
|
|
123
94
|
if let callback = pendingResponses[requestId] {
|
|
@@ -126,32 +97,237 @@ public protocol HttpLocalServerDelegate: AnyObject {
|
|
|
126
97
|
}
|
|
127
98
|
}
|
|
128
99
|
}
|
|
129
|
-
|
|
130
|
-
//
|
|
100
|
+
|
|
101
|
+
// MARK: - Private Methods
|
|
102
|
+
private func disconnect() {
|
|
103
|
+
webServer?.stop()
|
|
104
|
+
webServer = nil
|
|
105
|
+
|
|
106
|
+
// Clear pending responses
|
|
107
|
+
Self.queue.async {
|
|
108
|
+
Self.pendingResponses.removeAll()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func setupHandlers() {
|
|
113
|
+
guard let webServer = webServer else { return }
|
|
114
|
+
|
|
115
|
+
// GET requests
|
|
116
|
+
webServer.addDefaultHandler(
|
|
117
|
+
forMethod: "GET",
|
|
118
|
+
request: GCDWebServerRequest.self,
|
|
119
|
+
processBlock: { [weak self] request in
|
|
120
|
+
return self?.processRequest(request) ?? self?.errorResponse() ?? GCDWebServerResponse()
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
// POST requests (with body)
|
|
125
|
+
webServer.addDefaultHandler(
|
|
126
|
+
forMethod: "POST",
|
|
127
|
+
request: GCDWebServerDataRequest.self,
|
|
128
|
+
processBlock: { [weak self] request in
|
|
129
|
+
return self?.processRequest(request) ?? self?.errorResponse() ?? GCDWebServerResponse()
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// PUT requests (with body)
|
|
134
|
+
webServer.addDefaultHandler(
|
|
135
|
+
forMethod: "PUT",
|
|
136
|
+
request: GCDWebServerDataRequest.self,
|
|
137
|
+
processBlock: { [weak self] request in
|
|
138
|
+
return self?.processRequest(request) ?? self?.errorResponse() ?? GCDWebServerResponse()
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// PATCH requests (with body)
|
|
143
|
+
webServer.addDefaultHandler(
|
|
144
|
+
forMethod: "PATCH",
|
|
145
|
+
request: GCDWebServerDataRequest.self,
|
|
146
|
+
processBlock: { [weak self] request in
|
|
147
|
+
return self?.processRequest(request) ?? self?.errorResponse() ?? GCDWebServerResponse()
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// DELETE requests
|
|
152
|
+
webServer.addDefaultHandler(
|
|
153
|
+
forMethod: "DELETE",
|
|
154
|
+
request: GCDWebServerRequest.self,
|
|
155
|
+
processBlock: { [weak self] request in
|
|
156
|
+
return self?.processRequest(request) ?? self?.errorResponse() ?? GCDWebServerResponse()
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// OPTIONS requests (CORS preflight)
|
|
161
|
+
webServer.addDefaultHandler(
|
|
162
|
+
forMethod: "OPTIONS",
|
|
163
|
+
request: GCDWebServerRequest.self,
|
|
164
|
+
processBlock: { [weak self] request in
|
|
165
|
+
return self?.corsResponse() ?? GCDWebServerResponse()
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private func processRequest(_ request: GCDWebServerRequest) -> GCDWebServerResponse {
|
|
171
|
+
let method = request.method
|
|
172
|
+
let path = request.url.path
|
|
173
|
+
let body = extractBody(from: request)
|
|
174
|
+
let headers = request.headers
|
|
175
|
+
let query = request.query
|
|
176
|
+
|
|
177
|
+
let requestId = UUID().uuidString
|
|
178
|
+
var responseString: String?
|
|
179
|
+
|
|
180
|
+
// Setup semaphore for synchronous waiting
|
|
181
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
182
|
+
|
|
183
|
+
Self.queue.async {
|
|
184
|
+
Self.pendingResponses[requestId] = { responseBody in
|
|
185
|
+
responseString = responseBody
|
|
186
|
+
semaphore.signal()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Notify delegate with request info
|
|
191
|
+
var requestData: [String: Any] = [
|
|
192
|
+
"requestId": requestId,
|
|
193
|
+
"method": method,
|
|
194
|
+
"path": path
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
if let body = body {
|
|
198
|
+
requestData["body"] = body
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if let headers = headers as? [String: String], !headers.isEmpty {
|
|
202
|
+
requestData["headers"] = headers
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if let query = query, !query.isEmpty {
|
|
206
|
+
requestData["query"] = query
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
delegate?.httpLocalServerDidReceiveRequest(requestData)
|
|
210
|
+
|
|
211
|
+
// Wait for JS response or timeout
|
|
212
|
+
let result = semaphore.wait(timeout: .now() + defaultTimeout)
|
|
213
|
+
|
|
214
|
+
// Cleanup
|
|
215
|
+
Self.queue.async {
|
|
216
|
+
Self.pendingResponses.removeValue(forKey: requestId)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Handle timeout
|
|
220
|
+
if result == .timedOut {
|
|
221
|
+
let timeoutResponse = "{\"error\":\"Request timeout\",\"requestId\":\"\(requestId)\"}"
|
|
222
|
+
return createJsonResponse(timeoutResponse, statusCode: 408)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let reply = responseString ?? "{\"error\":\"No response from handler\"}"
|
|
226
|
+
return createJsonResponse(reply)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private func extractBody(from request: GCDWebServerRequest) -> String? {
|
|
230
|
+
guard let dataRequest = request as? GCDWebServerDataRequest else {
|
|
231
|
+
return nil
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return String(data: dataRequest.data, encoding: .utf8)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private func createJsonResponse(_ body: String, statusCode: Int = 200) -> GCDWebServerDataResponse {
|
|
238
|
+
let response = GCDWebServerDataResponse(text: body)
|
|
239
|
+
response?.statusCode = statusCode
|
|
240
|
+
response?.contentType = "application/json"
|
|
241
|
+
|
|
242
|
+
// CORS headers
|
|
243
|
+
response?.setValue("*", forAdditionalHeader: "Access-Control-Allow-Origin")
|
|
244
|
+
response?.setValue("GET, POST, PUT, PATCH, DELETE, OPTIONS", forAdditionalHeader: "Access-Control-Allow-Methods")
|
|
245
|
+
response?.setValue("Origin, Content-Type, Accept, Authorization", forAdditionalHeader: "Access-Control-Allow-Headers")
|
|
246
|
+
response?.setValue("true", forAdditionalHeader: "Access-Control-Allow-Credentials")
|
|
247
|
+
response?.setValue("3600", forAdditionalHeader: "Access-Control-Max-Age")
|
|
248
|
+
|
|
249
|
+
return response ?? GCDWebServerDataResponse()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private func corsResponse() -> GCDWebServerDataResponse {
|
|
253
|
+
return createJsonResponse("{}", statusCode: 204)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private func errorResponse() -> GCDWebServerDataResponse {
|
|
257
|
+
return createJsonResponse("{\"error\":\"Server error\"}", statusCode: 500)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private func startServer() throws {
|
|
261
|
+
guard let webServer = webServer else {
|
|
262
|
+
throw NSError(
|
|
263
|
+
domain: "HttpLocalServer",
|
|
264
|
+
code: -1,
|
|
265
|
+
userInfo: [NSLocalizedDescriptionKey: "WebServer not initialized"]
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let options: [String: Any] = [
|
|
270
|
+
GCDWebServerOption_Port: defaultPort,
|
|
271
|
+
GCDWebServerOption_BonjourName: "",
|
|
272
|
+
GCDWebServerOption_BindToLocalhost: false,
|
|
273
|
+
GCDWebServerOption_AutomaticallySuspendInBackground: false
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
try webServer.start(options: options)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// MARK: - Network Utilities
|
|
131
280
|
static func getWiFiAddress() -> String? {
|
|
132
281
|
var address: String?
|
|
133
282
|
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let name = String(cString: interface.ifa_name)
|
|
141
|
-
if name == "en0" { // WiFi interface
|
|
142
|
-
var addr = interface.ifa_addr.pointee
|
|
143
|
-
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
|
144
|
-
getnameinfo(&addr, socklen_t(interface.ifa_addr.pointee.sa_len),
|
|
145
|
-
&hostname, socklen_t(hostname.count),
|
|
146
|
-
nil, socklen_t(0), NI_NUMERICHOST)
|
|
147
|
-
address = String(cString: hostname)
|
|
148
|
-
break
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
ptr = interface.ifa_next
|
|
152
|
-
}
|
|
283
|
+
|
|
284
|
+
guard getifaddrs(&ifaddr) == 0 else {
|
|
285
|
+
return nil
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
defer {
|
|
153
289
|
freeifaddrs(ifaddr)
|
|
154
290
|
}
|
|
291
|
+
|
|
292
|
+
var ptr = ifaddr
|
|
293
|
+
while ptr != nil {
|
|
294
|
+
defer { ptr = ptr?.pointee.ifa_next }
|
|
295
|
+
|
|
296
|
+
guard let interface = ptr?.pointee else { continue }
|
|
297
|
+
|
|
298
|
+
let addrFamily = interface.ifa_addr.pointee.sa_family
|
|
299
|
+
|
|
300
|
+
// Check for IPv4 interface
|
|
301
|
+
guard addrFamily == UInt8(AF_INET) else { continue }
|
|
302
|
+
|
|
303
|
+
let name = String(cString: interface.ifa_name)
|
|
304
|
+
|
|
305
|
+
// WiFi interface (en0) or cellular (pdp_ip0)
|
|
306
|
+
guard name == "en0" || name == "pdp_ip0" else { continue }
|
|
307
|
+
|
|
308
|
+
var addr = interface.ifa_addr.pointee
|
|
309
|
+
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
|
310
|
+
|
|
311
|
+
let result = getnameinfo(
|
|
312
|
+
&addr,
|
|
313
|
+
socklen_t(interface.ifa_addr.pointee.sa_len),
|
|
314
|
+
&hostname,
|
|
315
|
+
socklen_t(hostname.count),
|
|
316
|
+
nil,
|
|
317
|
+
0,
|
|
318
|
+
NI_NUMERICHOST
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
guard result == 0 else { continue }
|
|
322
|
+
|
|
323
|
+
address = String(cString: hostname)
|
|
324
|
+
|
|
325
|
+
// Prefer en0 (WiFi) over pdp_ip0 (cellular)
|
|
326
|
+
if name == "en0" {
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
155
331
|
return address
|
|
156
332
|
}
|
|
157
|
-
}
|
|
333
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cappitolian/http-local-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Runs a local HTTP server on your device, accessible over LAN. Supports connect, disconnect, GET, and POST methods with IP and port discovery.",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|