@kccd/expo-http-server 0.1.13

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/.eslintrc.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ["universe/native", "universe/web"],
4
+ ignorePatterns: ["build"],
5
+ };
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+
2
+ # expo-http-server
3
+
4
+ [![npm](https://img.shields.io/npm/v/expo-http-server?style=for-the-badge)](https://www.npmjs.com/package/expo-http-server)
5
+ [![npm](https://img.shields.io/npm/dt/expo-http-server?style=for-the-badge)](https://www.npmjs.com/package/expo-http-server)
6
+ [![GitHub contributors](https://img.shields.io/github/contributors/simonsturge/expo-http-server?style=for-the-badge)](https://github.com/simonsturge/expo-http-server)
7
+ [![GitHub Repo stars](https://img.shields.io/github/stars/simonsturge/expo-http-server?style=for-the-badge)](https://github.com/simonsturge/expo-http-server)
8
+
9
+ A simple HTTP Server [Expo module](https://docs.expo.dev/modules/) .
10
+
11
+ Current implementation is for iOS / Android only ([React Native](https://github.com/facebook/react-native)).
12
+
13
+ iOS: [Criollo](https://github.com/thecatalinstan/Criollo)
14
+ Android: [AndroidServer](https://github.com/fengzhizi715/AndroidServer)
15
+ Web: **Not implemented**
16
+
17
+ ## Install
18
+
19
+ ```shell
20
+ npx expo install @kccd/expo-http-server
21
+ ```
22
+
23
+ ## Example
24
+ ```tsx
25
+ import * as server from "expo-http-server";
26
+ import { useEffect, useState } from "react";
27
+ import { Text, View } from "react-native";
28
+
29
+ export default function App() {
30
+ const [lastCalled, setLastCalled] = useState<number | undefined>();
31
+
32
+ const html = `
33
+ <!DOCTYPE html>
34
+ <html>
35
+ <body style="background-color:powderblue;">
36
+ <h1>expo-http-server</h1>
37
+ <p>You can load HTML!</p>
38
+ </body>
39
+ </html>`;
40
+
41
+ const obj = { app: "expo-http-server", desc: "You can load JSON!" };
42
+
43
+ useEffect(() => {
44
+ server.setup(9666, (event: server.StatusEvent) => {
45
+ if (event.status === "ERROR") {
46
+ // there was an error...
47
+ } else {
48
+ // server was STARTED, PAUSED, RESUMED or STOPPED
49
+ }
50
+ });
51
+ server.route("/", "GET", async (request) => {
52
+ console.log("Request", "/", "GET", request);
53
+ setLastCalled(Date.now());
54
+ return {
55
+ statusCode: 200,
56
+ headers: {
57
+ "Custom-Header": "Bazinga",
58
+ },
59
+ contentType: "application/json",
60
+ body: JSON.stringify(obj),
61
+ };
62
+ });
63
+ server.route("/html", "GET", async (request) => {
64
+ console.log("Request", "/html", "GET", request);
65
+ setLastCalled(Date.now());
66
+ return {
67
+ statusCode: 200,
68
+ statusDescription: "OK - CUSTOM STATUS",
69
+ contentType: "text/html",
70
+ body: html,
71
+ };
72
+ });
73
+ server.start();
74
+ return () => {
75
+ server.stop();
76
+ };
77
+ }, []);
78
+
79
+ return (
80
+ <View
81
+ style={{
82
+ flex: 1,
83
+ backgroundColor: "#fff",
84
+ alignItems: "center",
85
+ justifyContent: "center",
86
+ }}
87
+ >
88
+ <Text>
89
+ {lastCalled === undefined
90
+ ? "Request webserver to change text"
91
+ : "Called at " + new Date(lastCalled).toLocaleString()}
92
+ </Text>
93
+ </View>
94
+ );
95
+ }
96
+
97
+ ```
98
+
99
+ ## Running in the background
100
+ **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
+
102
+ **Android**: The server can be ran continuously in the background using a foreground service, e.g. a persistent notification. [Notifee](https://notifee.app/react-native/docs/android/foreground-service#building-a-long-lived-task) can be used to do this. Take a look at the example project for how to set this up.
103
+
104
+ ## Testing
105
+
106
+ Send a request to the server in a browser `browser` or `curl`:
107
+
108
+ ```shell
109
+ curl http://IP_OF_DEVICE:MY_PORT
110
+ ```
111
+ For example:
112
+ ```shell
113
+ curl http://192.168.1.109:3000
114
+ ```
File without changes
@@ -0,0 +1,2 @@
1
+ #Wed Jul 17 17:56:56 CST 2024
2
+ java.home=/home/pxgo/Applications/android-studio/jbr
File without changes
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="GradleSettings">
4
+ <option name="linkedExternalProjectsSettings">
5
+ <GradleProjectSettings>
6
+ <option name="externalProjectPath" value="$PROJECT_DIR$" />
7
+ <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
8
+ <option name="resolveExternalAnnotations" value="false" />
9
+ </GradleProjectSettings>
10
+ </option>
11
+ </component>
12
+ </project>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectMigrations">
4
+ <option name="MigrateToGradleLocalJavaHome">
5
+ <set>
6
+ <option value="$PROJECT_DIR$" />
7
+ </set>
8
+ </option>
9
+ </component>
10
+ </project>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ExternalStorageConfigurationManager" enabled="true" />
4
+ <component name="ProjectRootManager">
5
+ <output url="file://$PROJECT_DIR$/build/classes" />
6
+ </component>
7
+ <component name="ProjectType">
8
+ <option name="id" value="Android" />
9
+ </component>
10
+ </project>
@@ -0,0 +1,263 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="direct_access_persist.xml">
4
+ <option name="deviceSelectionList">
5
+ <list>
6
+ <PersistentDeviceSelectionData>
7
+ <option name="api" value="27" />
8
+ <option name="brand" value="DOCOMO" />
9
+ <option name="codename" value="F01L" />
10
+ <option name="id" value="F01L" />
11
+ <option name="manufacturer" value="FUJITSU" />
12
+ <option name="name" value="F-01L" />
13
+ <option name="screenDensity" value="360" />
14
+ <option name="screenX" value="720" />
15
+ <option name="screenY" value="1280" />
16
+ </PersistentDeviceSelectionData>
17
+ <PersistentDeviceSelectionData>
18
+ <option name="api" value="28" />
19
+ <option name="brand" value="DOCOMO" />
20
+ <option name="codename" value="SH-01L" />
21
+ <option name="id" value="SH-01L" />
22
+ <option name="manufacturer" value="SHARP" />
23
+ <option name="name" value="AQUOS sense2 SH-01L" />
24
+ <option name="screenDensity" value="480" />
25
+ <option name="screenX" value="1080" />
26
+ <option name="screenY" value="2160" />
27
+ </PersistentDeviceSelectionData>
28
+ <PersistentDeviceSelectionData>
29
+ <option name="api" value="31" />
30
+ <option name="brand" value="samsung" />
31
+ <option name="codename" value="a51" />
32
+ <option name="id" value="a51" />
33
+ <option name="manufacturer" value="Samsung" />
34
+ <option name="name" value="Galaxy A51" />
35
+ <option name="screenDensity" value="420" />
36
+ <option name="screenX" value="1080" />
37
+ <option name="screenY" value="2400" />
38
+ </PersistentDeviceSelectionData>
39
+ <PersistentDeviceSelectionData>
40
+ <option name="api" value="34" />
41
+ <option name="brand" value="google" />
42
+ <option name="codename" value="akita" />
43
+ <option name="id" value="akita" />
44
+ <option name="manufacturer" value="Google" />
45
+ <option name="name" value="Pixel 8a" />
46
+ <option name="screenDensity" value="420" />
47
+ <option name="screenX" value="1080" />
48
+ <option name="screenY" value="2400" />
49
+ </PersistentDeviceSelectionData>
50
+ <PersistentDeviceSelectionData>
51
+ <option name="api" value="33" />
52
+ <option name="brand" value="samsung" />
53
+ <option name="codename" value="b0q" />
54
+ <option name="id" value="b0q" />
55
+ <option name="manufacturer" value="Samsung" />
56
+ <option name="name" value="Galaxy S22 Ultra" />
57
+ <option name="screenDensity" value="600" />
58
+ <option name="screenX" value="1440" />
59
+ <option name="screenY" value="3088" />
60
+ </PersistentDeviceSelectionData>
61
+ <PersistentDeviceSelectionData>
62
+ <option name="api" value="32" />
63
+ <option name="brand" value="google" />
64
+ <option name="codename" value="bluejay" />
65
+ <option name="id" value="bluejay" />
66
+ <option name="manufacturer" value="Google" />
67
+ <option name="name" value="Pixel 6a" />
68
+ <option name="screenDensity" value="420" />
69
+ <option name="screenX" value="1080" />
70
+ <option name="screenY" value="2400" />
71
+ </PersistentDeviceSelectionData>
72
+ <PersistentDeviceSelectionData>
73
+ <option name="api" value="29" />
74
+ <option name="brand" value="samsung" />
75
+ <option name="codename" value="crownqlteue" />
76
+ <option name="id" value="crownqlteue" />
77
+ <option name="manufacturer" value="Samsung" />
78
+ <option name="name" value="Galaxy Note9" />
79
+ <option name="screenDensity" value="420" />
80
+ <option name="screenX" value="2220" />
81
+ <option name="screenY" value="1080" />
82
+ </PersistentDeviceSelectionData>
83
+ <PersistentDeviceSelectionData>
84
+ <option name="api" value="34" />
85
+ <option name="brand" value="samsung" />
86
+ <option name="codename" value="dm3q" />
87
+ <option name="id" value="dm3q" />
88
+ <option name="manufacturer" value="Samsung" />
89
+ <option name="name" value="Galaxy S23 Ultra" />
90
+ <option name="screenDensity" value="600" />
91
+ <option name="screenX" value="1440" />
92
+ <option name="screenY" value="3088" />
93
+ </PersistentDeviceSelectionData>
94
+ <PersistentDeviceSelectionData>
95
+ <option name="api" value="33" />
96
+ <option name="brand" value="google" />
97
+ <option name="codename" value="felix" />
98
+ <option name="id" value="felix" />
99
+ <option name="manufacturer" value="Google" />
100
+ <option name="name" value="Pixel Fold" />
101
+ <option name="screenDensity" value="420" />
102
+ <option name="screenX" value="2208" />
103
+ <option name="screenY" value="1840" />
104
+ </PersistentDeviceSelectionData>
105
+ <PersistentDeviceSelectionData>
106
+ <option name="api" value="33" />
107
+ <option name="brand" value="google" />
108
+ <option name="codename" value="felix_camera" />
109
+ <option name="id" value="felix_camera" />
110
+ <option name="manufacturer" value="Google" />
111
+ <option name="name" value="Pixel Fold (Camera-enabled)" />
112
+ <option name="screenDensity" value="420" />
113
+ <option name="screenX" value="2208" />
114
+ <option name="screenY" value="1840" />
115
+ </PersistentDeviceSelectionData>
116
+ <PersistentDeviceSelectionData>
117
+ <option name="api" value="33" />
118
+ <option name="brand" value="samsung" />
119
+ <option name="codename" value="gts8uwifi" />
120
+ <option name="id" value="gts8uwifi" />
121
+ <option name="manufacturer" value="Samsung" />
122
+ <option name="name" value="Galaxy Tab S8 Ultra" />
123
+ <option name="screenDensity" value="320" />
124
+ <option name="screenX" value="1848" />
125
+ <option name="screenY" value="2960" />
126
+ </PersistentDeviceSelectionData>
127
+ <PersistentDeviceSelectionData>
128
+ <option name="api" value="34" />
129
+ <option name="brand" value="google" />
130
+ <option name="codename" value="husky" />
131
+ <option name="id" value="husky" />
132
+ <option name="manufacturer" value="Google" />
133
+ <option name="name" value="Pixel 8 Pro" />
134
+ <option name="screenDensity" value="390" />
135
+ <option name="screenX" value="1008" />
136
+ <option name="screenY" value="2244" />
137
+ </PersistentDeviceSelectionData>
138
+ <PersistentDeviceSelectionData>
139
+ <option name="api" value="30" />
140
+ <option name="brand" value="motorola" />
141
+ <option name="codename" value="java" />
142
+ <option name="id" value="java" />
143
+ <option name="manufacturer" value="Motorola" />
144
+ <option name="name" value="G20" />
145
+ <option name="screenDensity" value="280" />
146
+ <option name="screenX" value="720" />
147
+ <option name="screenY" value="1600" />
148
+ </PersistentDeviceSelectionData>
149
+ <PersistentDeviceSelectionData>
150
+ <option name="api" value="33" />
151
+ <option name="brand" value="google" />
152
+ <option name="codename" value="lynx" />
153
+ <option name="id" value="lynx" />
154
+ <option name="manufacturer" value="Google" />
155
+ <option name="name" value="Pixel 7a" />
156
+ <option name="screenDensity" value="420" />
157
+ <option name="screenX" value="1080" />
158
+ <option name="screenY" value="2400" />
159
+ </PersistentDeviceSelectionData>
160
+ <PersistentDeviceSelectionData>
161
+ <option name="api" value="31" />
162
+ <option name="brand" value="google" />
163
+ <option name="codename" value="oriole" />
164
+ <option name="id" value="oriole" />
165
+ <option name="manufacturer" value="Google" />
166
+ <option name="name" value="Pixel 6" />
167
+ <option name="screenDensity" value="420" />
168
+ <option name="screenX" value="1080" />
169
+ <option name="screenY" value="2400" />
170
+ </PersistentDeviceSelectionData>
171
+ <PersistentDeviceSelectionData>
172
+ <option name="api" value="33" />
173
+ <option name="brand" value="google" />
174
+ <option name="codename" value="panther" />
175
+ <option name="id" value="panther" />
176
+ <option name="manufacturer" value="Google" />
177
+ <option name="name" value="Pixel 7" />
178
+ <option name="screenDensity" value="420" />
179
+ <option name="screenX" value="1080" />
180
+ <option name="screenY" value="2400" />
181
+ </PersistentDeviceSelectionData>
182
+ <PersistentDeviceSelectionData>
183
+ <option name="api" value="31" />
184
+ <option name="brand" value="samsung" />
185
+ <option name="codename" value="q2q" />
186
+ <option name="id" value="q2q" />
187
+ <option name="manufacturer" value="Samsung" />
188
+ <option name="name" value="Galaxy Z Fold3" />
189
+ <option name="screenDensity" value="420" />
190
+ <option name="screenX" value="1768" />
191
+ <option name="screenY" value="2208" />
192
+ </PersistentDeviceSelectionData>
193
+ <PersistentDeviceSelectionData>
194
+ <option name="api" value="34" />
195
+ <option name="brand" value="samsung" />
196
+ <option name="codename" value="q5q" />
197
+ <option name="id" value="q5q" />
198
+ <option name="manufacturer" value="Samsung" />
199
+ <option name="name" value="Galaxy Z Fold5" />
200
+ <option name="screenDensity" value="420" />
201
+ <option name="screenX" value="1812" />
202
+ <option name="screenY" value="2176" />
203
+ </PersistentDeviceSelectionData>
204
+ <PersistentDeviceSelectionData>
205
+ <option name="api" value="30" />
206
+ <option name="brand" value="google" />
207
+ <option name="codename" value="r11" />
208
+ <option name="id" value="r11" />
209
+ <option name="manufacturer" value="Google" />
210
+ <option name="name" value="Pixel Watch" />
211
+ <option name="screenDensity" value="320" />
212
+ <option name="screenX" value="384" />
213
+ <option name="screenY" value="384" />
214
+ <option name="type" value="WEAR_OS" />
215
+ </PersistentDeviceSelectionData>
216
+ <PersistentDeviceSelectionData>
217
+ <option name="api" value="30" />
218
+ <option name="brand" value="google" />
219
+ <option name="codename" value="redfin" />
220
+ <option name="id" value="redfin" />
221
+ <option name="manufacturer" value="Google" />
222
+ <option name="name" value="Pixel 5" />
223
+ <option name="screenDensity" value="440" />
224
+ <option name="screenX" value="1080" />
225
+ <option name="screenY" value="2340" />
226
+ </PersistentDeviceSelectionData>
227
+ <PersistentDeviceSelectionData>
228
+ <option name="api" value="34" />
229
+ <option name="brand" value="google" />
230
+ <option name="codename" value="shiba" />
231
+ <option name="id" value="shiba" />
232
+ <option name="manufacturer" value="Google" />
233
+ <option name="name" value="Pixel 8" />
234
+ <option name="screenDensity" value="420" />
235
+ <option name="screenX" value="1080" />
236
+ <option name="screenY" value="2400" />
237
+ </PersistentDeviceSelectionData>
238
+ <PersistentDeviceSelectionData>
239
+ <option name="api" value="33" />
240
+ <option name="brand" value="google" />
241
+ <option name="codename" value="tangorpro" />
242
+ <option name="id" value="tangorpro" />
243
+ <option name="manufacturer" value="Google" />
244
+ <option name="name" value="Pixel Tablet" />
245
+ <option name="screenDensity" value="320" />
246
+ <option name="screenX" value="1600" />
247
+ <option name="screenY" value="2560" />
248
+ </PersistentDeviceSelectionData>
249
+ <PersistentDeviceSelectionData>
250
+ <option name="api" value="29" />
251
+ <option name="brand" value="samsung" />
252
+ <option name="codename" value="x1q" />
253
+ <option name="id" value="x1q" />
254
+ <option name="manufacturer" value="Samsung" />
255
+ <option name="name" value="Galaxy S20" />
256
+ <option name="screenDensity" value="480" />
257
+ <option name="screenX" value="1440" />
258
+ <option name="screenY" value="3200" />
259
+ </PersistentDeviceSelectionData>
260
+ </list>
261
+ </option>
262
+ </component>
263
+ </project>
@@ -0,0 +1,94 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+ apply plugin: 'maven-publish'
4
+
5
+ group = 'expo.modules.httpserver'
6
+ version = '0.1.0'
7
+
8
+ buildscript {
9
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
10
+ if (expoModulesCorePlugin.exists()) {
11
+ apply from: expoModulesCorePlugin
12
+ applyKotlinExpoModulesCorePlugin()
13
+ }
14
+
15
+ // Simple helper that allows the root project to override versions declared by this library.
16
+ ext.safeExtGet = { prop, fallback ->
17
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
18
+ }
19
+
20
+ // Ensures backward compatibility
21
+ ext.getKotlinVersion = {
22
+ if (ext.has("kotlinVersion")) {
23
+ ext.kotlinVersion()
24
+ } else {
25
+ ext.safeExtGet("kotlinVersion", "1.8.10")
26
+ }
27
+ }
28
+
29
+ repositories {
30
+ mavenCentral()
31
+ }
32
+
33
+ dependencies {
34
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
35
+ }
36
+ }
37
+
38
+ afterEvaluate {
39
+ publishing {
40
+ publications {
41
+ release(MavenPublication) {
42
+ from components.release
43
+ }
44
+ }
45
+ repositories {
46
+ maven {
47
+ url = mavenLocal().url
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ android {
54
+ compileSdkVersion safeExtGet("compileSdkVersion", 33)
55
+
56
+ def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
57
+ if (agpVersion.tokenize('.')[0].toInteger() < 8) {
58
+ compileOptions {
59
+ sourceCompatibility JavaVersion.VERSION_11
60
+ targetCompatibility JavaVersion.VERSION_11
61
+ }
62
+
63
+ kotlinOptions {
64
+ jvmTarget = JavaVersion.VERSION_11.majorVersion
65
+ }
66
+ }
67
+
68
+ namespace "expo.modules.httpserver"
69
+ defaultConfig {
70
+ minSdkVersion safeExtGet("minSdkVersion", 21)
71
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
72
+ versionCode 1
73
+ versionName "0.1.0"
74
+ }
75
+ lintOptions {
76
+ abortOnError false
77
+ }
78
+ publishing {
79
+ singleVariant("release") {
80
+ withSourcesJar()
81
+ }
82
+ }
83
+ }
84
+
85
+ repositories {
86
+ mavenCentral()
87
+ }
88
+
89
+ dependencies {
90
+ implementation project(':expo-modules-core')
91
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
92
+ implementation 'com.github.fengzhizi715.AndroidServer:core:1.3.3'
93
+ implementation 'com.github.fengzhizi715.AndroidServer:gson:1.3.3'
94
+ }
@@ -0,0 +1,8 @@
1
+ ## This file must *NOT* be checked into Version Control Systems,
2
+ # as it contains information specific to your local configuration.
3
+ #
4
+ # Location of the SDK. This is only used by Gradle.
5
+ # For customization when using a Version Control System, please read the
6
+ # header note.
7
+ #Wed Jul 17 17:56:56 CST 2024
8
+ sdk.dir=/home/pxgo/Android/Sdk
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,107 @@
1
+ package expo.modules.httpserver
2
+
3
+ import androidx.core.os.bundleOf
4
+ import expo.modules.kotlin.modules.Module
5
+ import expo.modules.kotlin.modules.ModuleDefinition
6
+ import com.safframework.server.core.AndroidServer
7
+ import com.safframework.server.core.Server
8
+ import com.safframework.server.core.http.HttpMethod
9
+ import com.safframework.server.core.http.Request
10
+ import com.safframework.server.core.http.Response
11
+ import org.json.JSONObject
12
+ import java.util.UUID
13
+
14
+ class ExpoHttpServerModule : Module() {
15
+ class SimpleHttpResponse(val statusCode: Int,
16
+ val statusDescription: String,
17
+ val contentType: String,
18
+ val headers: HashMap<String, String>,
19
+ val body: String)
20
+
21
+ private var server: Server? = null;
22
+ private var started = false;
23
+ private val responses = HashMap<String, SimpleHttpResponse>()
24
+
25
+ override fun definition() = ModuleDefinition {
26
+
27
+ Name("ExpoHttpServer")
28
+
29
+ Events("onStatusUpdate", "onRequest")
30
+
31
+ Function("setup") { port: Int ->
32
+ server = AndroidServer.Builder{
33
+ port {
34
+ port
35
+ }
36
+ }.build()
37
+ }
38
+
39
+ Function("route") { path: String, method: String, uuid: String ->
40
+ server = server?.request(HttpMethod.getMethod(method), path) { request: Request, response: Response ->
41
+ val headers: Map<String, String> = request.headers()
42
+ val params: Map<String, String> = request.params()
43
+ val cookies: Map<String, String> = request.cookies().associate { it.name() to it.value() }
44
+ val requestId = UUID.randomUUID().toString()
45
+ sendEvent("onRequest", bundleOf(
46
+ "uuid" to uuid,
47
+ "requestId" to requestId,
48
+ "method" to request.method().name,
49
+ "path" to request.url(),
50
+ "body" to request.content(),
51
+ "headersJson" to JSONObject(headers).toString(),
52
+ "paramsJson" to JSONObject(params).toString(),
53
+ "cookiesJson" to JSONObject(cookies).toString(),
54
+ ))
55
+ while (!responses.containsKey(requestId)) {
56
+ Thread.sleep(10)
57
+ }
58
+ val res = responses[requestId]!!
59
+ response.setBodyText(res.body)
60
+ response.setStatus(res.statusCode)
61
+ response.addHeader("Content-Length", "" + res.body.length)
62
+ response.addHeader("Content-Type", res.contentType)
63
+ for ((key, value) in res.headers) {
64
+ response.addHeader(key, value)
65
+ }
66
+ responses.remove(requestId);
67
+ return@request response
68
+ };
69
+ }
70
+
71
+ Function("start") {
72
+ if (server == null) {
73
+ sendEvent("onStatusUpdate", bundleOf(
74
+ "status" to "ERROR",
75
+ "message" to "Server not setup / port not configured"
76
+ ))
77
+ } else {
78
+ if (!started) {
79
+ started = true
80
+ server?.start()
81
+ sendEvent("onStatusUpdate", bundleOf(
82
+ "status" to "STARTED",
83
+ "message" to "Server started"
84
+ ))
85
+ }
86
+ }
87
+ }
88
+
89
+ Function("respond") { requestId: String,
90
+ statusCode: Int,
91
+ statusDescription: String,
92
+ contentType: String,
93
+ headers: HashMap<String, String>,
94
+ body: String ->
95
+ responses[requestId] = SimpleHttpResponse(statusCode, statusDescription, contentType, headers, body);
96
+ }
97
+
98
+ Function("stop") {
99
+ started = false
100
+ server?.close()
101
+ sendEvent("onStatusUpdate", bundleOf(
102
+ "status" to "STOPPED",
103
+ "message" to "Server stopped"
104
+ ))
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["ios", "android"],
3
+ "ios": {
4
+ "modules": ["ExpoHttpServerModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.httpserver.ExpoHttpServerModule"]
8
+ }
9
+ }
@@ -0,0 +1,29 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ExpoHttpServer'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platform = :ios, '13.4'
14
+ s.swift_version = '5.4'
15
+ s.source = { git: 'https://github.com/simonsturge/expo-http-server' }
16
+ s.static_framework = true
17
+
18
+ s.dependency 'ExpoModulesCore'
19
+ s.dependency 'Criollo'
20
+ s.dependency 'CocoaAsyncSocket'
21
+
22
+ # Swift/Objective-C compatibility
23
+ s.pod_target_xcconfig = {
24
+ 'DEFINES_MODULE' => 'YES',
25
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule'
26
+ }
27
+
28
+ s.source_files = "**/*.{h,m,swift}"
29
+ end
@@ -0,0 +1,180 @@
1
+ import ExpoModulesCore
2
+ import Criollo
3
+ import Foundation
4
+
5
+ public class ExpoHttpServerModule: Module {
6
+ private let server = CRHTTPServer()
7
+ private var port: Int?
8
+ private var stopped = false
9
+ private var responses = [String: CRResponse]()
10
+ private var bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid
11
+
12
+ public func definition() -> ModuleDefinition {
13
+ Name("ExpoHttpServer")
14
+
15
+ Events("onStatusUpdate", "onRequest")
16
+
17
+ Function("setup", setupHandler)
18
+ Function("start", startHandler)
19
+ Function("route", routeHandler)
20
+ Function("respond", respondHandler)
21
+ Function("stop", stopHandler)
22
+ }
23
+
24
+ private func setupHandler(port: Int) {
25
+ self.port = port;
26
+ }
27
+
28
+ private func startHandler() {
29
+ NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [unowned self] notification in
30
+ if (!self.stopped) {
31
+ self.startServer(status: "RESUMED", message: "Server resumed")
32
+ }
33
+ }
34
+ stopped = false;
35
+ startServer(status: "STARTED", message: "Server started")
36
+ }
37
+
38
+ private func routeHandler(path: String, method: String, uuid: String) {
39
+ server.add(path, block: { (req, res, next) in
40
+ DispatchQueue.main.async {
41
+ var bodyString = "{}"
42
+ if let body = req.body, let bodyData = try? JSONSerialization.data(withJSONObject: body) {
43
+ bodyString = String(data: bodyData, encoding: .utf8) ?? "{}"
44
+ }
45
+ let requestId = UUID().uuidString
46
+ self.responses[requestId] = res
47
+ self.sendEvent("onRequest", [
48
+ "uuid": uuid,
49
+ "requestId": requestId,
50
+ "method": req.method.toString(),
51
+ "path": path,
52
+ "body": bodyString,
53
+ "headersJson": req.allHTTPHeaderFields.jsonString,
54
+ "paramsJson": req.query.jsonString,
55
+ "cookiesJson": req.cookies?.jsonString ?? "{}"
56
+ ])
57
+ }
58
+ }, recursive: false, method: CRHTTPMethod.fromString(method))
59
+ }
60
+
61
+ private func respondHandler(requestId: String,
62
+ statusCode: Int,
63
+ statusDescription: String,
64
+ contentType: String,
65
+ headers: [String: String],
66
+ body: String) {
67
+ DispatchQueue.main.async {
68
+ if let response = self.responses[requestId] {
69
+ response.setStatusCode(UInt(statusCode), description: statusDescription)
70
+ response.setValue(contentType, forHTTPHeaderField: "Content-type")
71
+ response.setValue("\(body.count)", forHTTPHeaderField: "Content-Length")
72
+ for (key, value) in headers {
73
+ response.setValue(value, forHTTPHeaderField: key)
74
+ }
75
+ response.send(body);
76
+ self.responses[requestId] = nil;
77
+ }
78
+ }
79
+
80
+ }
81
+
82
+ private func stopHandler() {
83
+ stopped = true;
84
+ stopServer(status: "STOPPED", message: "Server stopped")
85
+ }
86
+
87
+ private func startServer(status: String, message: String) {
88
+ stopServer()
89
+ if let port = port {
90
+ var error: NSError?
91
+ server.startListening(&error, portNumber: UInt(port))
92
+ if (error != nil) {
93
+ sendEvent("onStatusUpdate", [
94
+ "status": "ERROR",
95
+ "message": error?.localizedDescription ?? "Unknown error starting server"
96
+ ])
97
+ } else {
98
+ beginBackgroundTask()
99
+ sendEvent("onStatusUpdate", [
100
+ "status": status,
101
+ "message": message
102
+ ])
103
+ }
104
+ } else {
105
+ sendEvent("onStatusUpdate", [
106
+ "status": "ERROR",
107
+ "message": "Can't start server with port configured"
108
+ ])
109
+ }
110
+ }
111
+
112
+ private func stopServer(status: String? = nil, message: String? = nil) {
113
+ server.stopListening()
114
+ endBackgroundTask()
115
+ if let status = status, let message = message {
116
+ sendEvent("onStatusUpdate", [
117
+ "status": status,
118
+ "message": message
119
+ ])
120
+ }
121
+ }
122
+
123
+ private func beginBackgroundTask() {
124
+ if (bgTaskIdentifier == UIBackgroundTaskIdentifier.invalid) {
125
+ self.bgTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "BgTask", expirationHandler: {
126
+ self.stopServer(status: "PAUSED", message: "Server paused")
127
+ })
128
+ }
129
+ }
130
+
131
+ private func endBackgroundTask() {
132
+ if (bgTaskIdentifier != UIBackgroundTaskIdentifier.invalid) {
133
+ UIApplication.shared.endBackgroundTask(bgTaskIdentifier)
134
+ bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid
135
+ }
136
+ }
137
+ }
138
+
139
+ extension Dictionary {
140
+ var jsonString: String {
141
+ guard let data = try? JSONSerialization.data(withJSONObject: self) else {
142
+ return "{}";
143
+ }
144
+ return String(data: data, encoding: .utf8) ?? "{}"
145
+ }
146
+ }
147
+
148
+ extension CRHTTPMethod {
149
+ func toString() -> String {
150
+ switch self {
151
+ case .post:
152
+ return "POST"
153
+ case .put:
154
+ return "PUT"
155
+ case .delete:
156
+ return "DELETE"
157
+ case .options:
158
+ return "OPTIONS"
159
+ default:
160
+ return "GET"
161
+ }
162
+ }
163
+
164
+ static func fromString(_ string: String) -> Self {
165
+ var httpMethod: CRHTTPMethod
166
+ switch (string) {
167
+ case "POST":
168
+ httpMethod = .post
169
+ case "PUT":
170
+ httpMethod = .put
171
+ case "DELETE":
172
+ httpMethod = .delete
173
+ case "OPTIONS":
174
+ httpMethod = .options
175
+ default:
176
+ httpMethod = .get
177
+ }
178
+ return httpMethod
179
+ }
180
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@kccd/expo-http-server",
3
+ "version": "0.1.13",
4
+ "description": "A simple HTTP server expo module (iOS/Android)",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "expo-module build",
9
+ "clean": "expo-module clean",
10
+ "lint": "expo-module lint",
11
+ "test": "expo-module test",
12
+ "open:ios": "open -a \"Xcode\" example/ios",
13
+ "open:android": "open -a \"Android Studio\" example/android"
14
+ },
15
+ "keywords": [
16
+ "react-native",
17
+ "expo",
18
+ "expo-http-server",
19
+ "ExpoHttpServer"
20
+ ],
21
+ "repository": "https://github.com/simonsturge/expo-http-server",
22
+ "bugs": {
23
+ "url": "https://github.com/simonsturge/expo-http-server/issues"
24
+ },
25
+ "author": "Simon <development@simonsturge.com> (https://github.com/simonsturge)",
26
+ "license": "MIT",
27
+ "homepage": "https://github.com/simonsturge/expo-http-server#readme",
28
+ "dependencies": {},
29
+ "devDependencies": {
30
+ "@types/react": "^18.0.25",
31
+ "expo-module-scripts": "^3.4.0",
32
+ "expo-modules-core": "^1.11.8"
33
+ },
34
+ "peerDependencies": {
35
+ "expo": "*",
36
+ "react": "*",
37
+ "react-native": "*"
38
+ }
39
+ }
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from "expo-modules-core";
2
+
3
+ export default requireNativeModule("ExpoHttpServer");
package/src/index.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { EventEmitter } from "expo-modules-core";
2
+
3
+ import ExpoHttpServerModule from "./ExpoHttpServerModule";
4
+
5
+ const emitter = new EventEmitter(ExpoHttpServerModule);
6
+ const requestCallbacks: Callback[] = [];
7
+
8
+ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS";
9
+ /**
10
+ * PAUSED AND RESUMED are iOS only
11
+ */
12
+ export type Status = "STARTED" | "PAUSED" | "RESUMED" | "STOPPED" | "ERROR";
13
+
14
+ export interface StatusEvent {
15
+ status: Status;
16
+ message: string;
17
+ }
18
+
19
+ export interface RequestEvent {
20
+ uuid: string;
21
+ requestId: string;
22
+ method: string;
23
+ path: string;
24
+ body: string;
25
+ headersJson: string;
26
+ paramsJson: string;
27
+ cookiesJson: string;
28
+ }
29
+
30
+ export interface Response {
31
+ statusCode?: number;
32
+ statusDescription?: string;
33
+ contentType?: string;
34
+ headers?: Record<string, string>;
35
+ body?: string;
36
+ }
37
+
38
+ export interface Callback {
39
+ method: string;
40
+ path: string;
41
+ uuid: string;
42
+ callback: (request: RequestEvent) => Promise<Response>;
43
+ }
44
+
45
+ export const start = () => {
46
+ emitter.addListener<RequestEvent>("onRequest", async (event) => {
47
+ const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);
48
+ if (!responseHandler) {
49
+ ExpoHttpServerModule.respond(
50
+ event.requestId,
51
+ 404,
52
+ "Not Found",
53
+ "application/json",
54
+ {},
55
+ JSON.stringify({ error: "Handler not found" }),
56
+ );
57
+ return;
58
+ }
59
+ const response = await responseHandler.callback(event);
60
+ ExpoHttpServerModule.respond(
61
+ event.requestId,
62
+ response.statusCode || 200,
63
+ response.statusDescription || "OK",
64
+ response.contentType || "application/json",
65
+ response.headers || {},
66
+ response.body || "{}",
67
+ );
68
+ });
69
+ ExpoHttpServerModule.start();
70
+ };
71
+
72
+ export const route = (
73
+ path: string,
74
+ method: HttpMethod,
75
+ callback: (request: RequestEvent) => Promise<Response>,
76
+ ) => {
77
+ const uuid = Math.random().toString(16).slice(2);
78
+ requestCallbacks.push({
79
+ method,
80
+ path,
81
+ uuid,
82
+ callback,
83
+ });
84
+ ExpoHttpServerModule.route(path, method, uuid);
85
+ };
86
+
87
+ export const setup = (
88
+ port: number,
89
+ onStatusUpdate?: (event: StatusEvent) => void,
90
+ ) => {
91
+ if (onStatusUpdate) {
92
+ emitter.addListener<StatusEvent>("onStatusUpdate", async (event) => {
93
+ onStatusUpdate(event);
94
+ });
95
+ }
96
+ ExpoHttpServerModule.setup(port);
97
+ };
98
+
99
+ export const stop = () => ExpoHttpServerModule.stop();
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ // @generated by expo-module-scripts
2
+ {
3
+ "extends": "expo-module-scripts/tsconfig.base",
4
+ "compilerOptions": {
5
+ "outDir": "./build"
6
+ },
7
+ "include": ["./src"],
8
+ "exclude": ["**/__mocks__/*", "**/__tests__/*"]
9
+ }