@cappitolian/http-local-server-swifter 0.0.13 → 0.0.15

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.
@@ -2,338 +2,168 @@ import Foundation
2
2
  import Swifter
3
3
  import Capacitor
4
4
 
5
- // MARK: - Protocol
6
5
  public protocol HttpLocalServerSwifterDelegate: AnyObject {
7
6
  func httpLocalServerSwifterDidReceiveRequest(_ data: [String: Any])
8
7
  }
9
8
 
10
- // MARK: - HttpLocalServerSwifter
11
9
  @objc public class HttpLocalServerSwifter: NSObject {
12
- // MARK: - Properties
13
10
  private var webServer: HttpServer?
14
11
  private weak var delegate: HttpLocalServerSwifterDelegate?
15
12
 
13
+ // Store callbacks that expect a JSON String containing the full response object
16
14
  private static var pendingResponses = [String: (String) -> Void]()
17
15
  private static let queue = DispatchQueue(label: "com.cappitolian.HttpLocalServerSwifter.pendingResponses", qos: .userInitiated)
18
16
 
19
- private let defaultTimeout: TimeInterval = 30.0 // Aumentado para debugging
17
+ private let defaultTimeout: TimeInterval = 10.0
20
18
  private let defaultPort: UInt16 = 8080
21
19
 
22
- // MARK: - Initialization
23
20
  public init(delegate: HttpLocalServerSwifterDelegate) {
24
21
  self.delegate = delegate
25
22
  super.init()
26
23
  }
27
24
 
28
- deinit {
29
- disconnect()
30
- }
31
-
32
- // MARK: - Public Methods
33
25
  @objc public func connect(_ call: CAPPluginCall) {
34
- // Stop existing server if running
35
26
  self.disconnect()
36
-
37
27
  self.webServer = HttpServer()
38
28
  self.setupHandlers()
39
-
40
29
  do {
41
- try self.startServer()
30
+ try self.webServer?.start(defaultPort, forceIPv4: true)
42
31
  let ip = Self.getWiFiAddress() ?? "127.0.0.1"
43
-
44
- print("✅ HttpLocalServerSwifter: Server started on \(ip):\(self.defaultPort)")
45
-
46
- call.resolve([
47
- "ip": ip,
48
- "port": self.defaultPort
49
- ])
32
+ call.resolve(["ip": ip, "port": self.defaultPort])
50
33
  } catch {
51
- print("❌ HttpLocalServerSwifter: Failed to start server - \(error.localizedDescription)")
52
34
  call.reject("Failed to start server: \(error.localizedDescription)")
53
35
  }
54
36
  }
55
37
 
56
38
  @objc public func disconnect(_ call: CAPPluginCall? = nil) {
57
- disconnect()
39
+ webServer?.stop()
40
+ webServer = nil
41
+ Self.queue.async { Self.pendingResponses.removeAll() }
58
42
  call?.resolve()
59
43
  }
60
44
 
61
- // MARK: - Static Methods
62
- static func handleJsResponse(requestId: String, body: String) {
45
+ /**
46
+ * Serializes the JS response dictionary to JSON to pass it back to the processing thread
47
+ */
48
+ static func handleJsResponse(requestId: String, responseData: [String: Any]) {
63
49
  queue.async {
64
50
  if let callback = pendingResponses[requestId] {
65
- callback(body)
51
+ if let jsonData = try? JSONSerialization.data(withJSONObject: responseData),
52
+ let jsonString = String(data: jsonData, encoding: .utf8) {
53
+ callback(jsonString)
54
+ }
66
55
  pendingResponses.removeValue(forKey: requestId)
67
- print("✅ HttpLocalServerSwifter: Response sent for requestId: \(requestId)")
68
- } else {
69
- print("⚠️ HttpLocalServerSwifter: No pending callback for requestId: \(requestId)")
70
56
  }
71
57
  }
72
58
  }
73
59
 
74
- // MARK: - Private Methods
75
- private func disconnect() {
76
- webServer?.stop()
77
- webServer = nil
78
-
79
- // Clear pending responses
80
- Self.queue.async {
81
- Self.pendingResponses.removeAll()
82
- }
83
-
84
- print("🛑 HttpLocalServerSwifter: Server stopped")
85
- }
86
-
87
60
  private func setupHandlers() {
88
61
  guard let server = webServer else { return }
89
-
90
- server["/"] = { [weak self] request in
91
- return self?.handleRequest(request) ?? self?.errorResponse() ?? .internalServerError
92
- }
93
-
94
- server["/:path"] = { [weak self] request in
95
- return self?.handleRequest(request) ?? self?.errorResponse() ?? .internalServerError
96
- }
97
-
98
- server["/:first/:second"] = { [weak self] request in
99
- return self?.handleRequest(request) ?? self?.errorResponse() ?? .internalServerError
100
- }
101
-
102
- server["/:first/:second/:third"] = { [weak self] request in
103
- return self?.handleRequest(request) ?? self?.errorResponse() ?? .internalServerError
62
+
63
+ // This closure handles EVERY request regardless of the path
64
+ let handler: ((HttpRequest) -> HttpResponse) = { [weak self] request in
65
+ if request.method == "OPTIONS" {
66
+ return self?.corsResponse() ?? .raw(204, "No Content", nil, nil)
67
+ }
68
+ return self?.processRequest(request) ?? .raw(500, "Internal Server Error", nil, nil)
104
69
  }
105
70
 
106
- print("✅ HttpLocalServerSwifter: Handlers configured")
107
- }
108
-
109
- private func handleRequest(_ request: HttpRequest) -> HttpResponse {
110
- print("📨 HttpLocalServerSwifter: Received \(request.method) request to \(request.path)")
111
-
112
- // Handle OPTIONS (CORS preflight)
113
- if request.method == "OPTIONS" {
114
- print("🔄 HttpLocalServerSwifter: Handling CORS preflight")
115
- return corsResponse()
71
+ // Swifter needs a wildcard to catch everything including subpaths like /orders/123
72
+ server.middleware.append { request in
73
+ return handler(request)
116
74
  }
117
-
118
- return processRequest(request)
119
75
  }
120
76
 
121
77
  private func processRequest(_ request: HttpRequest) -> HttpResponse {
122
- let method = request.method
123
- let path = request.path
124
- let body = extractBody(from: request)
125
- let headers = extractHeaders(from: request)
126
- let query = extractQuery(from: request)
127
-
128
78
  let requestId = UUID().uuidString
129
79
  var responseString: String?
130
-
131
- // Setup semaphore for synchronous waiting
132
80
  let semaphore = DispatchSemaphore(value: 0)
133
81
 
134
82
  Self.queue.async {
135
- Self.pendingResponses[requestId] = { responseBody in
136
- responseString = responseBody
83
+ Self.pendingResponses[requestId] = { jsResponse in
84
+ responseString = jsResponse
137
85
  semaphore.signal()
138
86
  }
139
87
  }
140
88
 
141
- // Notify delegate with request info
142
- var requestData: [String: Any] = [
89
+ // Map request to dictionary for TS
90
+ let requestData: [String: Any] = [
143
91
  "requestId": requestId,
144
- "method": method,
145
- "path": path
92
+ "method": request.method,
93
+ "path": request.path,
94
+ "headers": request.headers,
95
+ "query": request.queryParams,
96
+ "body": String(bytes: request.body, encoding: .utf8) ?? ""
146
97
  ]
147
98
 
148
- // Only add if they exist (consistent with TypeScript and Android)
149
- if let body = body, !body.isEmpty {
150
- requestData["body"] = body
151
- }
152
-
153
- if !headers.isEmpty {
154
- requestData["headers"] = headers
155
- }
156
-
157
- if !query.isEmpty {
158
- requestData["query"] = query
159
- }
160
-
161
- print("📤 HttpLocalServerSwifter: Notifying delegate with requestId: \(requestId)")
99
+ DispatchQueue.main.async { self.delegate?.httpLocalServerSwifterDidReceiveRequest(requestData) }
162
100
 
163
- // Notify on main thread to ensure proper event delivery
164
- DispatchQueue.main.async { [weak self] in
165
- self?.delegate?.httpLocalServerSwifterDidReceiveRequest(requestData)
166
- }
167
-
168
- // Wait for JS response or timeout
169
101
  let result = semaphore.wait(timeout: .now() + defaultTimeout)
170
102
 
171
- // Cleanup
172
- Self.queue.async {
173
- Self.pendingResponses.removeValue(forKey: requestId)
174
- }
175
-
176
- // Handle timeout
177
103
  if result == .timedOut {
178
- print("⏱️ HttpLocalServerSwifter: Request timeout for requestId: \(requestId)")
179
- let timeoutResponse = "{\"error\":\"Request timeout\",\"requestId\":\"\(requestId)\"}"
180
- return createJsonResponse(timeoutResponse, statusCode: 408)
104
+ return createDynamicResponse("{\"error\":\"timeout\"}", statusCode: 408)
181
105
  }
182
106
 
183
- let reply = responseString ?? "{\"error\":\"No response from handler\"}"
184
- print("✅ HttpLocalServerSwifter: Sending response for requestId: \(requestId)")
185
- return createJsonResponse(reply)
107
+ return createDynamicResponse(responseString ?? "{\"error\":\"no_response\"}")
186
108
  }
187
109
 
188
- private func extractBody(from request: HttpRequest) -> String? {
189
- let bodyBytes = request.body
190
-
191
- guard !bodyBytes.isEmpty else {
192
- return nil
193
- }
194
-
195
- return String(bytes: bodyBytes, encoding: .utf8)
196
- }
197
-
198
- private func extractHeaders(from request: HttpRequest) -> [String: String] {
199
- var headersDict: [String: String] = [:]
200
- for (key, value) in request.headers {
201
- headersDict[key] = value
202
- }
203
- return headersDict
204
- }
205
-
206
- private func extractQuery(from request: HttpRequest) -> [String: String] {
207
- var queryDict: [String: String] = [:]
208
- for (key, value) in request.queryParams {
209
- queryDict[key] = value
210
- }
211
- return queryDict
212
- }
213
-
214
- private func createJsonResponse(_ body: String, statusCode: Int = 200) -> HttpResponse {
215
- let headers: [String: String] = [
110
+ /**
111
+ * Extracts status, headers, and body from the JSON string provided by TypeScript
112
+ */
113
+ private func createDynamicResponse(_ jsonResponse: String, statusCode defaultStatus: Int = 200) -> HttpResponse {
114
+ var finalStatus = defaultStatus
115
+ var finalBody = jsonResponse
116
+ var headers: [String: String] = [
216
117
  "Content-Type": "application/json",
217
118
  "Access-Control-Allow-Origin": "*",
218
119
  "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
219
- "Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization",
220
- "Access-Control-Allow-Credentials": "true",
120
+ "Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization, X-Requested-With",
221
121
  "Access-Control-Max-Age": "3600"
222
122
  ]
223
123
 
224
- let bodyData = [UInt8](body.utf8)
225
-
226
- return HttpResponse.raw(statusCode, statusDescription(for: statusCode), headers) { writer in
227
- try writer.write(bodyData)
124
+ if let data = jsonResponse.data(using: .utf8),
125
+ let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
126
+ finalBody = dict["body"] as? String ?? ""
127
+ finalStatus = dict["status"] as? Int ?? 200
128
+ if let customHeaders = dict["headers"] as? [String: String] {
129
+ for (key, value) in customHeaders { headers[key] = value }
130
+ }
228
131
  }
132
+
133
+ let bodyData = [UInt8](finalBody.utf8)
134
+ return HttpResponse.raw(finalStatus, "OK", headers) { try $0.write(bodyData) }
229
135
  }
230
136
 
231
137
  private func corsResponse() -> HttpResponse {
232
- let headers: [String: String] = [
138
+ return HttpResponse.raw(204, "No Content", [
233
139
  "Access-Control-Allow-Origin": "*",
234
140
  "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
235
- "Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization",
236
- "Access-Control-Allow-Credentials": "true",
141
+ "Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization, X-Requested-With",
237
142
  "Access-Control-Max-Age": "3600"
238
- ]
239
-
240
- return HttpResponse.raw(204, "No Content", headers, nil)
241
- }
242
-
243
- private func errorResponse() -> HttpResponse {
244
- return createJsonResponse("{\"error\":\"Server error\"}", statusCode: 500)
143
+ ], nil)
245
144
  }
246
-
247
- private func statusDescription(for code: Int) -> String {
248
- switch code {
249
- case 200: return "OK"
250
- case 201: return "Created"
251
- case 202: return "Accepted"
252
- case 204: return "No Content"
253
- case 400: return "Bad Request"
254
- case 401: return "Unauthorized"
255
- case 403: return "Forbidden"
256
- case 404: return "Not Found"
257
- case 408: return "Request Timeout"
258
- case 500: return "Internal Server Error"
259
- default: return "Unknown"
260
- }
261
- }
262
-
263
- private func startServer() throws {
264
- guard let server = webServer else {
265
- throw NSError(
266
- domain: "HttpLocalServerSwifter",
267
- code: -1,
268
- userInfo: [NSLocalizedDescriptionKey: "WebServer not initialized"]
269
- )
270
- }
271
-
272
- // Try to start on the default port
273
- do {
274
- try server.start(defaultPort, forceIPv4: true)
275
- print("✅ HttpLocalServerSwifter: Server listening on port \(defaultPort)")
276
- } catch {
277
- print("❌ HttpLocalServerSwifter: Failed to bind to port \(defaultPort): \(error)")
278
- throw error
279
- }
280
- }
281
-
282
- // MARK: - Network Utilities
145
+
146
+ // WiFi address logic remains the same...
283
147
  static func getWiFiAddress() -> String? {
284
148
  var address: String?
285
149
  var ifaddr: UnsafeMutablePointer<ifaddrs>?
286
-
287
- guard getifaddrs(&ifaddr) == 0 else {
288
- print("❌ HttpLocalServerSwifter: Failed to get network interfaces")
289
- return nil
290
- }
291
-
292
- defer {
293
- freeifaddrs(ifaddr)
294
- }
295
-
296
- var ptr = ifaddr
297
- while ptr != nil {
298
- defer { ptr = ptr?.pointee.ifa_next }
299
-
300
- guard let interface = ptr?.pointee else { continue }
301
-
302
- let addrFamily = interface.ifa_addr.pointee.sa_family
303
-
304
- // Check for IPv4 interface
305
- guard addrFamily == UInt8(AF_INET) else { continue }
306
-
307
- let name = String(cString: interface.ifa_name)
308
-
309
- // WiFi interface (en0) or cellular (pdp_ip0)
310
- guard name == "en0" || name == "pdp_ip0" else { continue }
311
-
312
- var addr = interface.ifa_addr.pointee
313
- var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
314
-
315
- let result = getnameinfo(
316
- &addr,
317
- socklen_t(interface.ifa_addr.pointee.sa_len),
318
- &hostname,
319
- socklen_t(hostname.count),
320
- nil,
321
- 0,
322
- NI_NUMERICHOST
323
- )
324
-
325
- guard result == 0 else { continue }
326
-
327
- address = String(cString: hostname)
328
-
329
- print("📡 HttpLocalServerSwifter: Found \(name) interface with IP: \(address ?? "unknown")")
330
-
331
- // Prefer en0 (WiFi) over pdp_ip0 (cellular)
332
- if name == "en0" {
333
- break
150
+ if getifaddrs(&ifaddr) == 0 {
151
+ var ptr = ifaddr
152
+ while ptr != nil {
153
+ let interface = ptr!.pointee
154
+ let addrFamily = interface.ifa_addr.pointee.sa_family
155
+ if addrFamily == UInt8(AF_INET) {
156
+ let name = String(cString: interface.ifa_name)
157
+ if name == "en0" {
158
+ var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
159
+ getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len), &hostname, socklen_t(hostname.count), nil, 0, NI_NUMERICHOST)
160
+ address = String(cString: hostname)
161
+ }
162
+ }
163
+ ptr = interface.ifa_next
334
164
  }
165
+ freeifaddrs(ifaddr)
335
166
  }
336
-
337
167
  return address
338
168
  }
339
- }
169
+ }
@@ -3,7 +3,6 @@ import Capacitor
3
3
 
4
4
  @objc(HttpLocalServerSwifterPlugin)
5
5
  public class HttpLocalServerSwifterPlugin: CAPPlugin, CAPBridgedPlugin, HttpLocalServerSwifterDelegate {
6
- // MARK: - CAPBridgedPlugin Properties
7
6
  public let identifier = "HttpLocalServerSwifterPlugin"
8
7
  public let jsName = "HttpLocalServerSwifter"
9
8
  public let pluginMethods: [CAPPluginMethod] = [
@@ -12,59 +11,34 @@ public class HttpLocalServerSwifterPlugin: CAPPlugin, CAPBridgedPlugin, HttpLoca
12
11
  CAPPluginMethod(name: "sendResponse", returnType: CAPPluginReturnPromise)
13
12
  ]
14
13
 
15
- // MARK: - Properties
16
14
  private var localServer: HttpLocalServerSwifter?
17
15
 
18
- // MARK: - Lifecycle
19
- public override func load() {
20
- print("✅ HttpLocalServerSwifterPlugin: Plugin loaded")
21
- }
22
-
23
- // MARK: - Plugin Methods
24
16
  @objc func connect(_ call: CAPPluginCall) {
25
- print("📞 HttpLocalServerSwifterPlugin: connect() called")
26
-
27
- if localServer == nil {
28
- localServer = HttpLocalServerSwifter(delegate: self)
29
- print("✅ HttpLocalServerSwifterPlugin: Server instance created")
30
- }
31
-
17
+ if localServer == nil { localServer = HttpLocalServerSwifter(delegate: self) }
32
18
  localServer?.connect(call)
33
19
  }
34
20
 
35
21
  @objc func disconnect(_ call: CAPPluginCall) {
36
- print("📞 HttpLocalServerSwifterPlugin: disconnect() called")
37
-
38
- if localServer != nil {
39
- localServer?.disconnect(call)
40
- localServer = nil
41
- } else {
42
- call.resolve()
43
- }
22
+ localServer?.disconnect(call)
23
+ localServer = nil
44
24
  }
45
25
 
46
- @objc func sendResponse(_ call: CAPPluginCall) {
47
- guard let requestId = call.getString("requestId") else {
48
- print("❌ HttpLocalServerSwifterPlugin: Missing requestId")
49
- call.reject("Missing requestId")
50
- return
51
- }
52
-
53
- guard let body = call.getString("body") else {
54
- print("❌ HttpLocalServerSwifterPlugin: Missing body")
55
- call.reject("Missing body")
56
- return
57
- }
58
-
59
- print("📤 HttpLocalServerSwifterPlugin: sendResponse for requestId: \(requestId)")
60
- HttpLocalServerSwifter.handleJsResponse(requestId: requestId, body: body)
61
- call.resolve()
62
- }
26
+ @objc func sendResponse(_ call: CAPPluginCall) {
27
+ guard let requestId = call.getString("requestId") else {
28
+ call.reject("Missing requestId")
29
+ return
30
+ }
31
+
32
+ // Cast dictionaryRepresentation explicitly to [String: Any]
33
+ if let responseData = call.dictionaryRepresentation as? [String: Any] {
34
+ HttpLocalServerSwifter.handleJsResponse(requestId: requestId, responseData: responseData)
35
+ call.resolve()
36
+ } else {
37
+ call.reject("Could not parse response data")
38
+ }
39
+ }
63
40
 
64
- // MARK: - HttpLocalServerSwifterDelegate
65
41
  public func httpLocalServerSwifterDidReceiveRequest(_ data: [String: Any]) {
66
- print("📨 HttpLocalServerSwifterPlugin: Received request, notifying listeners")
67
- print(" Request data: \(data)")
68
42
  notifyListeners("onRequest", data: data)
69
43
  }
70
- }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cappitolian/http-local-server-swifter",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
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",