@cappitolian/http-local-server 0.0.8 → 0.0.10

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 CHANGED
@@ -34,13 +34,19 @@ import { HttpLocalServer } from '@cappitolian/http-local-server';
34
34
 
35
35
  ```typescript
36
36
  // 1. Set up the listener for incoming requests
37
- HttpLocalServer.addListener('onRequest', async (request) => {
38
- console.log('Request received:', request.path, request.body);
37
+ await HttpLocalServer.addListener('onRequest', async (data) => {
38
+ console.log(`${data.method} ${data.path}`);
39
+ console.log('Body:', data.body);
40
+ console.log('Headers:', data.headers);
41
+ console.log('Query:', data.query);
39
42
 
40
43
  // 2. Send a response back to the client using the requestId
41
44
  await HttpLocalServer.sendResponse({
42
- requestId: request.requestId,
43
- body: JSON.stringify({ message: "Hello from Ionic!" })
45
+ requestId: data.requestId,
46
+ body: JSON.stringify({
47
+ success: true,
48
+ message: 'Request processed!'
49
+ })
44
50
  });
45
51
  });
46
52
 
@@ -53,7 +59,8 @@ HttpLocalServer.connect().then(result => {
53
59
  ### Stop Server
54
60
 
55
61
  ```typescript
56
- HttpLocalServer.disconnect()
62
+ // 4. Stop the server
63
+ await HttpLocalServer.disconnect();
57
64
  ```
58
65
 
59
66
  ---
@@ -25,167 +25,365 @@
25
25
  package com.cappitolian.plugins.httplocalservice;
26
26
 
27
27
  import android.content.Context;
28
+ import android.net.wifi.WifiInfo;
28
29
  import android.net.wifi.WifiManager;
29
30
  import android.text.format.Formatter;
31
+ import android.util.Log;
32
+
33
+ import androidx.annotation.NonNull;
34
+ import androidx.annotation.Nullable;
35
+
30
36
  import com.getcapacitor.JSObject;
31
37
  import com.getcapacitor.Plugin;
32
38
  import com.getcapacitor.PluginCall;
33
- import fi.iki.elonen.NanoHTTPD;
39
+
40
+ import org.json.JSONException;
41
+ import org.json.JSONObject;
42
+
34
43
  import java.io.IOException;
35
- import java.util.UUID;
36
- import java.util.concurrent.*;
37
44
  import java.util.HashMap;
38
45
  import java.util.Map;
46
+ import java.util.UUID;
47
+ import java.util.concurrent.CompletableFuture;
48
+ import java.util.concurrent.ConcurrentHashMap;
49
+ import java.util.concurrent.TimeUnit;
50
+ import java.util.concurrent.TimeoutException;
51
+
52
+ import fi.iki.elonen.NanoHTTPD;
39
53
 
54
+ /**
55
+ * Local HTTP server implementation for Android using NanoHTTPD.
56
+ * Handles incoming HTTP requests and communicates with JavaScript layer.
57
+ */
40
58
  public class HttpLocalServer {
59
+
60
+ // MARK: - Constants
61
+ private static final String TAG = "HttpLocalServer";
62
+ private static final int DEFAULT_PORT = 8080;
63
+ private static final int DEFAULT_TIMEOUT_SECONDS = 5;
64
+ private static final String FALLBACK_IP = "127.0.0.1";
65
+
66
+ // MARK: - Properties
41
67
  private LocalNanoServer server;
42
- private String localIp;
43
- private int port = 8080;
44
- private Plugin plugin;
45
-
46
- // Map to wait for responses from JS (key: requestId)
68
+ private final Plugin plugin;
69
+ private final int port;
70
+ private final int timeoutSeconds;
71
+
47
72
  private static final ConcurrentHashMap<String, CompletableFuture<String>> pendingResponses = new ConcurrentHashMap<>();
48
-
49
- public HttpLocalServer(Plugin plugin) {
73
+
74
+ // MARK: - Constructor
75
+ public HttpLocalServer(@NonNull Plugin plugin) {
76
+ this(plugin, DEFAULT_PORT, DEFAULT_TIMEOUT_SECONDS);
77
+ }
78
+
79
+ public HttpLocalServer(@NonNull Plugin plugin, int port, int timeoutSeconds) {
50
80
  this.plugin = plugin;
81
+ this.port = port;
82
+ this.timeoutSeconds = timeoutSeconds;
51
83
  }
52
-
53
- public void connect(PluginCall call) {
54
- if (server == null) {
55
- localIp = getLocalIpAddress(plugin.getContext());
56
- server = new LocalNanoServer(localIp, port, plugin);
57
- try {
58
- server.start();
59
- JSObject ret = new JSObject();
60
- ret.put("ip", localIp);
61
- ret.put("port", port);
62
- call.resolve(ret);
63
- } catch (Exception e) {
64
- call.reject("Failed to start server: " + e.getMessage());
65
- }
66
- } else {
84
+
85
+ // MARK: - Public Methods
86
+ public void connect(@NonNull PluginCall call) {
87
+ if (server != null && server.isAlive()) {
67
88
  call.reject("Server is already running");
89
+ return;
90
+ }
91
+
92
+ try {
93
+ String localIp = getLocalIpAddress(plugin.getContext());
94
+ server = new LocalNanoServer(localIp, port, plugin, timeoutSeconds);
95
+ server.start();
96
+
97
+ JSObject response = new JSObject();
98
+ response.put("ip", localIp);
99
+ response.put("port", port);
100
+ call.resolve(response);
101
+
102
+ Log.i(TAG, "Server started at " + localIp + ":" + port);
103
+ } catch (IOException e) {
104
+ Log.e(TAG, "Failed to start server", e);
105
+ call.reject("Failed to start server: " + e.getMessage());
68
106
  }
69
107
  }
70
-
71
- public void disconnect(PluginCall call) {
108
+
109
+ public void disconnect(@Nullable PluginCall call) {
72
110
  if (server != null) {
73
111
  server.stop();
74
112
  server = null;
113
+
114
+ // Clear all pending responses
115
+ pendingResponses.clear();
116
+
117
+ Log.i(TAG, "Server stopped");
118
+ }
119
+
120
+ if (call != null) {
121
+ call.resolve();
75
122
  }
76
- call.resolve();
77
123
  }
78
-
79
- // Called by plugin when JS responds
80
- public static void handleJsResponse(String requestId, String body) {
124
+
125
+ // MARK: - Static Methods
126
+ /**
127
+ * Called by plugin when JavaScript responds to a request
128
+ */
129
+ public static void handleJsResponse(@NonNull String requestId, @NonNull String body) {
81
130
  CompletableFuture<String> future = pendingResponses.remove(requestId);
82
- if (future != null) {
131
+ if (future != null && !future.isDone()) {
83
132
  future.complete(body);
133
+ Log.d(TAG, "Response received for request: " + requestId);
134
+ } else {
135
+ Log.w(TAG, "No pending request found for ID: " + requestId);
84
136
  }
85
137
  }
86
-
87
- // Helper to get local WiFi IP Address
88
- private String getLocalIpAddress(Context context) {
89
- WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
90
- if (wm != null && wm.getConnectionInfo() != null) {
91
- return Formatter.formatIpAddress(wm.getConnectionInfo().getIpAddress());
138
+
139
+ // MARK: - Private Methods
140
+ /**
141
+ * Get the local WiFi IP address
142
+ */
143
+ @NonNull
144
+ private String getLocalIpAddress(@NonNull Context context) {
145
+ try {
146
+ WifiManager wifiManager = (WifiManager) context.getApplicationContext()
147
+ .getSystemService(Context.WIFI_SERVICE);
148
+
149
+ if (wifiManager == null) {
150
+ Log.w(TAG, "WifiManager is null, using fallback IP");
151
+ return FALLBACK_IP;
152
+ }
153
+
154
+ WifiInfo wifiInfo = wifiManager.getConnectionInfo();
155
+ if (wifiInfo == null) {
156
+ Log.w(TAG, "WifiInfo is null, using fallback IP");
157
+ return FALLBACK_IP;
158
+ }
159
+
160
+ int ipAddress = wifiInfo.getIpAddress();
161
+ if (ipAddress == 0) {
162
+ Log.w(TAG, "IP address is 0, using fallback IP");
163
+ return FALLBACK_IP;
164
+ }
165
+
166
+ return Formatter.formatIpAddress(ipAddress);
167
+ } catch (Exception e) {
168
+ Log.e(TAG, "Error getting IP address", e);
169
+ return FALLBACK_IP;
92
170
  }
93
- return "127.0.0.1"; // fallback
94
171
  }
95
-
172
+
173
+ // MARK: - Inner Class: LocalNanoServer
174
+ /**
175
+ * NanoHTTPD server implementation that handles HTTP requests
176
+ */
96
177
  private static class LocalNanoServer extends NanoHTTPD {
97
- private Plugin plugin;
98
-
99
- public LocalNanoServer(String hostname, int port, Plugin plugin) {
178
+
179
+ private final Plugin plugin;
180
+ private final int timeoutSeconds;
181
+
182
+ public LocalNanoServer(@NonNull String hostname, int port, @NonNull Plugin plugin, int timeoutSeconds) {
100
183
  super(hostname, port);
101
184
  this.plugin = plugin;
185
+ this.timeoutSeconds = timeoutSeconds;
102
186
  }
103
-
187
+
104
188
  @Override
105
- public Response serve(IHTTPSession session) {
106
- String path = session.getUri();
189
+ public Response serve(@NonNull IHTTPSession session) {
107
190
  String method = session.getMethod().name();
108
-
109
- // Read body if needed
110
- String body = "";
111
- if (session.getMethod() == Method.POST || session.getMethod() == Method.PUT) {
112
- try {
113
- // Crear un mapa para almacenar los datos parseados
114
- HashMap<String, String> files = new HashMap<>();
115
- session.parseBody(files);
116
-
117
- // El body viene en el mapa con la clave "postData"
118
- body = files.get("postData");
119
-
120
- // Si postData es null, intentar obtener de los parámetros (para form-data)
121
- if (body == null || body.isEmpty()) {
122
- // Para application/x-www-form-urlencoded
123
- body = session.getQueryParameterString();
124
- }
125
-
126
- // Log para debug
127
- System.out.println("Body received: " + body);
128
-
129
- } catch (Exception e) {
130
- System.err.println("Error parsing body: " + e.getMessage());
131
- e.printStackTrace();
191
+ String path = session.getUri();
192
+
193
+ // Handle CORS preflight
194
+ if (Method.OPTIONS.equals(session.getMethod())) {
195
+ return createCorsResponse();
196
+ }
197
+
198
+ try {
199
+ // Extract request data
200
+ String body = extractBody(session);
201
+ Map<String, String> headers = session.getHeaders();
202
+ Map<String, String> params = session.getParms();
203
+
204
+ // Process request
205
+ String responseBody = processRequest(method, path, body, headers, params);
206
+
207
+ return createJsonResponse(responseBody, Response.Status.OK);
208
+ } catch (Exception e) {
209
+ Log.e(TAG, "Error processing request", e);
210
+ return createErrorResponse("Internal server error: " + e.getMessage(),
211
+ Response.Status.INTERNAL_ERROR);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Extract body from POST/PUT/PATCH requests
217
+ */
218
+ @Nullable
219
+ private String extractBody(@NonNull IHTTPSession session) {
220
+ Method method = session.getMethod();
221
+
222
+ if (method != Method.POST && method != Method.PUT && method != Method.PATCH) {
223
+ return null;
224
+ }
225
+
226
+ try {
227
+ HashMap<String, String> files = new HashMap<>();
228
+ session.parseBody(files);
229
+
230
+ // Body comes in the map with key "postData"
231
+ String body = files.get("postData");
232
+
233
+ // Fallback to query parameters for form-data
234
+ if (body == null || body.isEmpty()) {
235
+ body = session.getQueryParameterString();
132
236
  }
237
+
238
+ Log.d(TAG, "Body received (" + body.length() + " bytes): " +
239
+ (body != null ? body.substring(0, Math.min(body.length(), 100)) : "null"));
240
+
241
+ return body;
242
+ } catch (IOException | ResponseException e) {
243
+ Log.e(TAG, "Error parsing body", e);
244
+ return null;
133
245
  }
134
-
135
- // Generate a unique requestId
246
+ }
247
+
248
+ /**
249
+ * Process the request and wait for JavaScript response
250
+ */
251
+ @NonNull
252
+ private String processRequest(@NonNull String method, @NonNull String path,
253
+ @Nullable String body, @NonNull Map<String, String> headers,
254
+ @NonNull Map<String, String> params) {
255
+
136
256
  String requestId = UUID.randomUUID().toString();
137
-
138
- // Prepare data for JS
139
- JSObject req = new JSObject();
140
- req.put("requestId", requestId);
141
- req.put("method", method);
142
- req.put("path", path);
143
- req.put("body", body);
144
- req.put("headers", getHeadersAsJson(session)); // Opcional: para debug
145
-
146
- // Future to wait for JS response
257
+
258
+ // Build request data for JavaScript
259
+ JSObject requestData = new JSObject();
260
+ requestData.put("requestId", requestId);
261
+ requestData.put("method", method);
262
+ requestData.put("path", path);
263
+
264
+ if (body != null && !body.isEmpty()) {
265
+ requestData.put("body", body);
266
+ }
267
+
268
+ if (!headers.isEmpty()) {
269
+ requestData.put("headers", mapToJson(headers));
270
+ }
271
+
272
+ if (!params.isEmpty()) {
273
+ requestData.put("query", mapToJson(params));
274
+ }
275
+
276
+ // Create future for response
147
277
  CompletableFuture<String> future = new CompletableFuture<>();
148
278
  pendingResponses.put(requestId, future);
149
-
150
- // Send event to JS
151
- if (plugin instanceof com.cappitolian.plugins.httplocalservice.HttpLocalServerPlugin) {
152
- ((com.cappitolian.plugins.httplocalservice.HttpLocalServerPlugin) plugin).fireOnRequest(req);
279
+
280
+ // Notify plugin
281
+ if (plugin instanceof HttpLocalServerPlugin) {
282
+ ((HttpLocalServerPlugin) plugin).fireOnRequest(requestData);
283
+ } else {
284
+ Log.e(TAG, "Plugin is not instance of HttpLocalServerPlugin");
153
285
  }
154
-
155
- String jsResponse = null;
286
+
287
+ // Wait for JavaScript response
156
288
  try {
157
- // Wait up to 55 seconds for JS response
158
- jsResponse = future.get(55, TimeUnit.SECONDS);
289
+ String response = future.get(timeoutSeconds, TimeUnit.SECONDS);
290
+ Log.d(TAG, "Response received for request: " + requestId);
291
+ return response;
159
292
  } catch (TimeoutException e) {
160
- jsResponse = "{\"error\": \"Timeout waiting for JS response\"}";
293
+ Log.w(TAG, "Timeout waiting for response: " + requestId);
294
+ return createTimeoutError(requestId);
161
295
  } catch (Exception e) {
162
- jsResponse = "{\"error\": \"Error waiting for JS response\"}";
296
+ Log.e(TAG, "Error waiting for response: " + requestId, e);
297
+ return createGenericError("Error waiting for response");
163
298
  } finally {
164
299
  pendingResponses.remove(requestId);
165
300
  }
166
-
167
- Response response = newFixedLengthResponse(Response.Status.OK, "application/json", jsResponse);
301
+ }
302
+
303
+ /**
304
+ * Convert Map to JSObject
305
+ */
306
+ @NonNull
307
+ private JSObject mapToJson(@NonNull Map<String, String> map) {
308
+ JSObject json = new JSObject();
309
+ for (Map.Entry<String, String> entry : map.entrySet()) {
310
+ json.put(entry.getKey(), entry.getValue());
311
+ }
312
+ return json;
313
+ }
314
+
315
+ /**
316
+ * Create JSON response with CORS headers
317
+ */
318
+ @NonNull
319
+ private Response createJsonResponse(@NonNull String body, @NonNull Response.Status status) {
320
+ Response response = newFixedLengthResponse(status, "application/json", body);
168
321
  addCorsHeaders(response);
169
322
  return response;
170
323
  }
171
-
172
- // Método auxiliar para obtener headers como JSObject
173
- private JSObject getHeadersAsJson(IHTTPSession session) {
174
- JSObject headers = new JSObject();
324
+
325
+ /**
326
+ * Create CORS preflight response
327
+ */
328
+ @NonNull
329
+ private Response createCorsResponse() {
330
+ Response response = newFixedLengthResponse(Response.Status.NO_CONTENT,
331
+ "text/plain", "");
332
+ addCorsHeaders(response);
333
+ return response;
334
+ }
335
+
336
+ /**
337
+ * Create error response
338
+ */
339
+ @NonNull
340
+ private Response createErrorResponse(@NonNull String message, @NonNull Response.Status status) {
175
341
  try {
176
- for (Map.Entry<String, String> entry : session.getHeaders().entrySet()) {
177
- headers.put(entry.getKey(), entry.getValue());
178
- }
179
- } catch (Exception e) {
180
- // ignore
342
+ JSONObject error = new JSONObject();
343
+ error.put("error", message);
344
+ return createJsonResponse(error.toString(), status);
345
+ } catch (JSONException e) {
346
+ return newFixedLengthResponse(status, "text/plain", message);
181
347
  }
182
- return headers;
183
348
  }
184
-
185
- private void addCorsHeaders(Response response) {
349
+
350
+ /**
351
+ * Create timeout error JSON
352
+ */
353
+ @NonNull
354
+ private String createTimeoutError(@NonNull String requestId) {
355
+ try {
356
+ JSONObject error = new JSONObject();
357
+ error.put("error", "Request timeout");
358
+ error.put("requestId", requestId);
359
+ return error.toString();
360
+ } catch (JSONException e) {
361
+ return "{\"error\":\"Request timeout\"}";
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Create generic error JSON
367
+ */
368
+ @NonNull
369
+ private String createGenericError(@NonNull String message) {
370
+ try {
371
+ JSONObject error = new JSONObject();
372
+ error.put("error", message);
373
+ return error.toString();
374
+ } catch (JSONException e) {
375
+ return "{\"error\":\"" + message + "\"}";
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Add CORS headers to response
381
+ */
382
+ private void addCorsHeaders(@NonNull Response response) {
186
383
  response.addHeader("Access-Control-Allow-Origin", "*");
187
- response.addHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
188
- response.addHeader("Access-Control-Allow-Headers", "origin, content-type, accept, authorization");
384
+ response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
385
+ response.addHeader("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization");
386
+ response.addHeader("Access-Control-Allow-Credentials", "true");
189
387
  response.addHeader("Access-Control-Max-Age", "3600");
190
388
  }
191
389
  }
@@ -46,16 +46,34 @@
46
46
 
47
47
  package com.cappitolian.plugins.httplocalservice;
48
48
 
49
- import com.getcapacitor.*;
49
+ import android.util.Log;
50
+
51
+ import androidx.annotation.NonNull;
52
+
53
+ import com.getcapacitor.JSObject;
54
+ import com.getcapacitor.Plugin;
55
+ import com.getcapacitor.PluginCall;
56
+ import com.getcapacitor.PluginMethod;
50
57
  import com.getcapacitor.annotation.CapacitorPlugin;
51
- import org.json.JSONException;
52
- import org.json.JSONObject;
53
58
 
54
59
  @CapacitorPlugin(name = "HttpLocalServer")
55
60
  public class HttpLocalServerPlugin extends Plugin {
56
-
61
+
62
+ private static final String TAG = "HttpLocalServerPlugin";
57
63
  private HttpLocalServer localServer;
58
64
 
65
+ @Override
66
+ public void load() {
67
+ super.load();
68
+ // Inicializar el servidor con configuración por defecto
69
+ localServer = new HttpLocalServer(this);
70
+
71
+ // O con configuración personalizada:
72
+ // localServer = new HttpLocalServer(this, 8080, 5); // puerto y timeout
73
+
74
+ Log.d(TAG, "Plugin loaded");
75
+ }
76
+
59
77
  @PluginMethod
60
78
  public void connect(PluginCall call) {
61
79
  if (localServer == null) {
@@ -64,11 +82,6 @@ public class HttpLocalServerPlugin extends Plugin {
64
82
  localServer.connect(call);
65
83
  }
66
84
 
67
- // Add this method:
68
- public void fireOnRequest(JSObject req) {
69
- notifyListeners("onRequest", req, true);
70
- }
71
-
72
85
  @PluginMethod
73
86
  public void disconnect(PluginCall call) {
74
87
  if (localServer != null) {
@@ -82,11 +95,34 @@ public class HttpLocalServerPlugin extends Plugin {
82
95
  public void sendResponse(PluginCall call) {
83
96
  String requestId = call.getString("requestId");
84
97
  String body = call.getString("body");
85
- if (requestId == null || body == null) {
86
- call.reject("Missing requestId or body");
98
+
99
+ if (requestId == null || requestId.isEmpty()) {
100
+ call.reject("Missing requestId");
87
101
  return;
88
102
  }
103
+
104
+ if (body == null || body.isEmpty()) {
105
+ call.reject("Missing body");
106
+ return;
107
+ }
108
+
89
109
  HttpLocalServer.handleJsResponse(requestId, body);
90
110
  call.resolve();
91
111
  }
112
+
113
+ /**
114
+ * Called by HttpLocalServer to notify JavaScript of incoming requests
115
+ */
116
+ public void fireOnRequest(@NonNull JSObject data) {
117
+ notifyListeners("onRequest", data);
118
+ }
119
+
120
+ @Override
121
+ protected void handleOnDestroy() {
122
+ if (localServer != null) {
123
+ localServer.disconnect(null);
124
+ localServer = null;
125
+ }
126
+ super.handleOnDestroy();
127
+ }
92
128
  }