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 +12 -0
- package/android/src/main/java/expo/modules/httpserver/ExpoHttpServerModule.kt +25 -12
- package/build/index.d.ts +5 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +7 -5
- package/build/index.js.map +1 -1
- package/ios/ExpoHttpServerModule.swift +61 -15
- package/package.json +1 -1
- package/src/index.ts +20 -5
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(
|
|
94
|
+
while (!responses.containsKey(requestId) && System.currentTimeMillis() < deadline) {
|
|
89
95
|
Thread.sleep(10)
|
|
90
96
|
}
|
|
91
|
-
|
|
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.
|
|
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 =
|
|
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 =
|
|
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") {
|
|
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
|
-
|
|
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
|
|
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
|
package/build/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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.
|
|
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();
|
package/build/index.js.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","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
|
-
|
|
29
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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[
|
|
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[
|
|
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
|
-
|
|
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
|
|
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
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.
|
|
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.
|
|
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 = () => {
|