@cappitolian/http-local-server-swifter 0.0.27 → 0.0.28
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.
|
@@ -9,161 +9,201 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
|
|
|
9
9
|
@objc public class HttpLocalServerSwifter: NSObject {
|
|
10
10
|
private var webServer: HttpServer?
|
|
11
11
|
private weak var delegate: HttpLocalServerSwifterDelegate?
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
private static var pendingResponses = [String: (String) -> Void]()
|
|
14
|
-
private static let queue = DispatchQueue(
|
|
15
|
-
|
|
14
|
+
private static let queue = DispatchQueue(
|
|
15
|
+
label: "com.cappitolian.HttpLocalServerSwifter.pendingResponses",
|
|
16
|
+
qos: .userInitiated
|
|
17
|
+
)
|
|
18
|
+
|
|
16
19
|
private let defaultTimeout: TimeInterval = 10.0
|
|
17
20
|
private let defaultPort: UInt16 = 8080
|
|
18
|
-
|
|
21
|
+
|
|
19
22
|
public init(delegate: HttpLocalServerSwifterDelegate) {
|
|
20
23
|
self.delegate = delegate
|
|
21
24
|
super.init()
|
|
22
25
|
}
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
// MARK: - Public API
|
|
28
|
+
|
|
24
29
|
@objc public func connect(_ call: CAPPluginCall) {
|
|
25
|
-
// IMPORTANT: Move execution to a background thread immediately
|
|
26
30
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
27
|
-
guard let self
|
|
28
|
-
|
|
29
|
-
self.
|
|
31
|
+
guard let self else { return }
|
|
32
|
+
|
|
33
|
+
self.stopServer()
|
|
34
|
+
|
|
30
35
|
let server = HttpServer()
|
|
31
36
|
self.webServer = server
|
|
32
|
-
|
|
33
|
-
// Use middleware to catch ALL requests and avoid route misses
|
|
37
|
+
|
|
34
38
|
server.middleware.append { [weak self] request in
|
|
39
|
+
guard let self else { return .raw(503, "Service Unavailable", nil, nil) }
|
|
40
|
+
|
|
35
41
|
if request.method == "OPTIONS" {
|
|
36
|
-
return self
|
|
42
|
+
return self.corsResponse()
|
|
37
43
|
}
|
|
38
|
-
return self
|
|
44
|
+
return self.processRequest(request)
|
|
39
45
|
}
|
|
40
|
-
|
|
46
|
+
|
|
41
47
|
do {
|
|
42
|
-
// We start the server. This call is non-blocking in Swifter but
|
|
43
|
-
// it's safer to do it here.
|
|
44
48
|
try server.start(self.defaultPort, forceIPv4: true)
|
|
45
49
|
let ip = Self.getWiFiAddress() ?? "127.0.0.1"
|
|
46
|
-
|
|
50
|
+
|
|
47
51
|
print("🚀 SWIFTER: Server running on http://\(ip):\(self.defaultPort)")
|
|
48
|
-
|
|
49
|
-
// Resolve back to Angular
|
|
52
|
+
|
|
50
53
|
call.resolve([
|
|
51
54
|
"ip": ip,
|
|
52
55
|
"port": Int(self.defaultPort)
|
|
53
56
|
])
|
|
54
57
|
} catch {
|
|
55
58
|
print("❌ SWIFTER ERROR: \(error)")
|
|
56
|
-
call.reject("Could not start server")
|
|
59
|
+
call.reject("Could not start server: \(error.localizedDescription)")
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
|
|
64
|
+
/// Stops the server and clears state. Does NOT resolve/reject any call.
|
|
65
|
+
@objc public func stopServer() {
|
|
62
66
|
webServer?.stop()
|
|
63
67
|
webServer = nil
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
// Drain pending futures so blocked threads can unblock on their semaphore timeout.
|
|
69
|
+
Self.queue.sync { Self.pendingResponses.removeAll() }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Stops the server and resolves the Capacitor call.
|
|
73
|
+
@objc public func disconnect(resolving call: CAPPluginCall) {
|
|
74
|
+
stopServer()
|
|
75
|
+
call.resolve()
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
// MARK: - JS → Native response bridge
|
|
79
|
+
|
|
80
|
+
static func handleJsResponse(requestId: String, responseData: [String: Any]) {
|
|
81
|
+
// sync: guarantees the callback executes (and signals the semaphore) before
|
|
82
|
+
// this function returns, which eliminates the race condition where the
|
|
83
|
+
// semaphore wait could time out while the callback is still queued.
|
|
84
|
+
queue.sync {
|
|
85
|
+
guard let callback = pendingResponses.removeValue(forKey: requestId) else { return }
|
|
86
|
+
|
|
87
|
+
guard
|
|
88
|
+
let jsonData = try? JSONSerialization.data(withJSONObject: responseData),
|
|
89
|
+
let jsonString = String(data: jsonData, encoding: .utf8)
|
|
90
|
+
else { return }
|
|
91
|
+
|
|
92
|
+
callback(jsonString)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// MARK: - Request processing
|
|
97
|
+
|
|
68
98
|
private func processRequest(_ request: HttpRequest) -> HttpResponse {
|
|
69
99
|
let requestId = UUID().uuidString
|
|
100
|
+
|
|
101
|
+
// Use a protected local variable written only inside `queue.sync` inside
|
|
102
|
+
// the callback, and read only after the semaphore is signalled — the
|
|
103
|
+
// happens-before guarantee of DispatchSemaphore makes this safe.
|
|
70
104
|
var responseString: String?
|
|
71
105
|
let semaphore = DispatchSemaphore(value: 0)
|
|
72
|
-
|
|
73
|
-
Self.queue.
|
|
106
|
+
|
|
107
|
+
Self.queue.sync {
|
|
74
108
|
Self.pendingResponses[requestId] = { jsResponse in
|
|
75
109
|
responseString = jsResponse
|
|
76
110
|
semaphore.signal()
|
|
77
111
|
}
|
|
78
112
|
}
|
|
79
|
-
|
|
113
|
+
|
|
80
114
|
let requestData: [String: Any] = [
|
|
81
115
|
"requestId": requestId,
|
|
82
|
-
"method":
|
|
83
|
-
"path":
|
|
84
|
-
"headers":
|
|
85
|
-
"query":
|
|
86
|
-
"body":
|
|
116
|
+
"method": request.method,
|
|
117
|
+
"path": request.path,
|
|
118
|
+
"headers": request.headers,
|
|
119
|
+
"query": request.queryParams,
|
|
120
|
+
"body": String(bytes: request.body, encoding: .utf8) ?? ""
|
|
87
121
|
]
|
|
88
|
-
|
|
89
|
-
//
|
|
122
|
+
|
|
123
|
+
// notifyListeners MUST be called from the main thread.
|
|
90
124
|
DispatchQueue.main.async {
|
|
91
125
|
self.delegate?.httpLocalServerSwifterDidReceiveRequest(requestData)
|
|
92
126
|
}
|
|
93
|
-
|
|
127
|
+
|
|
94
128
|
let result = semaphore.wait(timeout: .now() + defaultTimeout)
|
|
95
|
-
|
|
129
|
+
|
|
96
130
|
if result == .timedOut {
|
|
97
|
-
|
|
131
|
+
// Remove callback in case handleJsResponse fires late.
|
|
132
|
+
Self.queue.sync { Self.pendingResponses.removeValue(forKey: requestId) }
|
|
98
133
|
return .raw(408, "Request Timeout", nil, nil)
|
|
99
134
|
}
|
|
100
|
-
|
|
135
|
+
|
|
101
136
|
return createDynamicResponse(responseString ?? "")
|
|
102
137
|
}
|
|
103
138
|
|
|
104
|
-
|
|
105
|
-
queue.async {
|
|
106
|
-
if let callback = pendingResponses[requestId] {
|
|
107
|
-
// Extract body, status and headers to pass as a JSON string to the semaphore
|
|
108
|
-
if let jsonData = try? JSONSerialization.data(withJSONObject: responseData),
|
|
109
|
-
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
110
|
-
callback(jsonString)
|
|
111
|
-
}
|
|
112
|
-
pendingResponses.removeValue(forKey: requestId)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
139
|
+
// MARK: - Response builders
|
|
116
140
|
|
|
117
141
|
private func createDynamicResponse(_ jsonResponse: String) -> HttpResponse {
|
|
118
142
|
var finalStatus = 200
|
|
119
|
-
var finalBody
|
|
143
|
+
var finalBody = jsonResponse
|
|
120
144
|
var headers: [String: String] = [
|
|
121
|
-
"Content-Type":
|
|
122
|
-
"Access-Control-Allow-Origin":
|
|
123
|
-
"Access-Control-Allow-Methods":
|
|
124
|
-
"Access-Control-Allow-Headers":
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
"Access-Control-Allow-Origin": "*",
|
|
147
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
148
|
+
"Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization, X-Requested-With"
|
|
125
149
|
]
|
|
126
|
-
|
|
127
|
-
if
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
|
|
151
|
+
if
|
|
152
|
+
let data = jsonResponse.data(using: .utf8),
|
|
153
|
+
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
154
|
+
{
|
|
155
|
+
finalBody = dict["body"] as? String ?? ""
|
|
156
|
+
finalStatus = dict["status"] as? Int ?? 200
|
|
157
|
+
|
|
131
158
|
if let customHeaders = dict["headers"] as? [String: String] {
|
|
132
159
|
for (key, value) in customHeaders { headers[key] = value }
|
|
133
160
|
}
|
|
134
161
|
}
|
|
135
|
-
|
|
136
|
-
return .raw(finalStatus, "OK", headers) {
|
|
162
|
+
|
|
163
|
+
return .raw(finalStatus, "OK", headers) { writer in
|
|
164
|
+
try writer.write([UInt8](finalBody.utf8))
|
|
165
|
+
}
|
|
137
166
|
}
|
|
138
167
|
|
|
139
168
|
private func corsResponse() -> HttpResponse {
|
|
140
169
|
return .raw(204, "No Content", [
|
|
141
|
-
"Access-Control-Allow-Origin":
|
|
170
|
+
"Access-Control-Allow-Origin": "*",
|
|
142
171
|
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
143
172
|
"Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization, X-Requested-With",
|
|
144
|
-
"Access-Control-Max-Age":
|
|
173
|
+
"Access-Control-Max-Age": "86400"
|
|
145
174
|
], nil)
|
|
146
175
|
}
|
|
147
176
|
|
|
177
|
+
// MARK: - Network helpers
|
|
178
|
+
|
|
148
179
|
static func getWiFiAddress() -> String? {
|
|
149
180
|
var address: String?
|
|
150
181
|
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
182
|
+
|
|
183
|
+
guard getifaddrs(&ifaddr) == 0 else { return nil }
|
|
184
|
+
defer { freeifaddrs(ifaddr) }
|
|
185
|
+
|
|
186
|
+
var ptr = ifaddr
|
|
187
|
+
while let current = ptr {
|
|
188
|
+
let interface = current.pointee
|
|
189
|
+
if interface.ifa_addr.pointee.sa_family == UInt8(AF_INET),
|
|
190
|
+
String(cString: interface.ifa_name) == "en0"
|
|
191
|
+
{
|
|
192
|
+
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
|
193
|
+
getnameinfo(
|
|
194
|
+
interface.ifa_addr,
|
|
195
|
+
socklen_t(interface.ifa_addr.pointee.sa_len),
|
|
196
|
+
&hostname,
|
|
197
|
+
socklen_t(hostname.count),
|
|
198
|
+
nil, 0,
|
|
199
|
+
NI_NUMERICHOST
|
|
200
|
+
)
|
|
201
|
+
address = String(cString: hostname)
|
|
202
|
+
break
|
|
164
203
|
}
|
|
165
|
-
|
|
204
|
+
ptr = interface.ifa_next
|
|
166
205
|
}
|
|
206
|
+
|
|
167
207
|
return address
|
|
168
208
|
}
|
|
169
209
|
}
|
|
@@ -3,42 +3,49 @@ import Capacitor
|
|
|
3
3
|
|
|
4
4
|
@objc(HttpLocalServerSwifterPlugin)
|
|
5
5
|
public class HttpLocalServerSwifterPlugin: CAPPlugin, CAPBridgedPlugin, HttpLocalServerSwifterDelegate {
|
|
6
|
-
public let identifier
|
|
7
|
-
public let jsName
|
|
6
|
+
public let identifier = "HttpLocalServerSwifterPlugin"
|
|
7
|
+
public let jsName = "HttpLocalServerSwifter"
|
|
8
8
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
9
|
-
CAPPluginMethod(name: "connect",
|
|
10
|
-
CAPPluginMethod(name: "disconnect",
|
|
9
|
+
CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise),
|
|
10
|
+
CAPPluginMethod(name: "disconnect", returnType: CAPPluginReturnPromise),
|
|
11
11
|
CAPPluginMethod(name: "sendResponse", returnType: CAPPluginReturnPromise)
|
|
12
12
|
]
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
private var localServer: HttpLocalServerSwifter?
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// MARK: - Plugin methods
|
|
17
|
+
|
|
16
18
|
@objc func connect(_ call: CAPPluginCall) {
|
|
17
|
-
if localServer == nil {
|
|
19
|
+
if localServer == nil {
|
|
20
|
+
localServer = HttpLocalServerSwifter(delegate: self)
|
|
21
|
+
}
|
|
18
22
|
localServer?.connect(call)
|
|
19
23
|
}
|
|
20
|
-
|
|
24
|
+
|
|
21
25
|
@objc func disconnect(_ call: CAPPluginCall) {
|
|
22
|
-
|
|
26
|
+
// Use the dedicated method that accepts a call to resolve so the
|
|
27
|
+
// ambiguous no-argument overload is gone entirely.
|
|
28
|
+
localServer?.disconnect(resolving: call)
|
|
23
29
|
localServer = nil
|
|
24
30
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
|
|
32
|
+
@objc func sendResponse(_ call: CAPPluginCall) {
|
|
33
|
+
guard let requestId = call.getString("requestId"), !requestId.isEmpty else {
|
|
34
|
+
call.reject("Missing requestId")
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// dictionaryRepresentation already returns [String: Any] — no cast needed.
|
|
39
|
+
HttpLocalServerSwifter.handleJsResponse(
|
|
40
|
+
requestId: requestId,
|
|
41
|
+
responseData: call.dictionaryRepresentation
|
|
42
|
+
)
|
|
43
|
+
call.resolve()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// MARK: - Delegate
|
|
47
|
+
|
|
41
48
|
public func httpLocalServerSwifterDidReceiveRequest(_ data: [String: Any]) {
|
|
42
49
|
notifyListeners("onRequest", data: data)
|
|
43
50
|
}
|
|
44
|
-
}
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cappitolian/http-local-server-swifter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
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",
|