@cappitolian/http-local-server 0.0.7 → 0.0.9
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.
- package/android/src/main/java/com/cappitolian/plugins/httplocalserver/HttpLocalServer.java +306 -108
- package/android/src/main/java/com/cappitolian/plugins/httplocalserver/HttpLocalServerPlugin.java +47 -11
- package/dist/docs.json +115 -19
- package/dist/esm/definitions.d.ts +35 -6
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +24 -3
- package/dist/esm/index.js +24 -6
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +7 -5
- package/dist/esm/web.js +36 -4
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +60 -7
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +60 -7
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/HttpLocalServerPlugin/HttpLocalServer.swift +274 -96
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -2,25 +2,78 @@ var capacitorHttpLocalServer = (function (exports, core) {
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Plugin de servidor HTTP local para Android e iOS.
|
|
6
|
+
*
|
|
7
|
+
* Permite crear un servidor HTTP en el dispositivo que puede recibir
|
|
8
|
+
* peticiones desde otros dispositivos en la misma red.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { HttpLocalServer } from '@cappitolian/http-local-server';
|
|
13
|
+
*
|
|
14
|
+
* // Iniciar servidor
|
|
15
|
+
* const { ip, port } = await HttpLocalServer.connect();
|
|
16
|
+
* console.log(`Servidor en http://${ip}:${port}`);
|
|
17
|
+
*
|
|
18
|
+
* // Escuchar peticiones
|
|
19
|
+
* await HttpLocalServer.addListener('onRequest', async (data) => {
|
|
20
|
+
* console.log('Petición recibida:', data);
|
|
21
|
+
*
|
|
22
|
+
* // Procesar y responder
|
|
23
|
+
* await HttpLocalServer.sendResponse({
|
|
24
|
+
* requestId: data.requestId,
|
|
25
|
+
* body: JSON.stringify({ success: true })
|
|
26
|
+
* });
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
8
29
|
*/
|
|
9
30
|
const HttpLocalServer = core.registerPlugin('HttpLocalServer', {
|
|
10
31
|
web: () => Promise.resolve().then(function () { return web; }).then(m => new m.HttpLocalServerWeb()),
|
|
11
32
|
});
|
|
12
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Implementación web del plugin HttpLocalServer.
|
|
36
|
+
* Proporciona funcionalidad mock para desarrollo en navegador.
|
|
37
|
+
*/
|
|
13
38
|
class HttpLocalServerWeb extends core.WebPlugin {
|
|
39
|
+
constructor() {
|
|
40
|
+
super(...arguments);
|
|
41
|
+
this.isRunning = false;
|
|
42
|
+
}
|
|
14
43
|
async connect() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
44
|
+
if (this.isRunning) {
|
|
45
|
+
console.warn('HttpLocalServer: El servidor ya está ejecutándose (Mock).');
|
|
46
|
+
return { ip: '127.0.0.1', port: 8080 };
|
|
47
|
+
}
|
|
48
|
+
console.warn('HttpLocalServer: El servidor nativo no está disponible en navegador. ' +
|
|
49
|
+
'Retornando valores mock para desarrollo.');
|
|
50
|
+
this.isRunning = true;
|
|
51
|
+
return {
|
|
52
|
+
ip: '127.0.0.1',
|
|
53
|
+
port: 8080
|
|
54
|
+
};
|
|
18
55
|
}
|
|
19
56
|
async disconnect() {
|
|
57
|
+
if (!this.isRunning) {
|
|
58
|
+
console.log('HttpLocalServer: El servidor ya está detenido (Mock).');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
20
61
|
console.log('HttpLocalServer: Servidor detenido (Mock).');
|
|
62
|
+
this.isRunning = false;
|
|
21
63
|
}
|
|
22
64
|
async sendResponse(options) {
|
|
23
|
-
|
|
65
|
+
if (!this.isRunning) {
|
|
66
|
+
console.warn('HttpLocalServer: El servidor no está ejecutándose (Mock).');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const { requestId, body } = options;
|
|
70
|
+
if (!requestId) {
|
|
71
|
+
throw new Error('Missing requestId');
|
|
72
|
+
}
|
|
73
|
+
if (!body) {
|
|
74
|
+
throw new Error('Missing body');
|
|
75
|
+
}
|
|
76
|
+
console.log(`HttpLocalServer: Respuesta mock enviada para requestId: ${requestId}`, '\nBody:', body.substring(0, 100) + (body.length > 100 ? '...' : ''));
|
|
24
77
|
}
|
|
25
78
|
}
|
|
26
79
|
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n *
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Plugin de servidor HTTP local para Android e iOS.\n *\n * Permite crear un servidor HTTP en el dispositivo que puede recibir\n * peticiones desde otros dispositivos en la misma red.\n *\n * @example\n * ```typescript\n * import { HttpLocalServer } from '@cappitolian/http-local-server';\n *\n * // Iniciar servidor\n * const { ip, port } = await HttpLocalServer.connect();\n * console.log(`Servidor en http://${ip}:${port}`);\n *\n * // Escuchar peticiones\n * await HttpLocalServer.addListener('onRequest', async (data) => {\n * console.log('Petición recibida:', data);\n *\n * // Procesar y responder\n * await HttpLocalServer.sendResponse({\n * requestId: data.requestId,\n * body: JSON.stringify({ success: true })\n * });\n * });\n * ```\n */\nconst HttpLocalServer = registerPlugin('HttpLocalServer', {\n web: () => import('./web').then(m => new m.HttpLocalServerWeb()),\n});\nexport * from './definitions';\nexport { HttpLocalServer };\nexport default HttpLocalServer;\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\n/**\n * Implementación web del plugin HttpLocalServer.\n * Proporciona funcionalidad mock para desarrollo en navegador.\n */\nexport class HttpLocalServerWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.isRunning = false;\n }\n async connect() {\n if (this.isRunning) {\n console.warn('HttpLocalServer: El servidor ya está ejecutándose (Mock).');\n return { ip: '127.0.0.1', port: 8080 };\n }\n console.warn('HttpLocalServer: El servidor nativo no está disponible en navegador. ' +\n 'Retornando valores mock para desarrollo.');\n this.isRunning = true;\n return {\n ip: '127.0.0.1',\n port: 8080\n };\n }\n async disconnect() {\n if (!this.isRunning) {\n console.log('HttpLocalServer: El servidor ya está detenido (Mock).');\n return;\n }\n console.log('HttpLocalServer: Servidor detenido (Mock).');\n this.isRunning = false;\n }\n async sendResponse(options) {\n if (!this.isRunning) {\n console.warn('HttpLocalServer: El servidor no está ejecutándose (Mock).');\n return;\n }\n const { requestId, body } = options;\n if (!requestId) {\n throw new Error('Missing requestId');\n }\n if (!body) {\n throw new Error('Missing body');\n }\n console.log(`HttpLocalServer: Respuesta mock enviada para requestId: ${requestId}`, '\\nBody:', body.substring(0, 100) + (body.length > 100 ? '...' : ''));\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACK,UAAC,eAAe,GAAGA,mBAAc,CAAC,iBAAiB,EAAE;IAC1D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,kBAAkB,EAAE,CAAC;IACpE,CAAC;;IC5BD;IACA;IACA;IACA;IACO,MAAM,kBAAkB,SAASC,cAAS,CAAC;IAClD,IAAI,WAAW,GAAG;IAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;IAC3B,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;IAC9B,IAAI;IACJ,IAAI,MAAM,OAAO,GAAG;IACpB,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE;IAC5B,YAAY,OAAO,CAAC,IAAI,CAAC,2DAA2D,CAAC;IACrF,YAAY,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE;IAClD,QAAQ;IACR,QAAQ,OAAO,CAAC,IAAI,CAAC,uEAAuE;IAC5F,YAAY,0CAA0C,CAAC;IACvD,QAAQ,IAAI,CAAC,SAAS,GAAG,IAAI;IAC7B,QAAQ,OAAO;IACf,YAAY,EAAE,EAAE,WAAW;IAC3B,YAAY,IAAI,EAAE;IAClB,SAAS;IACT,IAAI;IACJ,IAAI,MAAM,UAAU,GAAG;IACvB,QAAQ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE;IAC7B,YAAY,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC;IAChF,YAAY;IACZ,QAAQ;IACR,QAAQ,OAAO,CAAC,GAAG,CAAC,4CAA4C,CAAC;IACjE,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;IAC9B,IAAI;IACJ,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;IAChC,QAAQ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE;IAC7B,YAAY,OAAO,CAAC,IAAI,CAAC,2DAA2D,CAAC;IACrF,YAAY;IACZ,QAAQ;IACR,QAAQ,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO;IAC3C,QAAQ,IAAI,CAAC,SAAS,EAAE;IACxB,YAAY,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC;IAChD,QAAQ;IACR,QAAQ,IAAI,CAAC,IAAI,EAAE;IACnB,YAAY,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC;IAC3C,QAAQ;IACR,QAAQ,OAAO,CAAC,GAAG,CAAC,CAAC,wDAAwD,EAAE,SAAS,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,GAAG,KAAK,GAAG,EAAE,CAAC,CAAC;IACjK,IAAI;IACJ;;;;;;;;;;;;;;;;;;"}
|
|
@@ -20,104 +20,76 @@ 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
|
+
|
|
31
|
+
// MARK: - Properties
|
|
32
|
+
private var webServer: GCDWebServer?
|
|
33
|
+
private weak var delegate: HttpLocalServerDelegate?
|
|
34
|
+
|
|
35
|
+
private static var pendingResponses = [String: (String) -> Void]()
|
|
36
|
+
private static let queue = DispatchQueue(label: "com.cappitolian.HttpLocalServer.pendingResponses", qos: .userInitiated)
|
|
37
|
+
|
|
38
|
+
private let defaultTimeout: TimeInterval = 5.0
|
|
39
|
+
private let defaultPort: UInt = 8080
|
|
40
|
+
|
|
41
|
+
// MARK: - Initialization
|
|
33
42
|
public init(delegate: HttpLocalServerDelegate) {
|
|
34
43
|
self.delegate = delegate
|
|
44
|
+
super.init()
|
|
35
45
|
}
|
|
36
|
-
|
|
46
|
+
|
|
47
|
+
deinit {
|
|
48
|
+
disconnect()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// MARK: - Public Methods
|
|
37
52
|
@objc public func connect(_ call: CAPPluginCall) {
|
|
38
53
|
DispatchQueue.main.async { [weak self] in
|
|
39
|
-
guard let self = self else {
|
|
40
|
-
|
|
54
|
+
guard let self = self else {
|
|
55
|
+
call.reject("Server instance deallocated")
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Stop existing server if running
|
|
60
|
+
if self.webServer?.isRunning == true {
|
|
61
|
+
self.webServer?.stop()
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
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
|
|
65
|
+
self.setupHandlers()
|
|
66
|
+
|
|
95
67
|
do {
|
|
96
|
-
try self.
|
|
97
|
-
GCDWebServerOption_Port: port,
|
|
98
|
-
GCDWebServerOption_BonjourName: "",
|
|
99
|
-
GCDWebServerOption_BindToLocalhost: false
|
|
100
|
-
])
|
|
68
|
+
try self.startServer()
|
|
101
69
|
let ip = Self.getWiFiAddress() ?? "127.0.0.1"
|
|
102
70
|
call.resolve([
|
|
103
71
|
"ip": ip,
|
|
104
|
-
"port":
|
|
72
|
+
"port": self.defaultPort
|
|
105
73
|
])
|
|
106
74
|
} catch {
|
|
107
75
|
call.reject("Failed to start server: \(error.localizedDescription)")
|
|
108
76
|
}
|
|
109
77
|
}
|
|
110
78
|
}
|
|
111
|
-
|
|
112
|
-
@objc public func disconnect(_ call: CAPPluginCall) {
|
|
79
|
+
|
|
80
|
+
@objc public func disconnect(_ call: CAPPluginCall? = nil) {
|
|
113
81
|
DispatchQueue.main.async { [weak self] in
|
|
114
|
-
self
|
|
115
|
-
|
|
116
|
-
|
|
82
|
+
guard let self = self else {
|
|
83
|
+
call?.reject("Server instance deallocated")
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
self.disconnect()
|
|
88
|
+
call?.resolve()
|
|
117
89
|
}
|
|
118
90
|
}
|
|
119
|
-
|
|
120
|
-
//
|
|
91
|
+
|
|
92
|
+
// MARK: - Static Methods
|
|
121
93
|
static func handleJsResponse(requestId: String, body: String) {
|
|
122
94
|
queue.async {
|
|
123
95
|
if let callback = pendingResponses[requestId] {
|
|
@@ -126,32 +98,238 @@ public protocol HttpLocalServerDelegate: AnyObject {
|
|
|
126
98
|
}
|
|
127
99
|
}
|
|
128
100
|
}
|
|
129
|
-
|
|
130
|
-
//
|
|
101
|
+
|
|
102
|
+
// MARK: - Private Methods
|
|
103
|
+
private func disconnect() {
|
|
104
|
+
webServer?.stop()
|
|
105
|
+
webServer = nil
|
|
106
|
+
|
|
107
|
+
// Clear pending responses
|
|
108
|
+
Self.queue.async {
|
|
109
|
+
Self.pendingResponses.removeAll()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func setupHandlers() {
|
|
114
|
+
guard let webServer = webServer else { return }
|
|
115
|
+
|
|
116
|
+
// GET requests
|
|
117
|
+
webServer.addDefaultHandler(
|
|
118
|
+
forMethod: "GET",
|
|
119
|
+
request: GCDWebServerRequest.self,
|
|
120
|
+
processBlock: { [weak self] request in
|
|
121
|
+
return self?.processRequest(request) ?? self?.errorResponse() ?? GCDWebServerResponse()
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// POST requests (with body)
|
|
126
|
+
webServer.addDefaultHandler(
|
|
127
|
+
forMethod: "POST",
|
|
128
|
+
request: GCDWebServerDataRequest.self,
|
|
129
|
+
processBlock: { [weak self] request in
|
|
130
|
+
return self?.processRequest(request) ?? self?.errorResponse() ?? GCDWebServerResponse()
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// PUT requests (with body)
|
|
135
|
+
webServer.addDefaultHandler(
|
|
136
|
+
forMethod: "PUT",
|
|
137
|
+
request: GCDWebServerDataRequest.self,
|
|
138
|
+
processBlock: { [weak self] request in
|
|
139
|
+
return self?.processRequest(request) ?? self?.errorResponse() ?? GCDWebServerResponse()
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// PATCH requests (with body)
|
|
144
|
+
webServer.addDefaultHandler(
|
|
145
|
+
forMethod: "PATCH",
|
|
146
|
+
request: GCDWebServerDataRequest.self,
|
|
147
|
+
processBlock: { [weak self] request in
|
|
148
|
+
return self?.processRequest(request) ?? self?.errorResponse() ?? GCDWebServerResponse()
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// DELETE requests
|
|
153
|
+
webServer.addDefaultHandler(
|
|
154
|
+
forMethod: "DELETE",
|
|
155
|
+
request: GCDWebServerRequest.self,
|
|
156
|
+
processBlock: { [weak self] request in
|
|
157
|
+
return self?.processRequest(request) ?? self?.errorResponse() ?? GCDWebServerResponse()
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
// OPTIONS requests (CORS preflight)
|
|
162
|
+
webServer.addDefaultHandler(
|
|
163
|
+
forMethod: "OPTIONS",
|
|
164
|
+
request: GCDWebServerRequest.self,
|
|
165
|
+
processBlock: { [weak self] request in
|
|
166
|
+
return self?.corsResponse() ?? GCDWebServerResponse()
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private func processRequest(_ request: GCDWebServerRequest) -> GCDWebServerResponse {
|
|
172
|
+
let method = request.method
|
|
173
|
+
let path = request.url.path
|
|
174
|
+
let body = extractBody(from: request)
|
|
175
|
+
let headers = request.headers
|
|
176
|
+
let query = request.query
|
|
177
|
+
|
|
178
|
+
let requestId = UUID().uuidString
|
|
179
|
+
var responseString: String?
|
|
180
|
+
|
|
181
|
+
// Setup semaphore for synchronous waiting
|
|
182
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
183
|
+
|
|
184
|
+
Self.queue.async {
|
|
185
|
+
Self.pendingResponses[requestId] = { responseBody in
|
|
186
|
+
responseString = responseBody
|
|
187
|
+
semaphore.signal()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Notify delegate with request info
|
|
192
|
+
var requestData: [String: Any] = [
|
|
193
|
+
"requestId": requestId,
|
|
194
|
+
"method": method,
|
|
195
|
+
"path": path
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
// Solo agregar si existen (consistente con TypeScript y Android)
|
|
199
|
+
if let body = body, !body.isEmpty {
|
|
200
|
+
requestData["body"] = body
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if let headers = headers as? [String: String], !headers.isEmpty {
|
|
204
|
+
requestData["headers"] = headers
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if let query = query, !query.isEmpty {
|
|
208
|
+
requestData["query"] = query
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
delegate?.httpLocalServerDidReceiveRequest(requestData)
|
|
212
|
+
|
|
213
|
+
// Wait for JS response or timeout
|
|
214
|
+
let result = semaphore.wait(timeout: .now() + defaultTimeout)
|
|
215
|
+
|
|
216
|
+
// Cleanup
|
|
217
|
+
Self.queue.async {
|
|
218
|
+
Self.pendingResponses.removeValue(forKey: requestId)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Handle timeout
|
|
222
|
+
if result == .timedOut {
|
|
223
|
+
let timeoutResponse = "{\"error\":\"Request timeout\",\"requestId\":\"\(requestId)\"}"
|
|
224
|
+
return createJsonResponse(timeoutResponse, statusCode: 408)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let reply = responseString ?? "{\"error\":\"No response from handler\"}"
|
|
228
|
+
return createJsonResponse(reply)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private func extractBody(from request: GCDWebServerRequest) -> String? {
|
|
232
|
+
guard let dataRequest = request as? GCDWebServerDataRequest else {
|
|
233
|
+
return nil
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return String(data: dataRequest.data, encoding: .utf8)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func createJsonResponse(_ body: String, statusCode: Int = 200) -> GCDWebServerDataResponse {
|
|
240
|
+
let response = GCDWebServerDataResponse(text: body)
|
|
241
|
+
response?.statusCode = statusCode
|
|
242
|
+
response?.contentType = "application/json"
|
|
243
|
+
|
|
244
|
+
// CORS headers
|
|
245
|
+
response?.setValue("*", forAdditionalHeader: "Access-Control-Allow-Origin")
|
|
246
|
+
response?.setValue("GET, POST, PUT, PATCH, DELETE, OPTIONS", forAdditionalHeader: "Access-Control-Allow-Methods")
|
|
247
|
+
response?.setValue("Origin, Content-Type, Accept, Authorization", forAdditionalHeader: "Access-Control-Allow-Headers")
|
|
248
|
+
response?.setValue("true", forAdditionalHeader: "Access-Control-Allow-Credentials")
|
|
249
|
+
response?.setValue("3600", forAdditionalHeader: "Access-Control-Max-Age")
|
|
250
|
+
|
|
251
|
+
return response ?? GCDWebServerDataResponse()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private func corsResponse() -> GCDWebServerDataResponse {
|
|
255
|
+
return createJsonResponse("{}", statusCode: 204)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private func errorResponse() -> GCDWebServerDataResponse {
|
|
259
|
+
return createJsonResponse("{\"error\":\"Server error\"}", statusCode: 500)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private func startServer() throws {
|
|
263
|
+
guard let webServer = webServer else {
|
|
264
|
+
throw NSError(
|
|
265
|
+
domain: "HttpLocalServer",
|
|
266
|
+
code: -1,
|
|
267
|
+
userInfo: [NSLocalizedDescriptionKey: "WebServer not initialized"]
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let options: [String: Any] = [
|
|
272
|
+
GCDWebServerOption_Port: defaultPort,
|
|
273
|
+
GCDWebServerOption_BonjourName: "",
|
|
274
|
+
GCDWebServerOption_BindToLocalhost: false,
|
|
275
|
+
GCDWebServerOption_AutomaticallySuspendInBackground: false
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
try webServer.start(options: options)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// MARK: - Network Utilities
|
|
131
282
|
static func getWiFiAddress() -> String? {
|
|
132
283
|
var address: String?
|
|
133
284
|
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
|
-
}
|
|
285
|
+
|
|
286
|
+
guard getifaddrs(&ifaddr) == 0 else {
|
|
287
|
+
return nil
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
defer {
|
|
153
291
|
freeifaddrs(ifaddr)
|
|
154
292
|
}
|
|
293
|
+
|
|
294
|
+
var ptr = ifaddr
|
|
295
|
+
while ptr != nil {
|
|
296
|
+
defer { ptr = ptr?.pointee.ifa_next }
|
|
297
|
+
|
|
298
|
+
guard let interface = ptr?.pointee else { continue }
|
|
299
|
+
|
|
300
|
+
let addrFamily = interface.ifa_addr.pointee.sa_family
|
|
301
|
+
|
|
302
|
+
// Check for IPv4 interface
|
|
303
|
+
guard addrFamily == UInt8(AF_INET) else { continue }
|
|
304
|
+
|
|
305
|
+
let name = String(cString: interface.ifa_name)
|
|
306
|
+
|
|
307
|
+
// WiFi interface (en0) or cellular (pdp_ip0)
|
|
308
|
+
guard name == "en0" || name == "pdp_ip0" else { continue }
|
|
309
|
+
|
|
310
|
+
var addr = interface.ifa_addr.pointee
|
|
311
|
+
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
|
312
|
+
|
|
313
|
+
let result = getnameinfo(
|
|
314
|
+
&addr,
|
|
315
|
+
socklen_t(interface.ifa_addr.pointee.sa_len),
|
|
316
|
+
&hostname,
|
|
317
|
+
socklen_t(hostname.count),
|
|
318
|
+
nil,
|
|
319
|
+
0,
|
|
320
|
+
NI_NUMERICHOST
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
guard result == 0 else { continue }
|
|
324
|
+
|
|
325
|
+
address = String(cString: hostname)
|
|
326
|
+
|
|
327
|
+
// Prefer en0 (WiFi) over pdp_ip0 (cellular)
|
|
328
|
+
if name == "en0" {
|
|
329
|
+
break
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
155
333
|
return address
|
|
156
334
|
}
|
|
157
|
-
}
|
|
335
|
+
}
|
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.9",
|
|
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",
|