@cappitolian/http-local-server-swifter 0.0.35 → 1.0.0
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 +118 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@cappitolian/http-local-server-swifter)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://capacitorjs.com/)
|
|
6
6
|
|
|
7
7
|
A Capacitor plugin that embeds a real HTTP server directly on your device — powered by **NanoHTTPD** on Android and **Swifter** on iOS. It allows you to receive and respond to HTTP requests from the JavaScript layer, enabling local peer-to-peer communication between devices on the same network without a cloud backend.
|
|
8
8
|
|
|
@@ -14,8 +14,10 @@ A Capacitor plugin that embeds a real HTTP server directly on your device — po
|
|
|
14
14
|
- [Platform Configuration](#platform-configuration)
|
|
15
15
|
- [Usage](#usage)
|
|
16
16
|
- [Security](#security)
|
|
17
|
+
- [Architecture](#architecture)
|
|
17
18
|
- [API Reference](#api-reference)
|
|
18
19
|
- [Platform Support](#platform-support)
|
|
20
|
+
- [Troubleshooting](#troubleshooting)
|
|
19
21
|
- [Contributing](#contributing)
|
|
20
22
|
- [Changelog](#changelog)
|
|
21
23
|
- [License](#license)
|
|
@@ -38,43 +40,60 @@ npx cap sync
|
|
|
38
40
|
Add the following permissions to your `AndroidManifest.xml`:
|
|
39
41
|
|
|
40
42
|
```xml
|
|
43
|
+
<!-- Required: bind socket and receive connections -->
|
|
41
44
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
45
|
+
<!-- Required: resolve local IP address via WifiManager -->
|
|
42
46
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
43
47
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
44
48
|
```
|
|
45
49
|
|
|
50
|
+
> **Note:** `CHANGE_WIFI_MULTICAST_STATE` is **not** needed by this plugin. It belongs to a Network Discovery plugin (mDNS/NSD).
|
|
51
|
+
|
|
46
52
|
To allow cleartext HTTP traffic on the local network, add or update your `network_security_config.xml`:
|
|
47
53
|
|
|
48
54
|
```xml
|
|
49
55
|
<!-- res/xml/network_security_config.xml -->
|
|
56
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
50
57
|
<network-security-config>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
<base-config cleartextTrafficPermitted="true">
|
|
59
|
+
<trust-anchors>
|
|
60
|
+
<certificates src="system" />
|
|
61
|
+
</trust-anchors>
|
|
62
|
+
</base-config>
|
|
63
|
+
|
|
64
|
+
<domain-config cleartextTrafficPermitted="true">
|
|
65
|
+
<domain includeSubdomains="false">localhost</domain>
|
|
66
|
+
<domain includeSubdomains="false">127.0.0.1</domain>
|
|
67
|
+
</domain-config>
|
|
54
68
|
</network-security-config>
|
|
55
69
|
```
|
|
56
70
|
|
|
57
|
-
Then reference it in `AndroidManifest.xml`:
|
|
71
|
+
Then reference it and enable cleartext in `AndroidManifest.xml`:
|
|
58
72
|
|
|
59
73
|
```xml
|
|
60
74
|
<application
|
|
61
75
|
android:networkSecurityConfig="@xml/network_security_config"
|
|
76
|
+
android:usesCleartextTraffic="true"
|
|
62
77
|
...>
|
|
63
78
|
```
|
|
64
79
|
|
|
65
80
|
### iOS
|
|
66
81
|
|
|
67
|
-
|
|
82
|
+
Add the following to your `Info.plist` to allow your app to serve and receive HTTP traffic on the local network:
|
|
68
83
|
|
|
69
84
|
```xml
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
<
|
|
75
|
-
</
|
|
85
|
+
<!-- Required: allow cleartext HTTP from/to the local server -->
|
|
86
|
+
<key>NSAppTransportSecurity</key>
|
|
87
|
+
<dict>
|
|
88
|
+
<key>NSAllowsLocalNetworking</key>
|
|
89
|
+
<true/>
|
|
90
|
+
<key>NSAllowsArbitraryLoads</key>
|
|
91
|
+
<true/>
|
|
92
|
+
</dict>
|
|
76
93
|
```
|
|
77
94
|
|
|
95
|
+
> **Note:** `NSLocalNetworkUsageDescription` and `NSBonjourServices` are **not** required by this plugin. Those entries belong to a Network Discovery plugin (mDNS/Bonjour). Do not add them here unless you are also using that plugin.
|
|
96
|
+
|
|
78
97
|
---
|
|
79
98
|
|
|
80
99
|
## Usage
|
|
@@ -88,14 +107,14 @@ import { HttpLocalServerSwifter } from '@cappitolian/http-local-server-swifter';
|
|
|
88
107
|
### Start the server and listen for requests
|
|
89
108
|
|
|
90
109
|
```typescript
|
|
91
|
-
// 1. Register the request listener
|
|
110
|
+
// 1. Register the request listener BEFORE connecting
|
|
92
111
|
await HttpLocalServerSwifter.addListener('onRequest', async (data) => {
|
|
93
112
|
console.log(`${data.method} ${data.path}`);
|
|
94
113
|
console.log('Headers:', data.headers);
|
|
95
114
|
console.log('Body:', data.body);
|
|
96
115
|
console.log('Query:', data.query);
|
|
97
116
|
|
|
98
|
-
// 2.
|
|
117
|
+
// 2. Always send a response — the server blocks the native thread until you do
|
|
99
118
|
await HttpLocalServerSwifter.sendResponse({
|
|
100
119
|
requestId: data.requestId,
|
|
101
120
|
status: 200,
|
|
@@ -111,6 +130,8 @@ const { ip, port } = await HttpLocalServerSwifter.connect();
|
|
|
111
130
|
console.log(`Server running at http://${ip}:${port}`);
|
|
112
131
|
```
|
|
113
132
|
|
|
133
|
+
> ⚠️ **Always call `sendResponse`** for every request. On Android, the native thread is blocked via `CompletableFuture.get()` until the response arrives or the timeout elapses. On iOS, a `DispatchSemaphore` is held open. Failing to respond will cause the request to time out with `408 Request Timeout` (iOS) or a JSON timeout error (Android) after the configured timeout (default: **10s on iOS**, **5s on Android**).
|
|
134
|
+
|
|
114
135
|
### Stop the server
|
|
115
136
|
|
|
116
137
|
```typescript
|
|
@@ -121,16 +142,16 @@ await HttpLocalServerSwifter.disconnect();
|
|
|
121
142
|
|
|
122
143
|
## Security
|
|
123
144
|
|
|
124
|
-
This plugin runs an HTTP server on the local network. The following mechanisms are built into the native layer
|
|
145
|
+
This plugin runs an HTTP server on the local network. The following mechanisms are built into the native layer.
|
|
125
146
|
|
|
126
147
|
### Rate Limiting (Native — Android & iOS)
|
|
127
148
|
|
|
128
149
|
IP-based rate limiting is enforced natively before requests reach TypeScript, protecting the server against denial-of-service (DoS) attacks.
|
|
129
150
|
|
|
130
|
-
| Platform | Limit | Window |
|
|
131
|
-
|
|
132
|
-
| Android | 30 requests | 60 seconds |
|
|
133
|
-
| iOS | 30 requests | 60 seconds |
|
|
151
|
+
| Platform | Limit | Window | Client IP source |
|
|
152
|
+
|---|---|---|---|
|
|
153
|
+
| Android | 30 requests | 60 seconds | `http-client-ip` → `remote-addr` header |
|
|
154
|
+
| iOS | 30 requests | 60 seconds | `x-forwarded-for` → `request.address` |
|
|
134
155
|
|
|
135
156
|
Requests exceeding the limit receive a `429 Too Many Requests` response automatically:
|
|
136
157
|
|
|
@@ -153,23 +174,61 @@ Server generates key → Client calls /pair → Client stores key
|
|
|
153
174
|
All requests: x-api-key: <key> → Server validates → 401 if invalid
|
|
154
175
|
```
|
|
155
176
|
|
|
156
|
-
> ⚠️ Since this plugin operates over HTTP, the API key is transmitted in plaintext. For sensitive environments, consider implementing HMAC request signing with short-lived timestamps to prevent replay attacks.
|
|
177
|
+
> ⚠️ Since this plugin operates over HTTP (cleartext), the API key is transmitted in plaintext. For sensitive environments, consider implementing HMAC request signing with short-lived timestamps to prevent replay attacks. The `x-signature` and `x-timestamp` headers are already allowed by the built-in CORS configuration on both platforms.
|
|
157
178
|
|
|
158
179
|
### CORS
|
|
159
180
|
|
|
160
|
-
CORS headers are set
|
|
181
|
+
CORS headers are set natively on every response. The following headers are allowed by default on both platforms:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
Access-Control-Allow-Origin: *
|
|
185
|
+
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
|
|
186
|
+
Access-Control-Allow-Headers: Origin, Content-Type, Accept, Authorization,
|
|
187
|
+
X-Requested-With, x-api-key, x-signature, x-timestamp
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Custom headers returned via `sendResponse` are merged on top of these defaults and can override them.
|
|
161
191
|
|
|
162
192
|
---
|
|
163
193
|
|
|
164
|
-
##
|
|
194
|
+
## Architecture
|
|
165
195
|
|
|
166
|
-
|
|
196
|
+
### Request / Response Bridge
|
|
197
|
+
|
|
198
|
+
The plugin uses a **request ID bridge** pattern to cross the native ↔ JavaScript boundary asynchronously:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
Incoming HTTP request
|
|
202
|
+
↓
|
|
203
|
+
Native layer generates requestId → fires onRequest event to JS
|
|
204
|
+
↓
|
|
205
|
+
JS handler processes logic → calls sendResponse({ requestId, ... })
|
|
206
|
+
↓
|
|
207
|
+
Native layer resolves the pending future/semaphore → returns HTTP response
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
| Platform | Blocking mechanism | Default timeout |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| Android | `CompletableFuture.get(timeout, SECONDS)` | 5 seconds |
|
|
213
|
+
| iOS | `DispatchSemaphore.wait(timeout:)` | 10 seconds |
|
|
214
|
+
|
|
215
|
+
Pending responses are stored in a thread-safe map (`ConcurrentHashMap` on Android, `DispatchQueue`-protected `Dictionary` on iOS) and cleaned up on timeout or `disconnect`.
|
|
216
|
+
|
|
217
|
+
### Connection Behavior
|
|
218
|
+
|
|
219
|
+
- **Android (NanoHTTPD):** Each request is handled in its own thread. The server forces `Connection: close` on every response to prevent keep-alive issues under rapid sequential requests (`ERR_INVALID_HTTP_RESPONSE`).
|
|
220
|
+
- **iOS (Swifter):** Requests are processed via a middleware chain on a global background queue. `notifyListeners` (Capacitor event bridge) is always dispatched to the main thread.
|
|
221
|
+
- **IP resolution:** Both platforms resolve the local WiFi IP via `WifiManager` (Android) / `getifaddrs en0` (iOS), falling back to `127.0.0.1` if unavailable.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## API Reference
|
|
167
226
|
|
|
168
227
|
### Methods
|
|
169
228
|
|
|
170
229
|
#### `connect() => Promise<HttpConnectResult>`
|
|
171
230
|
|
|
172
|
-
Starts the HTTP server and begins listening for incoming requests
|
|
231
|
+
Starts the HTTP server and begins listening for incoming requests on port `8080`.
|
|
173
232
|
|
|
174
233
|
**Returns:** `Promise<HttpConnectResult>`
|
|
175
234
|
|
|
@@ -177,17 +236,17 @@ Starts the HTTP server and begins listening for incoming requests.
|
|
|
177
236
|
|
|
178
237
|
#### `disconnect() => Promise<void>`
|
|
179
238
|
|
|
180
|
-
Stops the HTTP server and releases all resources.
|
|
239
|
+
Stops the HTTP server, drains all pending response futures/semaphores, and releases all resources.
|
|
181
240
|
|
|
182
241
|
---
|
|
183
242
|
|
|
184
243
|
#### `sendResponse(options: HttpSendResponseOptions) => Promise<void>`
|
|
185
244
|
|
|
186
|
-
Sends an HTTP response back to the client for a given request.
|
|
245
|
+
Sends an HTTP response back to the client for a given request. Must be called for every `onRequest` event received.
|
|
187
246
|
|
|
188
247
|
| Param | Type | Description |
|
|
189
248
|
|---|---|---|
|
|
190
|
-
| `options` | `HttpSendResponseOptions` | Response options including requestId
|
|
249
|
+
| `options` | `HttpSendResponseOptions` | Response options including `requestId`, `status`, `headers`, and `body` |
|
|
191
250
|
|
|
192
251
|
---
|
|
193
252
|
|
|
@@ -214,8 +273,8 @@ Removes all registered listeners.
|
|
|
214
273
|
|
|
215
274
|
| Property | Type | Description |
|
|
216
275
|
|---|---|---|
|
|
217
|
-
| `ip` | `string` | Local IP address where the server is bound |
|
|
218
|
-
| `port` | `number` | Port the server is listening on (
|
|
276
|
+
| `ip` | `string` | Local WiFi IP address where the server is bound (`127.0.0.1` fallback) |
|
|
277
|
+
| `port` | `number` | Port the server is listening on (fixed: `8080`) |
|
|
219
278
|
|
|
220
279
|
---
|
|
221
280
|
|
|
@@ -223,12 +282,12 @@ Removes all registered listeners.
|
|
|
223
282
|
|
|
224
283
|
| Property | Type | Description |
|
|
225
284
|
|---|---|---|
|
|
226
|
-
| `requestId` | `string` | Unique
|
|
227
|
-
| `method` | `string` | HTTP method (GET
|
|
285
|
+
| `requestId` | `string` | Unique UUID used to correlate the response — **required in `sendResponse`** |
|
|
286
|
+
| `method` | `string` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`) |
|
|
228
287
|
| `path` | `string` | Request path (e.g. `/menu`, `/orders/123`) |
|
|
229
288
|
| `headers` | `Record<string, string>` | Request headers |
|
|
230
289
|
| `query` | `Record<string, string>` | Query string parameters |
|
|
231
|
-
| `body` | `string \| null` | Raw request body (for POST
|
|
290
|
+
| `body` | `string \| null` | Raw request body (present for `POST`, `PUT`, `PATCH`) |
|
|
232
291
|
|
|
233
292
|
---
|
|
234
293
|
|
|
@@ -238,8 +297,8 @@ Removes all registered listeners.
|
|
|
238
297
|
|---|---|---|---|
|
|
239
298
|
| `requestId` | `string` | ✅ | ID of the request to respond to |
|
|
240
299
|
| `status` | `number` | ❌ | HTTP status code (default: `200`) |
|
|
241
|
-
| `headers` | `Record<string, string>` | ❌ | Custom response headers |
|
|
242
|
-
| `body` | `string` | ❌ | Response body |
|
|
300
|
+
| `headers` | `Record<string, string>` | ❌ | Custom response headers (merged with CORS defaults) |
|
|
301
|
+
| `body` | `string` | ❌ | Response body string |
|
|
243
302
|
|
|
244
303
|
---
|
|
245
304
|
|
|
@@ -253,9 +312,33 @@ Removes all registered listeners.
|
|
|
253
312
|
| Custom status codes | ✅ | ✅ | ✅ (mock) |
|
|
254
313
|
| Custom response headers | ✅ | ✅ | ✅ (mock) |
|
|
255
314
|
| Request headers forwarding | ✅ | ✅ | ✅ (mock) |
|
|
256
|
-
| Dynamic routing | ✅ | ✅ | ❌ |
|
|
257
315
|
| IP-based rate limiting | ✅ | ✅ | ❌ |
|
|
258
316
|
| CORS preflight handling | ✅ | ✅ | ❌ |
|
|
317
|
+
| `Connection: close` enforcement | ✅ | ❌ | ❌ |
|
|
318
|
+
| Request body parsing (POST/PUT/PATCH) | ✅ | ✅ | ✅ (mock) |
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Troubleshooting
|
|
323
|
+
|
|
324
|
+
### Requests timing out on the client side
|
|
325
|
+
|
|
326
|
+
The server resolves the native thread synchronously. If your JS handler throws before calling `sendResponse`, the request will hang until the native timeout fires. Always wrap your handler in `try/catch` and call `sendResponse` in both branches.
|
|
327
|
+
|
|
328
|
+
### `ERR_INVALID_HTTP_RESPONSE` on Android
|
|
329
|
+
|
|
330
|
+
NanoHTTPD does not handle keep-alive correctly under rapid sequential requests. The plugin forces `Connection: close` on every response. If you are still seeing this error, ensure you are on the latest version.
|
|
331
|
+
|
|
332
|
+
### Server returns `127.0.0.1` instead of the LAN IP
|
|
333
|
+
|
|
334
|
+
This happens when WiFi is disconnected or the `WifiManager` / `en0` interface returns no address. Verify the device is connected to WiFi before calling `connect()`.
|
|
335
|
+
|
|
336
|
+
### iOS server not reachable from Android
|
|
337
|
+
|
|
338
|
+
Ensure:
|
|
339
|
+
1. Both devices are on the **same WiFi network**
|
|
340
|
+
2. `NSAllowsLocalNetworking` and `NSAllowsArbitraryLoads` are set in `Info.plist`
|
|
341
|
+
3. The client is using the IP returned by `connect()`, not `localhost`
|
|
259
342
|
|
|
260
343
|
---
|
|
261
344
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cappitolian/http-local-server-swifter",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|