@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.
Files changed (2) hide show
  1. package/README.md +118 -35
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@cappitolian/http-local-server-swifter)](https://www.npmjs.com/package/@cappitolian/http-local-server-swifter)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Capacitor 8](https://img.shields.io/badge/Capacitor-8-blue)](https://capacitorjs.com/)
5
+ [![Capacitor 7](https://img.shields.io/badge/Capacitor-7-blue)](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
- <domain-config cleartextTrafficPermitted="true">
52
- <domain includeSubdomains="true">192.168.0.0/16</domain>
53
- </domain-config>
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
- No additional `Info.plist` entries are required for local HTTP servers. However, if your app uses **Bonjour/mDNS** for Network Discovery, add the following:
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
- <key>NSLocalNetworkUsageDescription</key>
71
- <string>This app uses the local network to communicate with nearby devices.</string>
72
- <key>NSBonjourServices</key>
73
- <array>
74
- <string>_http._tcp</string>
75
- </array>
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. Send a response back using the requestId
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 and recommended at the application 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 by default in the native layer. The `x-api-key` header is explicitly included in `Access-Control-Allow-Headers` on both platforms.
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
- ## API Reference
194
+ ## Architecture
165
195
 
166
- <!-- Auto-generated by @capacitor/docgen -->
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, status, headers, and body |
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 (default: 8080) |
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 ID used to correlate the response |
227
- | `method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE, OPTIONS) |
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, PUT, PATCH) |
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.35",
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",