expo-http-server 0.1.14-beta.4 → 0.1.14

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
@@ -96,6 +96,18 @@ export default function App() {
96
96
 
97
97
  ```
98
98
 
99
+ ## Options
100
+
101
+ `setup` accepts an optional third argument:
102
+
103
+ ```ts
104
+ server.setup(9666, onStatusUpdate, { loopbackOnly: true });
105
+ ```
106
+
107
+ | Option | Default | Description |
108
+ | --- | --- | --- |
109
+ | `loopbackOnly` | `false` | Only accept connections from the device itself (`127.0.0.1`), so other devices on the network can't reach the server. |
110
+
99
111
  ## Running in the background
100
112
  **iOS**: When the app is backgrounded the server will inevitably get paused. There is no getting around this. expo-http-server will start a [background task](https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtask) that should provide a bit more background time, but this will only be ~25 seconds, which could be lowered by Apple in the future. expo-http-server will automatically pause the server when the time runs out, and resume it when the app is resumed.
101
113
 
@@ -10,6 +10,7 @@ import com.safframework.server.core.http.Request
10
10
  import com.safframework.server.core.http.Response
11
11
  import org.json.JSONObject
12
12
  import java.net.BindException
13
+ import java.util.UUID
13
14
  import java.util.concurrent.ConcurrentHashMap
14
15
 
15
16
  class ExpoHttpServerModule : Module() {
@@ -60,8 +61,10 @@ class ExpoHttpServerModule : Module() {
60
61
  // rebuilding in that case only adds an unnecessary close/rebind race.
61
62
  private var hasBoundSocket = false
62
63
  private var configuredPort: Int = 0
64
+ private var configuredLoopbackOnly = false
63
65
  private val routeConfigs = mutableListOf<RouteConfig>()
64
66
  private val responses = ConcurrentHashMap<String, SimpleHttpResponse>()
67
+ private val pendingRequests = ConcurrentHashMap.newKeySet<String>()
65
68
  private val onBindFailure: (String) -> Unit = { message ->
66
69
  started = false
67
70
  hasBoundSocket = false
@@ -75,8 +78,11 @@ class ExpoHttpServerModule : Module() {
75
78
  val headers: Map<String, String> = request.headers()
76
79
  val params: Map<String, String> = request.params()
77
80
  val cookies: Map<String, String> = request.cookies().associate { it.name() to it.value() }
81
+ val requestId = UUID.randomUUID().toString()
82
+ pendingRequests.add(requestId)
78
83
  sendEvent("onRequest", bundleOf(
79
84
  "uuid" to uuid,
85
+ "requestId" to requestId,
80
86
  "method" to request.method().name,
81
87
  "path" to request.url(),
82
88
  "body" to request.content(),
@@ -85,13 +91,14 @@ class ExpoHttpServerModule : Module() {
85
91
  "cookiesJson" to JSONObject(cookies).toString(),
86
92
  ))
87
93
  val deadline = System.currentTimeMillis() + 30_000
88
- while (!responses.containsKey(uuid) && System.currentTimeMillis() < deadline) {
94
+ while (!responses.containsKey(requestId) && System.currentTimeMillis() < deadline) {
89
95
  Thread.sleep(10)
90
96
  }
91
- val res = responses.remove(uuid) ?: return response
97
+ pendingRequests.remove(requestId)
98
+ val res = responses.remove(requestId) ?: return response
92
99
  response.setBodyText(res.body)
93
100
  response.setStatus(res.statusCode)
94
- response.addHeader("Content-Length", "" + res.body.length)
101
+ response.addHeader("Content-Length", "" + res.body.toByteArray(Charsets.UTF_8).size)
95
102
  response.addHeader("Content-Type", res.contentType)
96
103
  for ((key, value) in res.headers) {
97
104
  response.addHeader(key, value)
@@ -99,10 +106,15 @@ class ExpoHttpServerModule : Module() {
99
106
  return response
100
107
  }
101
108
 
109
+ private fun buildServer(): Server = AndroidServer.Builder {
110
+ port { configuredPort }
111
+ if (configuredLoopbackOnly) {
112
+ address { "127.0.0.1" }
113
+ }
114
+ }.build()
115
+
102
116
  private fun buildServerWithRoutes(): Server {
103
- var s: Server = AndroidServer.Builder {
104
- port { configuredPort }
105
- }.build()
117
+ var s: Server = buildServer()
106
118
  for (route in routeConfigs) {
107
119
  s = s.request(HttpMethod.getMethod(route.method), route.path) { request: Request, response: Response ->
108
120
  handleRequest(route.uuid, request, response)
@@ -126,15 +138,14 @@ class ExpoHttpServerModule : Module() {
126
138
  statusUpdateListeners.remove(onBindFailure)
127
139
  }
128
140
 
129
- Function("setup") { port: Int ->
141
+ Function("setup") { port: Int, loopbackOnly: Boolean ->
130
142
  configuredPort = port
143
+ configuredLoopbackOnly = loopbackOnly
131
144
  server?.close()
132
145
  started = false
133
146
  hasBoundSocket = false
134
147
  routeConfigs.clear()
135
- server = AndroidServer.Builder {
136
- port { configuredPort }
137
- }.build()
148
+ server = buildServer()
138
149
  }
139
150
 
140
151
  Function("route") { path: String, method: String, uuid: String ->
@@ -185,13 +196,15 @@ class ExpoHttpServerModule : Module() {
185
196
  }
186
197
  }
187
198
 
188
- Function("respond") { uuid: String,
199
+ Function("respond") { requestId: String,
189
200
  statusCode: Int,
190
201
  statusDescription: String,
191
202
  contentType: String,
192
203
  headers: HashMap<String, String>,
193
204
  body: String ->
194
- responses[uuid] = SimpleHttpResponse(statusCode, statusDescription, contentType, headers, body);
205
+ if (pendingRequests.contains(requestId)) {
206
+ responses[requestId] = SimpleHttpResponse(statusCode, statusDescription, contentType, headers, body);
207
+ }
195
208
  }
196
209
 
197
210
  Function("stop") {
package/build/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface StatusEvent {
9
9
  }
10
10
  export interface RequestEvent {
11
11
  uuid: string;
12
+ requestId: string;
12
13
  method: string;
13
14
  path: string;
14
15
  body: string;
@@ -31,6 +32,9 @@ export interface Callback {
31
32
  }
32
33
  export declare const start: () => void;
33
34
  export declare const route: (path: string, method: HttpMethod, callback: (request: RequestEvent) => Promise<Response>) => void;
34
- export declare const setup: (port: number, onStatusUpdate?: ((event: StatusEvent) => void) | undefined) => void;
35
+ export interface SetupOptions {
36
+ loopbackOnly?: boolean;
37
+ }
38
+ export declare const setup: (port: number, onStatusUpdate?: ((event: StatusEvent) => void) | undefined, options?: SetupOptions) => void;
35
39
  export declare const stop: () => void;
36
40
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AACvE;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxD;AAED,eAAO,MAAM,KAAK,YA4BjB,CAAC;AAEF,eAAO,MAAM,KAAK,SACV,MAAM,UACJ,UAAU,sBACE,YAAY,KAAK,QAAQ,QAAQ,CAAC,SAUvD,CAAC;AAEF,eAAO,MAAM,KAAK,SACV,MAAM,4BACa,WAAW,KAAK,IAAI,sBAU9C,CAAC;AAEF,eAAO,MAAM,IAAI,YAIhB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AACvE;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxD;AAED,eAAO,MAAM,KAAK,YAqCjB,CAAC;AAEF,eAAO,MAAM,KAAK,SACV,MAAM,UACJ,UAAU,sBACE,YAAY,KAAK,QAAQ,QAAQ,CAAC,SAUvD,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,KAAK,SACV,MAAM,4BACa,WAAW,KAAK,IAAI,yBACpC,YAAY,SAUtB,CAAC;AAEF,eAAO,MAAM,IAAI,YAIhB,CAAC"}
package/build/index.js CHANGED
@@ -9,13 +9,15 @@ export const start = () => {
9
9
  try {
10
10
  const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);
11
11
  if (!responseHandler) {
12
- ExpoHttpServerModule.respond(event.uuid, 404, "Not Found", "application/json", {}, JSON.stringify({ error: "Handler not found" }));
12
+ ExpoHttpServerModule.respond(event.requestId, 404, "Not Found", "application/json", {}, JSON.stringify({ error: "Handler not found" }));
13
13
  return;
14
14
  }
15
15
  const response = await responseHandler.callback(event);
16
- ExpoHttpServerModule.respond(event.uuid, response.statusCode || 200, response.statusDescription || "OK", response.contentType || "application/json", response.headers || {}, response.body || "{}");
16
+ ExpoHttpServerModule.respond(event.requestId, response.statusCode || 200, response.statusDescription || "OK", response.contentType || "application/json", response.headers || {}, response.body ?? "{}");
17
+ }
18
+ catch {
19
+ ExpoHttpServerModule.respond(event.requestId, 500, "Internal Server Error", "application/json", {}, JSON.stringify({ error: "Handler failed" }));
17
20
  }
18
- catch { }
19
21
  });
20
22
  ExpoHttpServerModule.start();
21
23
  };
@@ -29,7 +31,7 @@ export const route = (path, method, callback) => {
29
31
  });
30
32
  ExpoHttpServerModule.route(path, method, uuid);
31
33
  };
32
- export const setup = (port, onStatusUpdate) => {
34
+ export const setup = (port, onStatusUpdate, options = {}) => {
33
35
  if (onStatusUpdate) {
34
36
  emitter.addListener("onStatusUpdate", async (event) => {
35
37
  try {
@@ -38,7 +40,7 @@ export const setup = (port, onStatusUpdate) => {
38
40
  catch { }
39
41
  });
40
42
  }
41
- ExpoHttpServerModule.setup(port);
43
+ ExpoHttpServerModule.setup(port, options.loopbackOnly ?? false);
42
44
  };
43
45
  export const stop = () => {
44
46
  requestSubscription?.remove();
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,oBAAoB,MAAM,wBAAwB,CAAC;AAE1D,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,oBAAoB,CAAC,CAAC;AACvD,MAAM,gBAAgB,GAAe,EAAE,CAAC;AACxC,IAAI,mBAAmB,GAAkD,IAAI,CAAC;AAsC9E,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,EAAE;IACxB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAe,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QACnF,IAAI,CAAC;YACH,MAAM,eAAe,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5E,IAAI,CAAC,eAAe,EAAE,CAAC;gBACrB,oBAAoB,CAAC,OAAO,CAC1B,KAAK,CAAC,IAAI,EACV,GAAG,EACH,WAAW,EACX,kBAAkB,EAClB,EAAE,EACF,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAC/C,CAAC;gBACF,OAAO;YACT,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YACvD,oBAAoB,CAAC,OAAO,CAC1B,KAAK,CAAC,IAAI,EACV,QAAQ,CAAC,UAAU,IAAI,GAAG,EAC1B,QAAQ,CAAC,iBAAiB,IAAI,IAAI,EAClC,QAAQ,CAAC,WAAW,IAAI,kBAAkB,EAC1C,QAAQ,CAAC,OAAO,IAAI,EAAE,EACtB,QAAQ,CAAC,IAAI,IAAI,IAAI,CACtB,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACZ,CAAC,CAAC,CAAC;IACH,oBAAoB,CAAC,KAAK,EAAE,CAAC;AAC/B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,IAAY,EACZ,MAAkB,EAClB,QAAsD,EACtD,EAAE;IACF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjD,gBAAgB,CAAC,IAAI,CAAC;QACpB,MAAM;QACN,IAAI;QACJ,IAAI;QACJ,QAAQ;KACT,CAAC,CAAC;IACH,oBAAoB,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AACjD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,IAAY,EACZ,cAA6C,EAC7C,EAAE;IACF,IAAI,cAAc,EAAE,CAAC;QACnB,OAAO,CAAC,WAAW,CAAc,gBAAgB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;YACjE,IAAI,CAAC;gBACH,MAAM,cAAc,CAAC,KAAK,CAAC,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC;IACD,oBAAoB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,EAAE;IACvB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,mBAAmB,GAAG,IAAI,CAAC;IAC3B,oBAAoB,CAAC,IAAI,EAAE,CAAC;AAC9B,CAAC,CAAC","sourcesContent":["import { EventEmitter } from \"expo-modules-core\";\n\nimport ExpoHttpServerModule from \"./ExpoHttpServerModule\";\n\nconst emitter = new EventEmitter(ExpoHttpServerModule);\nconst requestCallbacks: Callback[] = [];\nlet requestSubscription: ReturnType<typeof emitter.addListener> | null = null;\n\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"OPTIONS\";\n/**\n * PAUSED AND RESUMED are iOS only\n */\nexport type Status = \"STARTED\" | \"PAUSED\" | \"RESUMED\" | \"STOPPED\" | \"ERROR\";\n\nexport interface StatusEvent {\n status: Status;\n message: string;\n}\n\nexport interface RequestEvent {\n uuid: string;\n method: string;\n path: string;\n body: string;\n headersJson: string;\n paramsJson: string;\n cookiesJson: string;\n}\n\nexport interface Response {\n statusCode?: number;\n statusDescription?: string;\n contentType?: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface Callback {\n method: string;\n path: string;\n uuid: string;\n callback: (request: RequestEvent) => Promise<Response>;\n}\n\nexport const start = () => {\n requestSubscription?.remove();\n requestSubscription = emitter.addListener<RequestEvent>(\"onRequest\", async (event) => {\n try {\n const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);\n if (!responseHandler) {\n ExpoHttpServerModule.respond(\n event.uuid,\n 404,\n \"Not Found\",\n \"application/json\",\n {},\n JSON.stringify({ error: \"Handler not found\" }),\n );\n return;\n }\n const response = await responseHandler.callback(event);\n ExpoHttpServerModule.respond(\n event.uuid,\n response.statusCode || 200,\n response.statusDescription || \"OK\",\n response.contentType || \"application/json\",\n response.headers || {},\n response.body || \"{}\",\n );\n } catch {}\n });\n ExpoHttpServerModule.start();\n};\n\nexport const route = (\n path: string,\n method: HttpMethod,\n callback: (request: RequestEvent) => Promise<Response>,\n) => {\n const uuid = Math.random().toString(16).slice(2);\n requestCallbacks.push({\n method,\n path,\n uuid,\n callback,\n });\n ExpoHttpServerModule.route(path, method, uuid);\n};\n\nexport const setup = (\n port: number,\n onStatusUpdate?: (event: StatusEvent) => void,\n) => {\n if (onStatusUpdate) {\n emitter.addListener<StatusEvent>(\"onStatusUpdate\", async (event) => {\n try {\n await onStatusUpdate(event);\n } catch {}\n });\n }\n ExpoHttpServerModule.setup(port);\n};\n\nexport const stop = () => {\n requestSubscription?.remove();\n requestSubscription = null;\n ExpoHttpServerModule.stop();\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,oBAAoB,MAAM,wBAAwB,CAAC;AAE1D,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,oBAAoB,CAAC,CAAC;AACvD,MAAM,gBAAgB,GAAe,EAAE,CAAC;AACxC,IAAI,mBAAmB,GAAkD,IAAI,CAAC;AAuC9E,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,EAAE;IACxB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAe,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QACnF,IAAI,CAAC;YACH,MAAM,eAAe,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5E,IAAI,CAAC,eAAe,EAAE,CAAC;gBACrB,oBAAoB,CAAC,OAAO,CAC1B,KAAK,CAAC,SAAS,EACf,GAAG,EACH,WAAW,EACX,kBAAkB,EAClB,EAAE,EACF,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAC/C,CAAC;gBACF,OAAO;YACT,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YACvD,oBAAoB,CAAC,OAAO,CAC1B,KAAK,CAAC,SAAS,EACf,QAAQ,CAAC,UAAU,IAAI,GAAG,EAC1B,QAAQ,CAAC,iBAAiB,IAAI,IAAI,EAClC,QAAQ,CAAC,WAAW,IAAI,kBAAkB,EAC1C,QAAQ,CAAC,OAAO,IAAI,EAAE,EACtB,QAAQ,CAAC,IAAI,IAAI,IAAI,CACtB,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,oBAAoB,CAAC,OAAO,CAC1B,KAAK,CAAC,SAAS,EACf,GAAG,EACH,uBAAuB,EACvB,kBAAkB,EAClB,EAAE,EACF,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAC5C,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IACH,oBAAoB,CAAC,KAAK,EAAE,CAAC;AAC/B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,IAAY,EACZ,MAAkB,EAClB,QAAsD,EACtD,EAAE;IACF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjD,gBAAgB,CAAC,IAAI,CAAC;QACpB,MAAM;QACN,IAAI;QACJ,IAAI;QACJ,QAAQ;KACT,CAAC,CAAC;IACH,oBAAoB,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AACjD,CAAC,CAAC;AAMF,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,IAAY,EACZ,cAA6C,EAC7C,UAAwB,EAAE,EAC1B,EAAE;IACF,IAAI,cAAc,EAAE,CAAC;QACnB,OAAO,CAAC,WAAW,CAAc,gBAAgB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;YACjE,IAAI,CAAC;gBACH,MAAM,cAAc,CAAC,KAAK,CAAC,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC;IACD,oBAAoB,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,YAAY,IAAI,KAAK,CAAC,CAAC;AAClE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,EAAE;IACvB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,mBAAmB,GAAG,IAAI,CAAC;IAC3B,oBAAoB,CAAC,IAAI,EAAE,CAAC;AAC9B,CAAC,CAAC","sourcesContent":["import { EventEmitter } from \"expo-modules-core\";\n\nimport ExpoHttpServerModule from \"./ExpoHttpServerModule\";\n\nconst emitter = new EventEmitter(ExpoHttpServerModule);\nconst requestCallbacks: Callback[] = [];\nlet requestSubscription: ReturnType<typeof emitter.addListener> | null = null;\n\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"OPTIONS\";\n/**\n * PAUSED AND RESUMED are iOS only\n */\nexport type Status = \"STARTED\" | \"PAUSED\" | \"RESUMED\" | \"STOPPED\" | \"ERROR\";\n\nexport interface StatusEvent {\n status: Status;\n message: string;\n}\n\nexport interface RequestEvent {\n uuid: string;\n requestId: string;\n method: string;\n path: string;\n body: string;\n headersJson: string;\n paramsJson: string;\n cookiesJson: string;\n}\n\nexport interface Response {\n statusCode?: number;\n statusDescription?: string;\n contentType?: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface Callback {\n method: string;\n path: string;\n uuid: string;\n callback: (request: RequestEvent) => Promise<Response>;\n}\n\nexport const start = () => {\n requestSubscription?.remove();\n requestSubscription = emitter.addListener<RequestEvent>(\"onRequest\", async (event) => {\n try {\n const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);\n if (!responseHandler) {\n ExpoHttpServerModule.respond(\n event.requestId,\n 404,\n \"Not Found\",\n \"application/json\",\n {},\n JSON.stringify({ error: \"Handler not found\" }),\n );\n return;\n }\n const response = await responseHandler.callback(event);\n ExpoHttpServerModule.respond(\n event.requestId,\n response.statusCode || 200,\n response.statusDescription || \"OK\",\n response.contentType || \"application/json\",\n response.headers || {},\n response.body ?? \"{}\",\n );\n } catch {\n ExpoHttpServerModule.respond(\n event.requestId,\n 500,\n \"Internal Server Error\",\n \"application/json\",\n {},\n JSON.stringify({ error: \"Handler failed\" }),\n );\n }\n });\n ExpoHttpServerModule.start();\n};\n\nexport const route = (\n path: string,\n method: HttpMethod,\n callback: (request: RequestEvent) => Promise<Response>,\n) => {\n const uuid = Math.random().toString(16).slice(2);\n requestCallbacks.push({\n method,\n path,\n uuid,\n callback,\n });\n ExpoHttpServerModule.route(path, method, uuid);\n};\n\nexport interface SetupOptions {\n loopbackOnly?: boolean;\n}\n\nexport const setup = (\n port: number,\n onStatusUpdate?: (event: StatusEvent) => void,\n options: SetupOptions = {},\n) => {\n if (onStatusUpdate) {\n emitter.addListener<StatusEvent>(\"onStatusUpdate\", async (event) => {\n try {\n await onStatusUpdate(event);\n } catch {}\n });\n }\n ExpoHttpServerModule.setup(port, options.loopbackOnly ?? false);\n};\n\nexport const stop = () => {\n requestSubscription?.remove();\n requestSubscription = null;\n ExpoHttpServerModule.stop();\n};\n"]}
@@ -4,7 +4,10 @@ import Criollo
4
4
  public class ExpoHttpServerModule: Module {
5
5
  private let server = CRHTTPServer()
6
6
  private var port: Int?
7
+ private var loopbackOnly = false
7
8
  private var stopped = false
9
+ private var isListening = false
10
+ private var foregroundObserver: NSObjectProtocol?
8
11
  private var responses = [String: CRResponse]()
9
12
  private var bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid
10
13
 
@@ -18,32 +21,54 @@ public class ExpoHttpServerModule: Module {
18
21
  Function("route", routeHandler)
19
22
  Function("respond", respondHandler)
20
23
  Function("stop", stopHandler)
24
+
25
+ OnDestroy {
26
+ self.shutdown()
27
+ }
21
28
  }
22
29
 
23
- private func setupHandler(port: Int) {
30
+ private func setupHandler(port: Int, loopbackOnly: Bool) {
24
31
  self.port = port;
32
+ self.loopbackOnly = loopbackOnly;
25
33
  }
26
34
 
27
35
  private func startHandler() {
28
- NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [unowned self] notification in
29
- if (!self.stopped) {
36
+ if (foregroundObserver == nil) {
37
+ foregroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
38
+ guard let self = self, !self.stopped, !self.isListening else {
39
+ return
40
+ }
30
41
  self.startServer(status: "RESUMED", message: "Server resumed")
31
42
  }
32
43
  }
33
44
  stopped = false;
45
+ if (isListening) {
46
+ sendEvent("onStatusUpdate", [
47
+ "status": "STARTED",
48
+ "message": "Server started"
49
+ ])
50
+ return
51
+ }
34
52
  startServer(status: "STARTED", message: "Server started")
35
53
  }
36
54
 
37
55
  private func routeHandler(path: String, method: String, uuid: String) {
38
- server.add(path, block: { (req, res, next) in
56
+ server.add(path, block: { [weak self] (req, res, next) in
39
57
  DispatchQueue.main.async {
58
+ guard let self = self else {
59
+ return
60
+ }
40
61
  var bodyString = "{}"
41
- if let body = req.body, let bodyData = try? JSONSerialization.data(withJSONObject: body) {
62
+ if let body = req.body,
63
+ JSONSerialization.isValidJSONObject(body),
64
+ let bodyData = try? JSONSerialization.data(withJSONObject: body) {
42
65
  bodyString = String(data: bodyData, encoding: .utf8) ?? "{}"
43
66
  }
44
- self.responses[uuid] = res
67
+ let requestId = UUID().uuidString
68
+ self.responses[requestId] = res
45
69
  self.sendEvent("onRequest", [
46
70
  "uuid": uuid,
71
+ "requestId": requestId,
47
72
  "method": req.method.toString(),
48
73
  "path": path,
49
74
  "body": bodyString,
@@ -51,47 +76,63 @@ public class ExpoHttpServerModule: Module {
51
76
  "paramsJson": req.query.jsonString,
52
77
  "cookiesJson": req.cookies?.jsonString ?? "{}"
53
78
  ])
79
+ DispatchQueue.main.asyncAfter(deadline: .now() + 30) { [weak self] in
80
+ self?.respondHandler(requestId: requestId,
81
+ statusCode: 504,
82
+ statusDescription: "Gateway Timeout",
83
+ contentType: "application/json",
84
+ headers: [:],
85
+ body: "{}")
86
+ }
54
87
  }
55
88
  }, recursive: false, method: CRHTTPMethod.fromString(method))
56
89
  }
57
90
 
58
- private func respondHandler(udid: String,
91
+ private func respondHandler(requestId: String,
59
92
  statusCode: Int,
60
93
  statusDescription: String,
61
94
  contentType: String,
62
95
  headers: [String: String],
63
96
  body: String) {
64
97
  DispatchQueue.main.async {
65
- if let response = self.responses[udid] {
98
+ if let response = self.responses[requestId] {
66
99
  response.setStatusCode(UInt(statusCode), description: statusDescription)
67
100
  response.setValue(contentType, forHTTPHeaderField: "Content-type")
68
- response.setValue("\(body.count)", forHTTPHeaderField: "Content-Length")
101
+ response.setValue("\(body.utf8.count)", forHTTPHeaderField: "Content-Length")
69
102
  for (key, value) in headers {
70
103
  response.setValue(value, forHTTPHeaderField: key)
71
104
  }
72
105
  response.send(body);
73
- self.responses[udid] = nil;
106
+ self.responses[requestId] = nil;
74
107
  }
75
108
  }
76
109
 
77
110
  }
78
111
 
79
112
  private func stopHandler() {
113
+ shutdown(status: "STOPPED", message: "Server stopped")
114
+ }
115
+
116
+ private func shutdown(status: String? = nil, message: String? = nil) {
80
117
  stopped = true;
81
- stopServer(status: "STOPPED", message: "Server stopped")
118
+ if let observer = foregroundObserver {
119
+ NotificationCenter.default.removeObserver(observer)
120
+ foregroundObserver = nil
121
+ }
122
+ stopServer(status: status, message: message)
82
123
  }
83
124
 
84
125
  private func startServer(status: String, message: String) {
85
- stopServer()
86
126
  if let port = port {
87
127
  var error: NSError?
88
- server.startListening(&error, portNumber: UInt(port))
128
+ server.startListening(&error, portNumber: UInt(port), interface: loopbackOnly ? "localhost" : nil)
89
129
  if (error != nil) {
90
130
  sendEvent("onStatusUpdate", [
91
131
  "status": "ERROR",
92
132
  "message": error?.localizedDescription ?? "Unknown error starting server"
93
133
  ])
94
134
  } else {
135
+ isListening = true
95
136
  beginBackgroundTask()
96
137
  sendEvent("onStatusUpdate", [
97
138
  "status": status,
@@ -108,6 +149,7 @@ public class ExpoHttpServerModule: Module {
108
149
 
109
150
  private func stopServer(status: String? = nil, message: String? = nil) {
110
151
  server.stopListening()
152
+ isListening = false
111
153
  endBackgroundTask()
112
154
  if let status = status, let message = message {
113
155
  sendEvent("onStatusUpdate", [
@@ -119,8 +161,8 @@ public class ExpoHttpServerModule: Module {
119
161
 
120
162
  private func beginBackgroundTask() {
121
163
  if (bgTaskIdentifier == UIBackgroundTaskIdentifier.invalid) {
122
- self.bgTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "BgTask", expirationHandler: {
123
- self.stopServer(status: "PAUSED", message: "Server paused")
164
+ self.bgTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "BgTask", expirationHandler: { [weak self] in
165
+ self?.stopServer(status: "PAUSED", message: "Server paused")
124
166
  })
125
167
  }
126
168
  }
@@ -151,6 +193,8 @@ extension CRHTTPMethod {
151
193
  return "PUT"
152
194
  case .delete:
153
195
  return "DELETE"
196
+ case .options:
197
+ return "OPTIONS"
154
198
  default:
155
199
  return "GET"
156
200
  }
@@ -165,6 +209,8 @@ extension CRHTTPMethod {
165
209
  httpMethod = .put
166
210
  case "DELETE":
167
211
  httpMethod = .delete
212
+ case "OPTIONS":
213
+ httpMethod = .options
168
214
  default:
169
215
  httpMethod = .get
170
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-http-server",
3
- "version": "0.1.14-beta.4",
3
+ "version": "0.1.14",
4
4
  "description": "A simple HTTP server expo module (iOS/Android)",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export interface StatusEvent {
19
19
 
20
20
  export interface RequestEvent {
21
21
  uuid: string;
22
+ requestId: string;
22
23
  method: string;
23
24
  path: string;
24
25
  body: string;
@@ -49,7 +50,7 @@ export const start = () => {
49
50
  const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);
50
51
  if (!responseHandler) {
51
52
  ExpoHttpServerModule.respond(
52
- event.uuid,
53
+ event.requestId,
53
54
  404,
54
55
  "Not Found",
55
56
  "application/json",
@@ -60,14 +61,23 @@ export const start = () => {
60
61
  }
61
62
  const response = await responseHandler.callback(event);
62
63
  ExpoHttpServerModule.respond(
63
- event.uuid,
64
+ event.requestId,
64
65
  response.statusCode || 200,
65
66
  response.statusDescription || "OK",
66
67
  response.contentType || "application/json",
67
68
  response.headers || {},
68
- response.body || "{}",
69
+ response.body ?? "{}",
69
70
  );
70
- } catch {}
71
+ } catch {
72
+ ExpoHttpServerModule.respond(
73
+ event.requestId,
74
+ 500,
75
+ "Internal Server Error",
76
+ "application/json",
77
+ {},
78
+ JSON.stringify({ error: "Handler failed" }),
79
+ );
80
+ }
71
81
  });
72
82
  ExpoHttpServerModule.start();
73
83
  };
@@ -87,9 +97,14 @@ export const route = (
87
97
  ExpoHttpServerModule.route(path, method, uuid);
88
98
  };
89
99
 
100
+ export interface SetupOptions {
101
+ loopbackOnly?: boolean;
102
+ }
103
+
90
104
  export const setup = (
91
105
  port: number,
92
106
  onStatusUpdate?: (event: StatusEvent) => void,
107
+ options: SetupOptions = {},
93
108
  ) => {
94
109
  if (onStatusUpdate) {
95
110
  emitter.addListener<StatusEvent>("onStatusUpdate", async (event) => {
@@ -98,7 +113,7 @@ export const setup = (
98
113
  } catch {}
99
114
  });
100
115
  }
101
- ExpoHttpServerModule.setup(port);
116
+ ExpoHttpServerModule.setup(port, options.loopbackOnly ?? false);
102
117
  };
103
118
 
104
119
  export const stop = () => {