@cappitolian/http-local-server-swifter 0.0.16 → 0.0.18
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/README.md +57 -30
- package/dist/docs.json +18 -3
- package/dist/esm/definitions.d.ts +21 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +4 -0
- package/dist/esm/web.js +7 -1
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +7 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +7 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/HttpLocalServerSwifterPlugin/HttpLocalServerSwifter.swift +89 -77
- package/ios/Sources/HttpLocalServerSwifterPlugin/HttpLocalServerSwifterPlugin.swift +32 -21
- package/ios/Sources/HttpLocalServerSwifterPlugin/LocalNetworkPermission.swift +79 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,12 +10,14 @@ A Capacitor plugin to run a local HTTP server on your device, allowing you to re
|
|
|
10
10
|
- ✅ Receive requests via events and send responses back from the JS layer
|
|
11
11
|
- ✅ CORS support enabled by default for local communication
|
|
12
12
|
- ✅ Support for all HTTP methods (GET, POST, PUT, PATCH, DELETE, OPTIONS)
|
|
13
|
+
- ✅ Dynamic URL routing (e.g. `/orders/:id`) supported via middleware
|
|
13
14
|
- ✅ Swift Package Manager (SPM) support
|
|
14
15
|
- ✅ Tested with **Capacitor 8** and **Ionic 8**
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
20
|
+
|
|
19
21
|
```bash
|
|
20
22
|
npm install @cappitolian/http-local-server-swifter
|
|
21
23
|
npx cap sync
|
|
@@ -26,11 +28,13 @@ npx cap sync
|
|
|
26
28
|
## Usage
|
|
27
29
|
|
|
28
30
|
### Import
|
|
31
|
+
|
|
29
32
|
```typescript
|
|
30
33
|
import { HttpLocalServerSwifter } from '@cappitolian/http-local-server-swifter';
|
|
31
34
|
```
|
|
32
35
|
|
|
33
36
|
### Listen and Respond
|
|
37
|
+
|
|
34
38
|
```typescript
|
|
35
39
|
// 1. Set up the listener for incoming requests
|
|
36
40
|
await HttpLocalServerSwifter.addListener('onRequest', async (data) => {
|
|
@@ -42,9 +46,9 @@ await HttpLocalServerSwifter.addListener('onRequest', async (data) => {
|
|
|
42
46
|
// 2. Send a response back to the client using the requestId
|
|
43
47
|
await HttpLocalServerSwifter.sendResponse({
|
|
44
48
|
requestId: data.requestId,
|
|
45
|
-
body: JSON.stringify({
|
|
46
|
-
success: true,
|
|
47
|
-
message: 'Request processed!'
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
success: true,
|
|
51
|
+
message: 'Request processed!'
|
|
48
52
|
})
|
|
49
53
|
});
|
|
50
54
|
});
|
|
@@ -56,6 +60,7 @@ HttpLocalServerSwifter.connect().then(result => {
|
|
|
56
60
|
```
|
|
57
61
|
|
|
58
62
|
### Stop Server
|
|
63
|
+
|
|
59
64
|
```typescript
|
|
60
65
|
// 4. Stop the server
|
|
61
66
|
await HttpLocalServerSwifter.disconnect();
|
|
@@ -79,9 +84,9 @@ await HttpLocalServerSwifter.disconnect();
|
|
|
79
84
|
|
|
80
85
|
---
|
|
81
86
|
|
|
82
|
-
## Migration from v0.
|
|
87
|
+
## Migration from v0.1.x
|
|
83
88
|
|
|
84
|
-
Version 0.
|
|
89
|
+
Version 0.2.0 introduces middleware-based routing on iOS and dynamic response support (custom `status` and `headers`) on both platforms. See changes below.
|
|
85
90
|
|
|
86
91
|
---
|
|
87
92
|
|
|
@@ -94,42 +99,64 @@ MIT
|
|
|
94
99
|
## Support
|
|
95
100
|
|
|
96
101
|
If you have any issues or feature requests, please open an issue on the repository.
|
|
97
|
-
```
|
|
98
102
|
|
|
99
103
|
---
|
|
100
104
|
|
|
101
105
|
## 📋 Cambios Principales
|
|
102
106
|
|
|
103
|
-
### **
|
|
104
|
-
|
|
105
|
-
| Aspecto |
|
|
106
|
-
|
|
107
|
-
| **
|
|
108
|
-
| **
|
|
109
|
-
| **
|
|
110
|
-
| **
|
|
111
|
-
| **
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
### **Route Handlers → Middleware (iOS)**
|
|
108
|
+
|
|
109
|
+
| Aspecto | v0.1.x | v0.2.0 |
|
|
110
|
+
|---------|--------|--------|
|
|
111
|
+
| **Routing** | `server["/:path"] = { ... }` | `server.middleware.append { ... }` |
|
|
112
|
+
| **Rutas dinámicas** | ❌ Solo un segmento (`/menu`) | ✅ Cualquier ruta (`/orders/:id`) |
|
|
113
|
+
| **CORS preflight** | Manejado por handler estático | Interceptado en middleware antes del JS |
|
|
114
|
+
| **Thread de inicio** | Main thread | Background thread (`DispatchQueue.global`) |
|
|
115
|
+
| **Respuesta dinámica** | Solo `body` | `body` + `status` + `headers` |
|
|
116
|
+
|
|
117
|
+
### **sendResponse — Nuevos campos opcionales**
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
await HttpLocalServerSwifter.sendResponse({
|
|
121
|
+
requestId: data.requestId,
|
|
122
|
+
body: JSON.stringify({ success: true }),
|
|
123
|
+
status: 200, // NEW: opcional, default 200
|
|
124
|
+
headers: { // NEW: opcional, headers custom
|
|
125
|
+
'X-Custom-Header': 'value'
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### **Archivos modificados**
|
|
131
|
+
|
|
132
|
+
| Archivo | Cambio |
|
|
133
|
+
|---------|--------|
|
|
134
|
+
| `HttpLocalServerSwifter.swift` | Middleware en lugar de route handlers; `handleJsResponse` acepta `[String: Any]` |
|
|
135
|
+
| `HttpLocalServerSwifterPlugin.swift` | `sendResponse` pasa `dictionaryRepresentation` completo |
|
|
136
|
+
| `HttpLocalServerSwifterPlugin.java` | `sendResponse` pasa `call.getData()` completo |
|
|
137
|
+
| `definitions.ts` | `HttpSendResponseOptions` agrega `status?` y `headers?` |
|
|
138
|
+
| `web.ts` | Mock actualizado con los nuevos campos |
|
|
122
139
|
|
|
123
140
|
---
|
|
124
141
|
|
|
125
142
|
## ✅ Pasos para Aplicar
|
|
126
143
|
|
|
127
|
-
1. **Reemplaza `HttpLocalServerSwifter.swift`** con la versión
|
|
128
|
-
2. **
|
|
129
|
-
3. **
|
|
130
|
-
4. **Actualiza `
|
|
144
|
+
1. **Reemplaza `HttpLocalServerSwifter.swift`** con la versión nueva (middleware)
|
|
145
|
+
2. **Reemplaza `HttpLocalServerSwifterPlugin.swift`** con la versión nueva
|
|
146
|
+
3. **Reemplaza `HttpLocalServerSwifterPlugin.java`** con la versión nueva
|
|
147
|
+
4. **Actualiza `definitions.ts`**, **`web.ts`** e **`index.ts`**
|
|
131
148
|
5. **En Xcode**:
|
|
132
149
|
```
|
|
133
150
|
File → Packages → Reset Package Caches
|
|
134
151
|
File → Packages → Resolve Package Versions
|
|
135
|
-
Product → Clean Build Folder
|
|
152
|
+
Product → Clean Build Folder
|
|
153
|
+
Product → Run
|
|
154
|
+
```
|
|
155
|
+
6. **En Android Studio**:
|
|
156
|
+
```
|
|
157
|
+
Build → Clean Project
|
|
158
|
+
Build → Rebuild Project
|
|
159
|
+
Run
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
> ⚠️ `npx cap sync` solo sincroniza archivos web. Los cambios en código nativo Swift/Java **requieren recompilación desde el IDE**.
|
package/dist/docs.json
CHANGED
|
@@ -39,12 +39,27 @@
|
|
|
39
39
|
],
|
|
40
40
|
"returns": "Promise<void>",
|
|
41
41
|
"tags": [],
|
|
42
|
-
"docs": "Sends a response back to the client.\
|
|
42
|
+
"docs": "Sends a response back to the client.\nSupports status and headers to handle CORS Preflight correctly.",
|
|
43
43
|
"complexTypes": [
|
|
44
44
|
"HttpSendResponseOptions"
|
|
45
45
|
],
|
|
46
46
|
"slug": "sendresponse"
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
"name": "openSettings",
|
|
50
|
+
"signature": "() => Promise<void>",
|
|
51
|
+
"parameters": [],
|
|
52
|
+
"returns": "Promise<void>",
|
|
53
|
+
"tags": [
|
|
54
|
+
{
|
|
55
|
+
"name": "example",
|
|
56
|
+
"text": "try {\n await HttpLocalServerSwifter.connect();\n} catch (error) {\n if (error.message === 'LOCAL_NETWORK_PERMISSION_DENIED') {\n await HttpLocalServerSwifter.openSettings();\n }\n}"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"docs": "Opens the app's page in iOS Settings so the user can manually grant\nLocal Network permission after having denied it.\n\nOnly relevant on iOS — on Android this is a no-op.",
|
|
60
|
+
"complexTypes": [],
|
|
61
|
+
"slug": "opensettings"
|
|
62
|
+
},
|
|
48
63
|
{
|
|
49
64
|
"name": "addListener",
|
|
50
65
|
"signature": "(eventName: 'onRequest', listenerFunc: (data: HttpRequestData) => void | Promise<void>) => Promise<PluginListenerHandle>",
|
|
@@ -130,14 +145,14 @@
|
|
|
130
145
|
{
|
|
131
146
|
"name": "status",
|
|
132
147
|
"tags": [],
|
|
133
|
-
"docs": "
|
|
148
|
+
"docs": "HTTP Status code (e.g., 200, 204, 404).\nDefault is 200.",
|
|
134
149
|
"complexTypes": [],
|
|
135
150
|
"type": "number | undefined"
|
|
136
151
|
},
|
|
137
152
|
{
|
|
138
153
|
"name": "headers",
|
|
139
154
|
"tags": [],
|
|
140
|
-
"docs": "
|
|
155
|
+
"docs": "Custom HTTP headers.\nCrucial for fixing CORS by providing 'Access-Control-Allow-Origin'.",
|
|
141
156
|
"complexTypes": [
|
|
142
157
|
"Record"
|
|
143
158
|
],
|
|
@@ -20,11 +20,13 @@ export interface HttpSendResponseOptions {
|
|
|
20
20
|
requestId: string;
|
|
21
21
|
/** The response body (usually stringified JSON) */
|
|
22
22
|
body: string;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* HTTP Status code (e.g., 200, 204, 404).
|
|
24
25
|
* Default is 200.
|
|
25
26
|
*/
|
|
26
27
|
status?: number;
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Custom HTTP headers.
|
|
28
30
|
* Crucial for fixing CORS by providing 'Access-Control-Allow-Origin'.
|
|
29
31
|
*/
|
|
30
32
|
headers?: Record<string, string>;
|
|
@@ -34,9 +36,25 @@ export interface HttpLocalServerSwifterPlugin {
|
|
|
34
36
|
disconnect(): Promise<void>;
|
|
35
37
|
/**
|
|
36
38
|
* Sends a response back to the client.
|
|
37
|
-
*
|
|
39
|
+
* Supports status and headers to handle CORS Preflight correctly.
|
|
38
40
|
*/
|
|
39
41
|
sendResponse(options: HttpSendResponseOptions): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Opens the app's page in iOS Settings so the user can manually grant
|
|
44
|
+
* Local Network permission after having denied it.
|
|
45
|
+
*
|
|
46
|
+
* Only relevant on iOS — on Android this is a no-op.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* try {
|
|
50
|
+
* await HttpLocalServerSwifter.connect();
|
|
51
|
+
* } catch (error) {
|
|
52
|
+
* if (error.message === 'LOCAL_NETWORK_PERMISSION_DENIED') {
|
|
53
|
+
* await HttpLocalServerSwifter.openSettings();
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
openSettings(): Promise<void>;
|
|
40
58
|
addListener(eventName: 'onRequest', listenerFunc: (data: HttpRequestData) => void | Promise<void>): Promise<PluginListenerHandle>;
|
|
41
59
|
removeAllListeners(): Promise<void>;
|
|
42
60
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface HttpConnectResult {\n ip: string;\n port: number;\n}\n\nexport interface HttpRequestData {\n requestId: string;\n method: string;\n path: string;\n body?: string;\n headers?: Record<string, string>;\n query?: Record<string, string>;\n}\n\n/**\n * Options for sending an HTTP response.\n * Updated to support custom status codes and headers for CORS.\n */\nexport interface HttpSendResponseOptions {\n /** The ID received in the 'onRequest' event */\n requestId: string;\n\n /** The response body (usually stringified JSON) */\n body: string;\n\n
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface HttpConnectResult {\n ip: string;\n port: number;\n}\n\nexport interface HttpRequestData {\n requestId: string;\n method: string;\n path: string;\n body?: string;\n headers?: Record<string, string>;\n query?: Record<string, string>;\n}\n\n/**\n * Options for sending an HTTP response.\n * Updated to support custom status codes and headers for CORS.\n */\nexport interface HttpSendResponseOptions {\n /** The ID received in the 'onRequest' event */\n requestId: string;\n\n /** The response body (usually stringified JSON) */\n body: string;\n\n /**\n * HTTP Status code (e.g., 200, 204, 404).\n * Default is 200.\n */\n status?: number;\n\n /**\n * Custom HTTP headers.\n * Crucial for fixing CORS by providing 'Access-Control-Allow-Origin'.\n */\n headers?: Record<string, string>;\n}\n\nexport interface HttpLocalServerSwifterPlugin {\n connect(): Promise<HttpConnectResult>;\n disconnect(): Promise<void>;\n\n /**\n * Sends a response back to the client.\n * Supports status and headers to handle CORS Preflight correctly.\n */\n sendResponse(options: HttpSendResponseOptions): Promise<void>;\n\n /**\n * Opens the app's page in iOS Settings so the user can manually grant\n * Local Network permission after having denied it.\n *\n * Only relevant on iOS — on Android this is a no-op.\n *\n * @example\n * try {\n * await HttpLocalServerSwifter.connect();\n * } catch (error) {\n * if (error.message === 'LOCAL_NETWORK_PERMISSION_DENIED') {\n * await HttpLocalServerSwifter.openSettings();\n * }\n * }\n */\n openSettings(): Promise<void>;\n\n addListener(\n eventName: 'onRequest',\n listenerFunc: (data: HttpRequestData) => void | Promise<void>\n ): Promise<PluginListenerHandle>;\n\n removeAllListeners(): Promise<void>;\n}"]}
|
package/dist/esm/web.d.ts
CHANGED
|
@@ -5,4 +5,8 @@ export declare class HttpLocalServerSwifterWeb extends WebPlugin implements Http
|
|
|
5
5
|
connect(): Promise<HttpConnectResult>;
|
|
6
6
|
disconnect(): Promise<void>;
|
|
7
7
|
sendResponse(options: HttpSendResponseOptions): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* No-op on web and Android — Local Network permission dialogs only exist on iOS.
|
|
10
|
+
*/
|
|
11
|
+
openSettings(): Promise<void>;
|
|
8
12
|
}
|
package/dist/esm/web.js
CHANGED
|
@@ -17,7 +17,13 @@ export class HttpLocalServerSwifterWeb extends WebPlugin {
|
|
|
17
17
|
if (!this.isRunning)
|
|
18
18
|
throw new Error('Server not running');
|
|
19
19
|
const { requestId, body, status, headers } = options;
|
|
20
|
-
console.log(
|
|
20
|
+
console.log('[HttpLocalServerSwifter Web] Mock Response:', { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* No-op on web and Android — Local Network permission dialogs only exist on iOS.
|
|
24
|
+
*/
|
|
25
|
+
async openSettings() {
|
|
26
|
+
console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
//# sourceMappingURL=web.js.map
|
package/dist/esm/web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAO5C,MAAM,OAAO,yBAA0B,SAAQ,SAAS;IAAxD;;QACU,cAAS,GAAG,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAO5C,MAAM,OAAO,yBAA0B,SAAQ,SAAS;IAAxD;;QACU,cAAS,GAAG,KAAK,CAAC;IA4B5B,CAAC;IA1BC,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,oEAAoE,CAAC,CAAC;QACnF,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,UAAU;QACd,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAAgC;QACjD,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;QAC3D,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QACrD,OAAO,CAAC,GAAG,CACT,6CAA6C,EAC7C,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAN,MAAM,cAAN,MAAM,GAAI,GAAG,EAAE,OAAO,EAAE,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CACtF,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY;QAChB,OAAO,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;IACxF,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\nimport type {\n HttpLocalServerSwifterPlugin,\n HttpConnectResult,\n HttpSendResponseOptions\n} from './definitions';\n\nexport class HttpLocalServerSwifterWeb extends WebPlugin implements HttpLocalServerSwifterPlugin {\n private isRunning = false;\n\n async connect(): Promise<HttpConnectResult> {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n\n async disconnect(): Promise<void> {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n\n async sendResponse(options: HttpSendResponseOptions): Promise<void> {\n if (!this.isRunning) throw new Error('Server not running');\n const { requestId, body, status, headers } = options;\n console.log(\n '[HttpLocalServerSwifter Web] Mock Response:',\n { requestId, status: status ?? 200, headers: headers ?? {}, bodyLength: body.length }\n );\n }\n\n /**\n * No-op on web and Android — Local Network permission dialogs only exist on iOS.\n */\n async openSettings(): Promise<void> {\n console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');\n }\n}"]}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -31,7 +31,13 @@ class HttpLocalServerSwifterWeb extends core.WebPlugin {
|
|
|
31
31
|
if (!this.isRunning)
|
|
32
32
|
throw new Error('Server not running');
|
|
33
33
|
const { requestId, body, status, headers } = options;
|
|
34
|
-
console.log(
|
|
34
|
+
console.log('[HttpLocalServerSwifter Web] Mock Response:', { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* No-op on web and Android — Local Network permission dialogs only exist on iOS.
|
|
38
|
+
*/
|
|
39
|
+
async openSettings() {
|
|
40
|
+
console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Local HTTP server plugin for Android and iOS.\n * * Allows creating an HTTP server on the device that can receive\n * requests from other devices on the same local network or\n * from the app's own WebView (fixing CORS issues).\n */\nconst HttpLocalServerSwifter = registerPlugin('HttpLocalServerSwifter', {\n // We point to the web mock for browser development\n web: () => import('./web').then(m => new m.HttpLocalServerSwifterWeb()),\n});\n// Re-export everything from definitions so they are available to the app\nexport * from './definitions';\nexport { HttpLocalServerSwifter };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HttpLocalServerSwifterWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.isRunning = false;\n }\n async connect() {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n async disconnect() {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n async sendResponse(options) {\n if (!this.isRunning)\n throw new Error('Server not running');\n const { requestId, body, status, headers } = options;\n console.log(
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Local HTTP server plugin for Android and iOS.\n * * Allows creating an HTTP server on the device that can receive\n * requests from other devices on the same local network or\n * from the app's own WebView (fixing CORS issues).\n */\nconst HttpLocalServerSwifter = registerPlugin('HttpLocalServerSwifter', {\n // We point to the web mock for browser development\n web: () => import('./web').then(m => new m.HttpLocalServerSwifterWeb()),\n});\n// Re-export everything from definitions so they are available to the app\nexport * from './definitions';\nexport { HttpLocalServerSwifter };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HttpLocalServerSwifterWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.isRunning = false;\n }\n async connect() {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n async disconnect() {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n async sendResponse(options) {\n if (!this.isRunning)\n throw new Error('Server not running');\n const { requestId, body, status, headers } = options;\n console.log('[HttpLocalServerSwifter Web] Mock Response:', { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });\n }\n /**\n * No-op on web and Android — Local Network permission dialogs only exist on iOS.\n */\n async openSettings() {\n console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACA;AACA;AACA;AACA;AACA;AACA;AACK,MAAC,sBAAsB,GAAGA,mBAAc,CAAC,wBAAwB,EAAE;AACxE;AACA,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,yBAAyB,EAAE,CAAC;AAC3E,CAAC;;ACTM,MAAM,yBAAyB,SAASC,cAAS,CAAC;AACzD,IAAI,WAAW,GAAG;AAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;AAC3B,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;AAC9B,IAAI;AACJ,IAAI,MAAM,OAAO,GAAG;AACpB,QAAQ,IAAI,CAAC,SAAS,GAAG,IAAI;AAC7B,QAAQ,OAAO,CAAC,IAAI,CAAC,oEAAoE,CAAC;AAC1F,QAAQ,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE;AAC9C,IAAI;AACJ,IAAI,MAAM,UAAU,GAAG;AACvB,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;AAC9B,QAAQ,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC;AACxE,IAAI;AACJ,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;AAChC,QAAQ,IAAI,CAAC,IAAI,CAAC,SAAS;AAC3B,YAAY,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC;AACjD,QAAQ,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;AAC5D,QAAQ,OAAO,CAAC,GAAG,CAAC,6CAA6C,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,MAAM,GAAG,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,MAAM,GAAG,OAAO,GAAG,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AACvO,IAAI;AACJ;AACA;AACA;AACA,IAAI,MAAM,YAAY,GAAG;AACzB,QAAQ,OAAO,CAAC,IAAI,CAAC,uEAAuE,CAAC;AAC7F,IAAI;AACJ;;;;;;;;;"}
|
package/dist/plugin.js
CHANGED
|
@@ -30,7 +30,13 @@ var capacitorHttpLocalServerSwifter = (function (exports, core) {
|
|
|
30
30
|
if (!this.isRunning)
|
|
31
31
|
throw new Error('Server not running');
|
|
32
32
|
const { requestId, body, status, headers } = options;
|
|
33
|
-
console.log(
|
|
33
|
+
console.log('[HttpLocalServerSwifter Web] Mock Response:', { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* No-op on web and Android — Local Network permission dialogs only exist on iOS.
|
|
37
|
+
*/
|
|
38
|
+
async openSettings() {
|
|
39
|
+
console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
|
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 * Local HTTP server plugin for Android and iOS.\n * * Allows creating an HTTP server on the device that can receive\n * requests from other devices on the same local network or\n * from the app's own WebView (fixing CORS issues).\n */\nconst HttpLocalServerSwifter = registerPlugin('HttpLocalServerSwifter', {\n // We point to the web mock for browser development\n web: () => import('./web').then(m => new m.HttpLocalServerSwifterWeb()),\n});\n// Re-export everything from definitions so they are available to the app\nexport * from './definitions';\nexport { HttpLocalServerSwifter };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HttpLocalServerSwifterWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.isRunning = false;\n }\n async connect() {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n async disconnect() {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n async sendResponse(options) {\n if (!this.isRunning)\n throw new Error('Server not running');\n const { requestId, body, status, headers } = options;\n console.log(
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Local HTTP server plugin for Android and iOS.\n * * Allows creating an HTTP server on the device that can receive\n * requests from other devices on the same local network or\n * from the app's own WebView (fixing CORS issues).\n */\nconst HttpLocalServerSwifter = registerPlugin('HttpLocalServerSwifter', {\n // We point to the web mock for browser development\n web: () => import('./web').then(m => new m.HttpLocalServerSwifterWeb()),\n});\n// Re-export everything from definitions so they are available to the app\nexport * from './definitions';\nexport { HttpLocalServerSwifter };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HttpLocalServerSwifterWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.isRunning = false;\n }\n async connect() {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n async disconnect() {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n async sendResponse(options) {\n if (!this.isRunning)\n throw new Error('Server not running');\n const { requestId, body, status, headers } = options;\n console.log('[HttpLocalServerSwifter Web] Mock Response:', { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });\n }\n /**\n * No-op on web and Android — Local Network permission dialogs only exist on iOS.\n */\n async openSettings() {\n console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;IACA;IACA;IACA;IACA;IACA;IACA;AACK,UAAC,sBAAsB,GAAGA,mBAAc,CAAC,wBAAwB,EAAE;IACxE;IACA,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,yBAAyB,EAAE,CAAC;IAC3E,CAAC;;ICTM,MAAM,yBAAyB,SAASC,cAAS,CAAC;IACzD,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,CAAC,SAAS,GAAG,IAAI;IAC7B,QAAQ,OAAO,CAAC,IAAI,CAAC,oEAAoE,CAAC;IAC1F,QAAQ,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE;IAC9C,IAAI;IACJ,IAAI,MAAM,UAAU,GAAG;IACvB,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;IAC9B,QAAQ,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC;IACxE,IAAI;IACJ,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;IAChC,QAAQ,IAAI,CAAC,IAAI,CAAC,SAAS;IAC3B,YAAY,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC;IACjD,QAAQ,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;IAC5D,QAAQ,OAAO,CAAC,GAAG,CAAC,6CAA6C,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,MAAM,GAAG,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,MAAM,GAAG,OAAO,GAAG,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IACvO,IAAI;IACJ;IACA;IACA;IACA,IAAI,MAAM,YAAY,GAAG;IACzB,QAAQ,OAAO,CAAC,IAAI,CAAC,uEAAuE,CAAC;IAC7F,IAAI;IACJ;;;;;;;;;;;;;;;"}
|
|
@@ -9,94 +9,96 @@ 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
|
-
|
|
13
|
-
// Store callbacks that expect a JSON String containing the full response object
|
|
12
|
+
|
|
14
13
|
private static var pendingResponses = [String: (String) -> Void]()
|
|
15
14
|
private static let queue = DispatchQueue(label: "com.cappitolian.HttpLocalServerSwifter.pendingResponses", qos: .userInitiated)
|
|
16
|
-
|
|
15
|
+
|
|
17
16
|
private let defaultTimeout: TimeInterval = 10.0
|
|
18
17
|
private let defaultPort: UInt16 = 8080
|
|
19
|
-
|
|
18
|
+
|
|
19
|
+
// Handles Local Network permission dialog and denial detection.
|
|
20
|
+
private let networkPermission = LocalNetworkPermission()
|
|
21
|
+
|
|
20
22
|
public init(delegate: HttpLocalServerSwifterDelegate) {
|
|
21
23
|
self.delegate = delegate
|
|
22
24
|
super.init()
|
|
23
25
|
}
|
|
24
|
-
|
|
26
|
+
|
|
25
27
|
@objc public func connect(_ call: CAPPluginCall) {
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
+
// Trigger the Local Network permission dialog before doing any network work.
|
|
29
|
+
// iOS only shows the dialog once; subsequent calls are no-ops.
|
|
30
|
+
// We fire it here so the prompt appears at a predictable moment for the user.
|
|
31
|
+
networkPermission.requestPermissionIfNeeded()
|
|
32
|
+
|
|
33
|
+
// Move execution to a background thread immediately.
|
|
34
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
35
|
+
guard let self = self else { return }
|
|
36
|
+
|
|
28
37
|
self.disconnect()
|
|
29
|
-
|
|
30
|
-
self.
|
|
31
|
-
|
|
38
|
+
let server = HttpServer()
|
|
39
|
+
self.webServer = server
|
|
40
|
+
|
|
41
|
+
// Use middleware to catch ALL requests and avoid route misses.
|
|
42
|
+
server.middleware.append { [weak self] request in
|
|
43
|
+
if request.method == "OPTIONS" {
|
|
44
|
+
return self?.corsResponse() ?? .raw(204, "No Content", nil, nil)
|
|
45
|
+
}
|
|
46
|
+
return self?.processRequest(request) ?? .raw(500, "Internal Server Error", nil, nil)
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
do {
|
|
33
|
-
try
|
|
50
|
+
try server.start(self.defaultPort, forceIPv4: true)
|
|
34
51
|
let ip = Self.getWiFiAddress() ?? "127.0.0.1"
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
|
|
53
|
+
print("🚀 SWIFTER: Server running on http://\(ip):\(self.defaultPort)")
|
|
54
|
+
|
|
37
55
|
call.resolve([
|
|
38
56
|
"ip": ip,
|
|
39
57
|
"port": Int(self.defaultPort)
|
|
40
58
|
])
|
|
41
|
-
print("🚀 Server started on \(ip):\(self.defaultPort)")
|
|
42
59
|
} catch {
|
|
43
|
-
|
|
60
|
+
print("❌ SWIFTER ERROR: \(error)")
|
|
61
|
+
|
|
62
|
+
// Check if the failure is caused by a denied Local Network permission
|
|
63
|
+
// and surface a specific error code so the JS layer can handle it distinctly.
|
|
64
|
+
if self.networkPermission.isPermissionDenied(error) {
|
|
65
|
+
call.reject("LOCAL_NETWORK_PERMISSION_DENIED")
|
|
66
|
+
} else {
|
|
67
|
+
call.reject("Could not start server")
|
|
68
|
+
}
|
|
44
69
|
}
|
|
45
70
|
}
|
|
46
71
|
}
|
|
47
|
-
|
|
72
|
+
|
|
48
73
|
@objc public func disconnect(_ call: CAPPluginCall? = nil) {
|
|
49
74
|
webServer?.stop()
|
|
50
75
|
webServer = nil
|
|
51
76
|
Self.queue.async { Self.pendingResponses.removeAll() }
|
|
52
77
|
call?.resolve()
|
|
53
78
|
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Serializes the JS response dictionary to JSON to pass it back to the processing thread
|
|
57
|
-
*/
|
|
58
|
-
static func handleJsResponse(requestId: String, responseData: [String: Any]) {
|
|
59
|
-
queue.async {
|
|
60
|
-
if let callback = pendingResponses[requestId] {
|
|
61
|
-
if let jsonData = try? JSONSerialization.data(withJSONObject: responseData),
|
|
62
|
-
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
63
|
-
callback(jsonString)
|
|
64
|
-
}
|
|
65
|
-
pendingResponses.removeValue(forKey: requestId)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private func setupHandlers() {
|
|
71
|
-
guard let server = webServer else { return }
|
|
72
|
-
|
|
73
|
-
// This closure handles EVERY request regardless of the path
|
|
74
|
-
let handler: ((HttpRequest) -> HttpResponse) = { [weak self] request in
|
|
75
|
-
if request.method == "OPTIONS" {
|
|
76
|
-
return self?.corsResponse() ?? .raw(204, "No Content", nil, nil)
|
|
77
|
-
}
|
|
78
|
-
return self?.processRequest(request) ?? .raw(500, "Internal Server Error", nil, nil)
|
|
79
|
-
}
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
// MARK: - Settings redirect
|
|
81
|
+
|
|
82
|
+
/// Opens the app's page in iOS Settings so the user can grant Local Network access.
|
|
83
|
+
/// Called from the plugin when the JS layer requests it after a permission denial.
|
|
84
|
+
@objc public func openSettings() {
|
|
85
|
+
networkPermission.openAppSettings()
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
+
|
|
88
|
+
// MARK: - Request handling
|
|
89
|
+
|
|
87
90
|
private func processRequest(_ request: HttpRequest) -> HttpResponse {
|
|
88
91
|
let requestId = UUID().uuidString
|
|
89
92
|
var responseString: String?
|
|
90
93
|
let semaphore = DispatchSemaphore(value: 0)
|
|
91
|
-
|
|
94
|
+
|
|
92
95
|
Self.queue.async {
|
|
93
96
|
Self.pendingResponses[requestId] = { jsResponse in
|
|
94
97
|
responseString = jsResponse
|
|
95
98
|
semaphore.signal()
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
|
-
|
|
99
|
-
// Map request to dictionary for TS
|
|
101
|
+
|
|
100
102
|
let requestData: [String: Any] = [
|
|
101
103
|
"requestId": requestId,
|
|
102
104
|
"method": request.method,
|
|
@@ -105,35 +107,46 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
|
|
|
105
107
|
"query": request.queryParams,
|
|
106
108
|
"body": String(bytes: request.body, encoding: .utf8) ?? ""
|
|
107
109
|
]
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
|
|
111
|
+
// notifyListeners MUST be called from the Main Thread.
|
|
112
|
+
DispatchQueue.main.async {
|
|
113
|
+
self.delegate?.httpLocalServerSwifterDidReceiveRequest(requestData)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let result = semaphore.wait(timeout: .now() + defaultTimeout)
|
|
113
117
|
|
|
114
118
|
if result == .timedOut {
|
|
115
|
-
print("⚠️ Request \(requestId) timed out waiting for JS")
|
|
116
119
|
Self.queue.async { Self.pendingResponses.removeValue(forKey: requestId) }
|
|
117
120
|
return .raw(408, "Request Timeout", nil, nil)
|
|
118
121
|
}
|
|
119
|
-
|
|
120
|
-
return createDynamicResponse(responseString ?? "
|
|
122
|
+
|
|
123
|
+
return createDynamicResponse(responseString ?? "")
|
|
121
124
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
|
|
126
|
+
static func handleJsResponse(requestId: String, responseData: [String: Any]) {
|
|
127
|
+
queue.async {
|
|
128
|
+
if let callback = pendingResponses[requestId] {
|
|
129
|
+
if let jsonData = try? JSONSerialization.data(withJSONObject: responseData),
|
|
130
|
+
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
131
|
+
callback(jsonString)
|
|
132
|
+
}
|
|
133
|
+
pendingResponses.removeValue(forKey: requestId)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// MARK: - Response helpers
|
|
139
|
+
|
|
140
|
+
private func createDynamicResponse(_ jsonResponse: String) -> HttpResponse {
|
|
141
|
+
var finalStatus = 200
|
|
128
142
|
var finalBody = jsonResponse
|
|
129
143
|
var headers: [String: String] = [
|
|
130
144
|
"Content-Type": "application/json",
|
|
131
145
|
"Access-Control-Allow-Origin": "*",
|
|
132
146
|
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
133
|
-
"Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization, X-Requested-With"
|
|
134
|
-
"Access-Control-Max-Age": "3600"
|
|
147
|
+
"Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization, X-Requested-With"
|
|
135
148
|
]
|
|
136
|
-
|
|
149
|
+
|
|
137
150
|
if let data = jsonResponse.data(using: .utf8),
|
|
138
151
|
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
139
152
|
finalBody = dict["body"] as? String ?? ""
|
|
@@ -142,21 +155,21 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
|
|
|
142
155
|
for (key, value) in customHeaders { headers[key] = value }
|
|
143
156
|
}
|
|
144
157
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return HttpResponse.raw(finalStatus, "OK", headers) { try $0.write(bodyData) }
|
|
158
|
+
|
|
159
|
+
return .raw(finalStatus, "OK", headers) { try $0.write([UInt8](finalBody.utf8)) }
|
|
148
160
|
}
|
|
149
|
-
|
|
161
|
+
|
|
150
162
|
private func corsResponse() -> HttpResponse {
|
|
151
|
-
return
|
|
163
|
+
return .raw(204, "No Content", [
|
|
152
164
|
"Access-Control-Allow-Origin": "*",
|
|
153
165
|
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
154
166
|
"Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization, X-Requested-With",
|
|
155
|
-
"Access-Control-Max-Age": "
|
|
167
|
+
"Access-Control-Max-Age": "86400"
|
|
156
168
|
], nil)
|
|
157
169
|
}
|
|
158
170
|
|
|
159
|
-
//
|
|
171
|
+
// MARK: - Network utilities
|
|
172
|
+
|
|
160
173
|
static func getWiFiAddress() -> String? {
|
|
161
174
|
var address: String?
|
|
162
175
|
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
|
@@ -164,8 +177,7 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
|
|
|
164
177
|
var ptr = ifaddr
|
|
165
178
|
while ptr != nil {
|
|
166
179
|
let interface = ptr!.pointee
|
|
167
|
-
|
|
168
|
-
if addrFamily == UInt8(AF_INET) {
|
|
180
|
+
if interface.ifa_addr.pointee.sa_family == UInt8(AF_INET) {
|
|
169
181
|
let name = String(cString: interface.ifa_name)
|
|
170
182
|
if name == "en0" {
|
|
171
183
|
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
|
@@ -179,4 +191,4 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
|
|
|
179
191
|
}
|
|
180
192
|
return address
|
|
181
193
|
}
|
|
182
|
-
}
|
|
194
|
+
}
|
|
@@ -8,37 +8,48 @@ public class HttpLocalServerSwifterPlugin: CAPPlugin, CAPBridgedPlugin, HttpLoca
|
|
|
8
8
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
9
9
|
CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise),
|
|
10
10
|
CAPPluginMethod(name: "disconnect", returnType: CAPPluginReturnPromise),
|
|
11
|
-
CAPPluginMethod(name: "sendResponse", returnType: CAPPluginReturnPromise)
|
|
11
|
+
CAPPluginMethod(name: "sendResponse", returnType: CAPPluginReturnPromise),
|
|
12
|
+
// Redirects the user to Settings so they can grant Local Network permission manually.
|
|
13
|
+
CAPPluginMethod(name: "openSettings", returnType: CAPPluginReturnPromise)
|
|
12
14
|
]
|
|
13
|
-
|
|
15
|
+
|
|
14
16
|
private var localServer: HttpLocalServerSwifter?
|
|
15
|
-
|
|
17
|
+
|
|
16
18
|
@objc func connect(_ call: CAPPluginCall) {
|
|
17
19
|
if localServer == nil { localServer = HttpLocalServerSwifter(delegate: self) }
|
|
18
20
|
localServer?.connect(call)
|
|
19
21
|
}
|
|
20
|
-
|
|
22
|
+
|
|
21
23
|
@objc func disconnect(_ call: CAPPluginCall) {
|
|
22
24
|
localServer?.disconnect(call)
|
|
23
25
|
localServer = nil
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
|
|
28
|
+
@objc func sendResponse(_ call: CAPPluginCall) {
|
|
29
|
+
guard let requestId = call.getString("requestId") else {
|
|
30
|
+
call.reject("Missing requestId")
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if let responseData = call.dictionaryRepresentation as? [String: Any] {
|
|
35
|
+
HttpLocalServerSwifter.handleJsResponse(requestId: requestId, responseData: responseData)
|
|
36
|
+
call.resolve()
|
|
37
|
+
} else {
|
|
38
|
+
call.reject("Could not parse response data")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Opens the app page in iOS Settings so the user can grant Local Network permission.
|
|
43
|
+
///
|
|
44
|
+
/// Call this from JavaScript after catching a `LOCAL_NETWORK_PERMISSION_DENIED` rejection
|
|
45
|
+
/// from `connect()`. There is no programmatic way to re-prompt the system dialog —
|
|
46
|
+
/// redirecting to Settings is the only available recovery path.
|
|
47
|
+
@objc func openSettings(_ call: CAPPluginCall) {
|
|
48
|
+
localServer?.openSettings()
|
|
49
|
+
call.resolve()
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
public func httpLocalServerSwifterDidReceiveRequest(_ data: [String: Any]) {
|
|
42
53
|
notifyListeners("onRequest", data: data)
|
|
43
54
|
}
|
|
44
|
-
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Network
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
/// Handles Local Network permission lifecycle on iOS.
|
|
6
|
+
///
|
|
7
|
+
/// iOS does not provide a direct API to query Local Network permission status.
|
|
8
|
+
/// The only available approach is:
|
|
9
|
+
/// 1. Trigger the system dialog once by starting a NWBrowser with a Bonjour service.
|
|
10
|
+
/// 2. Infer denial from DNS/network errors (kDNSServiceErr_PolicyDenied = -65570).
|
|
11
|
+
/// 3. Redirect the user to Settings if denied — re-requesting programmatically is not possible.
|
|
12
|
+
@objc public class LocalNetworkPermission: NSObject {
|
|
13
|
+
|
|
14
|
+
// MARK: - Constants
|
|
15
|
+
|
|
16
|
+
/// kDNSServiceErr_PolicyDenied — returned by the system when Local Network access is denied.
|
|
17
|
+
private static let policyDeniedErrorCode = -65570
|
|
18
|
+
|
|
19
|
+
// MARK: - Permission trigger
|
|
20
|
+
|
|
21
|
+
/// Triggers the Local Network permission dialog by briefly starting a Bonjour browser.
|
|
22
|
+
///
|
|
23
|
+
/// - Important: iOS shows this dialog only once. On subsequent calls it is a no-op.
|
|
24
|
+
/// Call this before starting the HTTP server so the dialog appears in a controlled moment,
|
|
25
|
+
/// not mid-request when the user least expects it.
|
|
26
|
+
public func requestPermissionIfNeeded() {
|
|
27
|
+
let parameters = NWParameters()
|
|
28
|
+
parameters.includePeerToPeer = true
|
|
29
|
+
|
|
30
|
+
let browser = NWBrowser(
|
|
31
|
+
for: .bonjour(type: "_http._tcp", domain: "local."),
|
|
32
|
+
using: parameters
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
browser.start(queue: .main)
|
|
36
|
+
|
|
37
|
+
// We only need to fire the dialog — cancel shortly after.
|
|
38
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
39
|
+
browser.cancel()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// MARK: - Error detection
|
|
44
|
+
|
|
45
|
+
/// Returns `true` if the given error indicates the user denied Local Network permission.
|
|
46
|
+
///
|
|
47
|
+
/// - Parameter error: Any `Error` thrown during server start or network operations.
|
|
48
|
+
public func isPermissionDenied(_ error: Error) -> Bool {
|
|
49
|
+
let nsError = error as NSError
|
|
50
|
+
return nsError.code == Self.policyDeniedErrorCode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Returns `true` if an error string representation contains known denial markers.
|
|
54
|
+
///
|
|
55
|
+
/// Useful when the original `Error` object is unavailable and only a string is at hand
|
|
56
|
+
/// (e.g. errors arriving from JavaScript or serialized logs).
|
|
57
|
+
public func isPermissionDeniedString(_ errorString: String) -> Bool {
|
|
58
|
+
let lower = errorString.lowercased()
|
|
59
|
+
return lower.contains("policydenied")
|
|
60
|
+
|| lower.contains("-65570")
|
|
61
|
+
|| lower.contains("policy denied")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// MARK: - Recovery
|
|
65
|
+
|
|
66
|
+
/// Opens the app's page in iOS Settings so the user can manually grant Local Network access.
|
|
67
|
+
///
|
|
68
|
+
/// - Note: This is the only recovery path available. iOS does not allow re-prompting
|
|
69
|
+
/// after the user denies the permission.
|
|
70
|
+
/// - Warning: On iOS 17, the user may need to restart the device after granting
|
|
71
|
+
/// the permission for it to take effect. This is a known iOS 17 bug.
|
|
72
|
+
public func openAppSettings() {
|
|
73
|
+
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
|
74
|
+
|
|
75
|
+
DispatchQueue.main.async {
|
|
76
|
+
UIApplication.shared.open(url)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
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.18",
|
|
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",
|