edge-core-js 2.41.1 → 2.41.3
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/CHANGELOG.md +8 -0
- package/android/src/main/assets/edge-core-js/edge-core.js +1 -1
- package/android/src/main/assets/edge-core-js/index.html +1 -1
- package/android/src/main/java/app/edge/reactnative/core/EdgeCoreModule.java +31 -0
- package/android/src/main/java/app/edge/reactnative/core/EdgeCorePackage.java +1 -1
- package/android/src/main/java/app/edge/reactnative/core/EdgeCoreWebView.java +117 -65
- package/android/src/main/java/app/edge/reactnative/core/LocalContentWebViewClient.java +79 -0
- package/edge-core-js.podspec +3 -1
- package/ios/EdgeAssetsSchemeHandler.swift +175 -0
- package/ios/EdgeCoreModule.m +4 -0
- package/ios/EdgeCoreModule.swift +30 -0
- package/ios/EdgeCoreWebView.swift +32 -45
- package/lib/io/react-native/react-native-worker.js +3 -31
- package/lib/libs.d.js +12 -0
- package/lib/react-native.js +41 -4
- package/lib/util/nym.js +1 -8
- package/package.json +1 -1
- package/android/src/main/java/app/edge/reactnative/core/BundleHTTPServer.java +0 -318
- package/ios/BundleHTTPServer.swift +0 -394
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import WebKit
|
|
2
1
|
import Foundation
|
|
3
|
-
import
|
|
2
|
+
import WebKit
|
|
4
3
|
|
|
4
|
+
/// Default URL for the WebView
|
|
5
|
+
let DEFAULT_SOURCE = "\(BUNDLE_BASE_URI)/edge-core-js.bundle/index.html"
|
|
6
|
+
|
|
7
|
+
/// A WebView that loads edge-core-js content using a custom URL scheme handler.
|
|
8
|
+
///
|
|
9
|
+
/// Uses WKURLSchemeHandler to serve local assets via custom URLs (edgebundle://edge.bundle/...),
|
|
10
|
+
/// which provides a proper non-null origin for same-origin policy compliance
|
|
11
|
+
/// without requiring a local HTTP server.
|
|
12
|
+
///
|
|
13
|
+
/// Includes Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers
|
|
14
|
+
/// required for SharedArrayBuffer support (needed by mixFetch web workers).
|
|
15
|
+
///
|
|
16
|
+
/// Note: WKURLSchemeHandler only handles requests within this specific WKWebView instance.
|
|
17
|
+
/// It does not register a system-wide URL scheme - other apps cannot access this handler.
|
|
5
18
|
class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
|
|
6
19
|
var native = EdgeNative()
|
|
7
20
|
var webView: WKWebView?
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
private var serverReady = false
|
|
11
|
-
|
|
12
|
-
// react api--------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
// MARK: - React API
|
|
13
23
|
|
|
14
24
|
@objc var onMessage: RCTDirectEventBlock?
|
|
15
25
|
@objc var onScriptError: RCTDirectEventBlock?
|
|
@@ -32,7 +42,7 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
|
|
|
32
42
|
completionHandler: { result, error in return })
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
//
|
|
45
|
+
// MARK: - View API
|
|
36
46
|
|
|
37
47
|
required init?(coder: NSCoder) {
|
|
38
48
|
return nil
|
|
@@ -41,34 +51,23 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
|
|
|
41
51
|
override init(frame: CGRect) {
|
|
42
52
|
super.init(frame: frame)
|
|
43
53
|
|
|
44
|
-
// Set up our native bridge:
|
|
54
|
+
// Set up our native bridge and custom URL scheme handler:
|
|
45
55
|
let configuration = WKWebViewConfiguration()
|
|
46
56
|
configuration.userContentController = WKUserContentController()
|
|
47
57
|
configuration.userContentController.add(self, name: "edgeCore")
|
|
48
58
|
|
|
59
|
+
// Register custom URL scheme handler BEFORE creating WKWebView
|
|
60
|
+
let schemeHandler = EdgeAssetsSchemeHandler()
|
|
61
|
+
configuration.setURLSchemeHandler(schemeHandler, forURLScheme: EDGE_SCHEME)
|
|
62
|
+
|
|
49
63
|
// Set up the WKWebView child:
|
|
50
64
|
let webView = WKWebView(frame: bounds, configuration: configuration)
|
|
51
65
|
webView.navigationDelegate = self
|
|
52
66
|
addSubview(webView)
|
|
53
67
|
self.webView = webView
|
|
54
68
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
self.httpServer = server
|
|
58
|
-
server.start { [weak self] result in
|
|
59
|
-
DispatchQueue.main.async {
|
|
60
|
-
switch result {
|
|
61
|
-
case .success(let port):
|
|
62
|
-
self?.serverPort = port
|
|
63
|
-
self?.serverReady = true
|
|
64
|
-
// Now that the server is ready with its assigned port, load the page
|
|
65
|
-
self?.visitPage()
|
|
66
|
-
case .failure(let error):
|
|
67
|
-
print("Failed to start HTTP server: \(error)")
|
|
68
|
-
// Server failed to start - the WebView won't be able to load local content
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
69
|
+
// Scheme handler is ready immediately - no async startup needed
|
|
70
|
+
visitPage()
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
override func layoutSubviews() {
|
|
@@ -84,13 +83,9 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
|
|
|
84
83
|
webView.removeFromSuperview()
|
|
85
84
|
self.webView = nil
|
|
86
85
|
}
|
|
87
|
-
|
|
88
|
-
// Stop the HTTP server when view is removed
|
|
89
|
-
httpServer?.stop()
|
|
90
|
-
httpServer = nil
|
|
91
86
|
}
|
|
92
87
|
|
|
93
|
-
//
|
|
88
|
+
// MARK: - Navigation Delegate
|
|
94
89
|
|
|
95
90
|
func webView(
|
|
96
91
|
_: WKWebView,
|
|
@@ -106,6 +101,8 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
|
|
|
106
101
|
visitPage()
|
|
107
102
|
}
|
|
108
103
|
|
|
104
|
+
// MARK: - Script Message Handler
|
|
105
|
+
|
|
109
106
|
func userContentController(
|
|
110
107
|
_: WKUserContentController,
|
|
111
108
|
didReceive scriptMessage: WKScriptMessage
|
|
@@ -135,7 +132,7 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
|
|
|
135
132
|
}
|
|
136
133
|
}
|
|
137
134
|
|
|
138
|
-
//
|
|
135
|
+
// MARK: - Utilities
|
|
139
136
|
|
|
140
137
|
func handleMessage(
|
|
141
138
|
_ name: String, args: NSArray
|
|
@@ -150,12 +147,6 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
|
|
|
150
147
|
}
|
|
151
148
|
}
|
|
152
149
|
|
|
153
|
-
/// Returns the base URL for the local bundle HTTP server, or nil if the server isn't ready.
|
|
154
|
-
func defaultSource() -> String? {
|
|
155
|
-
guard serverReady else { return nil }
|
|
156
|
-
return "http://127.0.0.1:\(serverPort)/index.html"
|
|
157
|
-
}
|
|
158
|
-
|
|
159
150
|
func stringify(_ raw: Any?) -> String {
|
|
160
151
|
if let value = raw,
|
|
161
152
|
let data = try? JSONSerialization.data(
|
|
@@ -172,18 +163,14 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
|
|
|
172
163
|
|
|
173
164
|
func visitPage() {
|
|
174
165
|
// If source is set, use it directly (e.g., webpack dev server for debugging)
|
|
175
|
-
// Otherwise, use the
|
|
166
|
+
// Otherwise, use the custom URL scheme handler
|
|
176
167
|
let baseUrl: String
|
|
177
168
|
if let src = source, !src.isEmpty {
|
|
178
169
|
baseUrl = src
|
|
179
170
|
} else {
|
|
180
|
-
|
|
181
|
-
print("EdgeCoreWebView: visitPage called before server is ready")
|
|
182
|
-
return
|
|
183
|
-
}
|
|
184
|
-
baseUrl = defaultUrl
|
|
171
|
+
baseUrl = DEFAULT_SOURCE
|
|
185
172
|
}
|
|
186
|
-
|
|
173
|
+
|
|
187
174
|
guard let url = URL(string: baseUrl) else {
|
|
188
175
|
print("EdgeCoreWebView: Invalid URL string: \(baseUrl)")
|
|
189
176
|
return
|
|
@@ -73,35 +73,6 @@ window.addEdgeCorePlugins = addEdgeCorePlugins
|
|
|
73
73
|
window.nativeBridge = nativeBridge
|
|
74
74
|
window.reactBridge = reactBridge
|
|
75
75
|
|
|
76
|
-
/**
|
|
77
|
-
* Convert a file:// URI to a domain-relative path served by our local bundle server.
|
|
78
|
-
* This is needed because cross-origin isolation (COOP/COEP) blocks file:// loads.
|
|
79
|
-
* Using domain-relative paths ensures plugins load from the same origin as the page
|
|
80
|
-
* (localhost:3693 in production, localhost:8080 in debug mode).
|
|
81
|
-
*/
|
|
82
|
-
function convertPluginUri(uri) {
|
|
83
|
-
// If already an HTTP URI, use as-is (e.g., custom bundles from metro bundler)
|
|
84
|
-
if (uri.startsWith('http://') || uri.startsWith('https://')) {
|
|
85
|
-
return uri
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Convert file:// URIs to domain-relative paths
|
|
89
|
-
// Extract the bundle path (e.g., "edge-currency-accountbased.bundle/edge-currency-accountbased.js")
|
|
90
|
-
const bundleMatch = uri.match(/([^/]+\.bundle\/[^/]+\.js)$/)
|
|
91
|
-
if (bundleMatch != null) {
|
|
92
|
-
return `/plugin/${bundleMatch[1]}`
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Fallback: try to extract just the filename
|
|
96
|
-
const fileMatch = uri.match(/([^/]+\.js)$/)
|
|
97
|
-
if (fileMatch != null) {
|
|
98
|
-
return `/plugin/${fileMatch[1]}`
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// If we can't parse it, return as-is (will likely fail, but provides debugging info)
|
|
102
|
-
return uri
|
|
103
|
-
}
|
|
104
|
-
|
|
105
76
|
function loadPlugins(pluginUris) {
|
|
106
77
|
const { head } = window.document
|
|
107
78
|
if (head == null || pluginUris.length === 0) {
|
|
@@ -120,7 +91,7 @@ function loadPlugins(pluginUris) {
|
|
|
120
91
|
script.addEventListener('load', handleLoad)
|
|
121
92
|
script.charset = 'utf-8'
|
|
122
93
|
script.defer = true
|
|
123
|
-
script.src =
|
|
94
|
+
script.src = uri
|
|
124
95
|
head.appendChild(script)
|
|
125
96
|
}
|
|
126
97
|
}
|
|
@@ -208,10 +179,11 @@ async function makeIo(logBackend) {
|
|
|
208
179
|
// Ensure mixFetch is initialized before use
|
|
209
180
|
await initMixFetch(log)
|
|
210
181
|
// Use queued fetch to handle mixFetch's one-request-per-host limitation
|
|
211
|
-
|
|
182
|
+
const response = await queueMixFetch(uri, {
|
|
212
183
|
...opts,
|
|
213
184
|
mode: 'unsafe-ignore-cors'
|
|
214
185
|
})
|
|
186
|
+
return response
|
|
215
187
|
}
|
|
216
188
|
if (corsBypass === 'always') {
|
|
217
189
|
return await nativeFetch(uri, opts)
|
package/lib/libs.d.js
CHANGED
package/lib/react-native.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const _jsxFileName = "src/react-native.tsx";import { asObject, asString } from 'cleaners'
|
|
2
2
|
import { makeReactNativeDisklet } from 'disklet'
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { NativeModules } from 'react-native'
|
|
4
5
|
import { base64 } from 'rfc4648'
|
|
5
6
|
import { bridgifyObject } from 'yaob'
|
|
6
7
|
|
|
@@ -22,6 +23,15 @@ import { timeout } from './util/promise'
|
|
|
22
23
|
export { makeFakeIo } from './core/fake/fake-io'
|
|
23
24
|
export * from './types/types'
|
|
24
25
|
|
|
26
|
+
const { EdgeCoreModule } = NativeModules
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Constants exported from native:
|
|
30
|
+
* - bundleBaseUri: iOS "edgebundle://edge.bundle", Android "https://edge.bundle"
|
|
31
|
+
* - rootBaseUri: iOS "file:///path/to/Edge.app/", Android "file:///android_asset/"
|
|
32
|
+
*/
|
|
33
|
+
const { bundleBaseUri, rootBaseUri } = EdgeCoreModule.getConstants()
|
|
34
|
+
|
|
25
35
|
function defaultOnError(error) {
|
|
26
36
|
console.error(error)
|
|
27
37
|
}
|
|
@@ -69,7 +79,7 @@ export function MakeEdgeContext(props) {
|
|
|
69
79
|
const context = await root.makeEdgeContext(
|
|
70
80
|
bridgifyNativeIo(nativeIo),
|
|
71
81
|
bridgifyLogBackend({ crashReporter, onLog }),
|
|
72
|
-
pluginUris,
|
|
82
|
+
pluginUris.map(normalizePluginUri),
|
|
73
83
|
{
|
|
74
84
|
airbitzSupport,
|
|
75
85
|
apiKey,
|
|
@@ -91,7 +101,7 @@ export function MakeEdgeContext(props) {
|
|
|
91
101
|
}
|
|
92
102
|
)
|
|
93
103
|
await onLoad(context)
|
|
94
|
-
}, __self: this, __source: {fileName: _jsxFileName, lineNumber:
|
|
104
|
+
}, __self: this, __source: {fileName: _jsxFileName, lineNumber: 74}}
|
|
95
105
|
)
|
|
96
106
|
)
|
|
97
107
|
}
|
|
@@ -121,11 +131,11 @@ export function MakeFakeEdgeWorld(props) {
|
|
|
121
131
|
const fakeWorld = await root.makeFakeEdgeWorld(
|
|
122
132
|
bridgifyNativeIo(nativeIo),
|
|
123
133
|
bridgifyLogBackend({ crashReporter, onLog }),
|
|
124
|
-
pluginUris,
|
|
134
|
+
pluginUris.map(normalizePluginUri),
|
|
125
135
|
props.users
|
|
126
136
|
)
|
|
127
137
|
await onLoad(fakeWorld)
|
|
128
|
-
}, __self: this, __source: {fileName: _jsxFileName, lineNumber:
|
|
138
|
+
}, __self: this, __source: {fileName: _jsxFileName, lineNumber: 126}}
|
|
129
139
|
)
|
|
130
140
|
)
|
|
131
141
|
}
|
|
@@ -217,3 +227,30 @@ export async function fetchLoginMessages(
|
|
|
217
227
|
})
|
|
218
228
|
})
|
|
219
229
|
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Convert a legacy plugin URI to an absolute URL that can be loaded by the WebView.
|
|
233
|
+
*
|
|
234
|
+
* Handles `file://` URIs by replacing the rootBaseUri prefix with bundleBaseUri:
|
|
235
|
+
* - `file:///android_asset/folder/file.js` → `{bundleBaseUri}/folder/file.js`
|
|
236
|
+
* - `file:///path/to/Edge.app/name.bundle/file.js` → `{bundleBaseUri}/name.bundle/file.js`
|
|
237
|
+
* - `edge-core/plugin-bundle.js` → `{bundleBaseUri}/edge-core/plugin-bundle.js`
|
|
238
|
+
*
|
|
239
|
+
* Full URLs are returned unchanged (http, https, edgebundle).
|
|
240
|
+
*/
|
|
241
|
+
function normalizePluginUri(uri) {
|
|
242
|
+
// Handle file:// URIs that start with our root base URI
|
|
243
|
+
if (uri.startsWith(rootBaseUri)) {
|
|
244
|
+
const relativePath = uri.slice(rootBaseUri.length)
|
|
245
|
+
return `${bundleBaseUri}/${relativePath}`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Handle relative paths (no schema) like "edge-core/plugin-bundle.js"
|
|
249
|
+
if (!uri.includes('://') && uri.match(/^[^/]+\/[^/]+\.(js)$/) != null) {
|
|
250
|
+
// Relative paths are return as absolute paths
|
|
251
|
+
return `${bundleBaseUri}/${uri}`
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Full URLs and anything else pass through unchanged
|
|
255
|
+
return uri
|
|
256
|
+
}
|
package/lib/util/nym.js
CHANGED
|
@@ -11,14 +11,7 @@
|
|
|
11
11
|
* Configuration options for the NYM mixFetch client.
|
|
12
12
|
*/
|
|
13
13
|
export const mixFetchOptions = {
|
|
14
|
-
|
|
15
|
-
preferredNetworkRequester:
|
|
16
|
-
'5x6q9UfVHs5AohKMUqeivj7a556kVVy7QwoKige8xHxh.6CFoB3kJaDbYz6oafPJxNxNjzahpT2NtgtytcSyN9EvF@5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8',
|
|
17
|
-
mixFetchOverride: {
|
|
18
|
-
requestTimeoutMs: 60000
|
|
19
|
-
},
|
|
20
|
-
forceTls: true, // force WSS
|
|
21
|
-
extra: {}
|
|
14
|
+
forceTls: true // force WSS
|
|
22
15
|
}
|
|
23
16
|
|
|
24
17
|
// MixFetch initialization state
|
package/package.json
CHANGED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
package app.edge.reactnative.core;
|
|
2
|
-
|
|
3
|
-
import android.content.Context;
|
|
4
|
-
import android.content.res.AssetManager;
|
|
5
|
-
import android.util.Log;
|
|
6
|
-
|
|
7
|
-
import java.io.BufferedReader;
|
|
8
|
-
import java.io.ByteArrayOutputStream;
|
|
9
|
-
import java.io.IOException;
|
|
10
|
-
import java.io.InputStream;
|
|
11
|
-
import java.io.InputStreamReader;
|
|
12
|
-
import java.io.OutputStream;
|
|
13
|
-
import java.net.InetAddress;
|
|
14
|
-
import java.net.ServerSocket;
|
|
15
|
-
import java.net.Socket;
|
|
16
|
-
import java.util.concurrent.ExecutorService;
|
|
17
|
-
import java.util.concurrent.Executors;
|
|
18
|
-
import java.util.concurrent.atomic.AtomicBoolean;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* A simple HTTP server that serves files from the Android assets folder.
|
|
22
|
-
* Includes Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers
|
|
23
|
-
* required for SharedArrayBuffer support (needed by mixFetch web workers).
|
|
24
|
-
*
|
|
25
|
-
* Security: Binds to loopback interface only (127.0.0.1) on an ephemeral port
|
|
26
|
-
* to prevent access from other devices on the network.
|
|
27
|
-
*/
|
|
28
|
-
class BundleHTTPServer {
|
|
29
|
-
private static final String TAG = "BundleHTTPServer";
|
|
30
|
-
|
|
31
|
-
private final Context mContext;
|
|
32
|
-
private ServerSocket mServerSocket;
|
|
33
|
-
private ExecutorService mExecutor;
|
|
34
|
-
private final AtomicBoolean mRunning = new AtomicBoolean(false);
|
|
35
|
-
private int mAssignedPort = 0;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Callback interface for server start events.
|
|
39
|
-
*/
|
|
40
|
-
public interface OnServerStartedListener {
|
|
41
|
-
void onServerStarted(int port);
|
|
42
|
-
void onServerError(Exception error);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
public BundleHTTPServer(Context context) {
|
|
46
|
-
mContext = context;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Returns the assigned port after the server has started.
|
|
51
|
-
* Returns 0 if the server hasn't started yet.
|
|
52
|
-
*/
|
|
53
|
-
public int getAssignedPort() {
|
|
54
|
-
return mAssignedPort;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Starts the HTTP server on an ephemeral port bound to loopback only (127.0.0.1).
|
|
59
|
-
* This prevents other devices on the network from connecting to the server.
|
|
60
|
-
*
|
|
61
|
-
* @param listener Callback to receive the assigned port or error
|
|
62
|
-
*/
|
|
63
|
-
public void start(OnServerStartedListener listener) {
|
|
64
|
-
if (mRunning.get()) {
|
|
65
|
-
if (listener != null && mAssignedPort > 0) {
|
|
66
|
-
listener.onServerStarted(mAssignedPort);
|
|
67
|
-
}
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
mExecutor = Executors.newCachedThreadPool();
|
|
72
|
-
mRunning.set(true);
|
|
73
|
-
|
|
74
|
-
new Thread(() -> {
|
|
75
|
-
try {
|
|
76
|
-
// Bind to loopback only (127.0.0.1) on port 0 (ephemeral)
|
|
77
|
-
// This prevents access from other devices on the network
|
|
78
|
-
InetAddress loopback = InetAddress.getByName("127.0.0.1");
|
|
79
|
-
mServerSocket = new ServerSocket(0, 50, loopback);
|
|
80
|
-
|
|
81
|
-
// Get the assigned ephemeral port
|
|
82
|
-
mAssignedPort = mServerSocket.getLocalPort();
|
|
83
|
-
Log.d(TAG, "BundleHttpServer ready on 127.0.0.1:" + mAssignedPort);
|
|
84
|
-
|
|
85
|
-
// Notify listener of the assigned port
|
|
86
|
-
if (listener != null) {
|
|
87
|
-
listener.onServerStarted(mAssignedPort);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
while (mRunning.get()) {
|
|
91
|
-
try {
|
|
92
|
-
Socket clientSocket = mServerSocket.accept();
|
|
93
|
-
mExecutor.execute(() -> handleConnection(clientSocket));
|
|
94
|
-
} catch (IOException e) {
|
|
95
|
-
if (mRunning.get()) {
|
|
96
|
-
Log.e(TAG, "Error accepting connection: " + e.getMessage());
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
} catch (IOException e) {
|
|
101
|
-
Log.e(TAG, "Failed to start HTTP server: " + e.getMessage());
|
|
102
|
-
mRunning.set(false);
|
|
103
|
-
if (listener != null) {
|
|
104
|
-
listener.onServerError(e);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}).start();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
public void stop() {
|
|
111
|
-
mRunning.set(false);
|
|
112
|
-
|
|
113
|
-
if (mServerSocket != null) {
|
|
114
|
-
try {
|
|
115
|
-
mServerSocket.close();
|
|
116
|
-
} catch (IOException e) {
|
|
117
|
-
Log.e(TAG, "Error closing server socket: " + e.getMessage());
|
|
118
|
-
}
|
|
119
|
-
mServerSocket = null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (mExecutor != null) {
|
|
123
|
-
mExecutor.shutdown();
|
|
124
|
-
mExecutor = null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
Log.d(TAG, "BundleHttpServer stopped");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
private void handleConnection(Socket clientSocket) {
|
|
131
|
-
try {
|
|
132
|
-
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
|
133
|
-
OutputStream output = clientSocket.getOutputStream();
|
|
134
|
-
|
|
135
|
-
// Read the request line
|
|
136
|
-
String requestLine = reader.readLine();
|
|
137
|
-
if (requestLine == null || requestLine.isEmpty()) {
|
|
138
|
-
clientSocket.close();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Parse method and path
|
|
143
|
-
String[] parts = requestLine.split(" ");
|
|
144
|
-
if (parts.length < 2) {
|
|
145
|
-
sendResponse(output, 400, "Bad Request", "text/plain", "Bad Request".getBytes());
|
|
146
|
-
clientSocket.close();
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
String method = parts[0];
|
|
151
|
-
String path = parts[1];
|
|
152
|
-
|
|
153
|
-
// Read and discard headers
|
|
154
|
-
String line;
|
|
155
|
-
while ((line = reader.readLine()) != null && !line.isEmpty()) {
|
|
156
|
-
// Just consume headers
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Only support GET
|
|
160
|
-
if (!method.equals("GET")) {
|
|
161
|
-
sendResponse(output, 405, "Method Not Allowed", "text/plain", "Method Not Allowed".getBytes());
|
|
162
|
-
clientSocket.close();
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Remove query parameters
|
|
167
|
-
int queryIndex = path.indexOf('?');
|
|
168
|
-
if (queryIndex > 0) {
|
|
169
|
-
path = path.substring(0, queryIndex);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Require explicit file name - no auto-matching for root path
|
|
173
|
-
if (path.equals("/")) {
|
|
174
|
-
sendResponse(output, 404, "Not Found", "text/plain", "Not Found".getBytes());
|
|
175
|
-
clientSocket.close();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Handle plugin bundle requests (e.g., /plugin/edge-currency-accountbased.bundle/edge-currency-accountbased.js)
|
|
180
|
-
if (path.startsWith("/plugin/")) {
|
|
181
|
-
String pluginPath = path.substring("/plugin/".length());
|
|
182
|
-
servePluginFile(output, pluginPath);
|
|
183
|
-
clientSocket.close();
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Remove leading slash and prepend assets path
|
|
188
|
-
String assetPath = "edge-core-js" + path;
|
|
189
|
-
|
|
190
|
-
// Try to read the asset (using try-with-resources to ensure stream is closed)
|
|
191
|
-
try {
|
|
192
|
-
AssetManager assetManager = mContext.getAssets();
|
|
193
|
-
try (InputStream inputStream = assetManager.open(assetPath)) {
|
|
194
|
-
byte[] data = readAllBytes(inputStream);
|
|
195
|
-
String mimeType = getMimeType(path);
|
|
196
|
-
sendResponse(output, 200, "OK", mimeType, data);
|
|
197
|
-
}
|
|
198
|
-
} catch (IOException e) {
|
|
199
|
-
Log.d(TAG, "File not found: " + assetPath);
|
|
200
|
-
sendResponse(output, 404, "Not Found", "text/plain", "Not Found".getBytes());
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
clientSocket.close();
|
|
204
|
-
} catch (IOException e) {
|
|
205
|
-
Log.e(TAG, "Error handling connection: " + e.getMessage());
|
|
206
|
-
try {
|
|
207
|
-
clientSocket.close();
|
|
208
|
-
} catch (IOException ignored) {
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private void servePluginFile(OutputStream output, String pluginPath) throws IOException {
|
|
214
|
-
AssetManager assetManager = mContext.getAssets();
|
|
215
|
-
byte[] data = null;
|
|
216
|
-
|
|
217
|
-
// Plugin path format: "edge-currency-accountbased.bundle/edge-currency-accountbased.js"
|
|
218
|
-
// or just: "plugin-bundle.js"
|
|
219
|
-
// Try multiple asset path patterns
|
|
220
|
-
String[] pathsToTry;
|
|
221
|
-
|
|
222
|
-
if (pluginPath.contains(".bundle/")) {
|
|
223
|
-
// Extract bundle name and file name
|
|
224
|
-
String[] parts = pluginPath.split("\\.bundle/");
|
|
225
|
-
if (parts.length >= 2) {
|
|
226
|
-
String bundleName = parts[0];
|
|
227
|
-
String fileName = parts[1];
|
|
228
|
-
pathsToTry = new String[] {
|
|
229
|
-
pluginPath, // As-is
|
|
230
|
-
bundleName + "/" + fileName, // Without .bundle
|
|
231
|
-
bundleName + ".bundle/" + fileName, // With .bundle as folder
|
|
232
|
-
fileName, // Just the filename
|
|
233
|
-
"edge-core/" + pluginPath, // In edge-core assets folder
|
|
234
|
-
"edge-core-js/" + pluginPath // In edge-core-js assets folder
|
|
235
|
-
};
|
|
236
|
-
} else {
|
|
237
|
-
pathsToTry = new String[] { pluginPath, "edge-core/" + pluginPath, "edge-core-js/" + pluginPath };
|
|
238
|
-
}
|
|
239
|
-
} else {
|
|
240
|
-
// Just a filename like "plugin-bundle.js"
|
|
241
|
-
// Try to find it in various asset locations
|
|
242
|
-
String fileName = pluginPath;
|
|
243
|
-
int dotIndex = fileName.lastIndexOf('.');
|
|
244
|
-
if (dotIndex > 0) {
|
|
245
|
-
String baseName = fileName.substring(0, dotIndex);
|
|
246
|
-
pathsToTry = new String[] {
|
|
247
|
-
"edge-core/" + pluginPath, // e.g., edge-core/plugin-bundle.js (app's plugin bundle)
|
|
248
|
-
"edge-core-js/" + pluginPath, // e.g., edge-core-js/plugin-bundle.js
|
|
249
|
-
baseName + ".bundle/" + fileName, // e.g., plugin-bundle.bundle/plugin-bundle.js
|
|
250
|
-
baseName + "/" + fileName, // e.g., plugin-bundle/plugin-bundle.js
|
|
251
|
-
pluginPath // Just the filename
|
|
252
|
-
};
|
|
253
|
-
} else {
|
|
254
|
-
pathsToTry = new String[] { "edge-core/" + pluginPath, "edge-core-js/" + pluginPath, pluginPath };
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
for (String assetPath : pathsToTry) {
|
|
259
|
-
try (InputStream inputStream = assetManager.open(assetPath)) {
|
|
260
|
-
data = readAllBytes(inputStream);
|
|
261
|
-
Log.d(TAG, "Found plugin at: " + assetPath);
|
|
262
|
-
break;
|
|
263
|
-
} catch (IOException e) {
|
|
264
|
-
// Try next path
|
|
265
|
-
Log.d(TAG, "Plugin not found at: " + assetPath);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (data != null) {
|
|
270
|
-
String mimeType = getMimeType(pluginPath);
|
|
271
|
-
sendResponse(output, 200, "OK", mimeType, data);
|
|
272
|
-
} else {
|
|
273
|
-
Log.e(TAG, "Plugin file not found: " + pluginPath);
|
|
274
|
-
sendResponse(output, 404, "Not Found", "text/plain", ("Not Found: " + pluginPath).getBytes());
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
private void sendResponse(OutputStream output, int code, String status, String contentType, byte[] body)
|
|
279
|
-
throws IOException {
|
|
280
|
-
StringBuilder headers = new StringBuilder();
|
|
281
|
-
headers.append("HTTP/1.1 ").append(code).append(" ").append(status).append("\r\n");
|
|
282
|
-
headers.append("Content-Type: ").append(contentType).append("\r\n");
|
|
283
|
-
headers.append("Content-Length: ").append(body.length).append("\r\n");
|
|
284
|
-
headers.append("Connection: close\r\n");
|
|
285
|
-
headers.append("Server: EdgeCoreBundleServer/1.0\r\n");
|
|
286
|
-
// Cross-origin isolation headers required for SharedArrayBuffer (needed by mixFetch web workers)
|
|
287
|
-
headers.append("Cross-Origin-Opener-Policy: same-origin\r\n");
|
|
288
|
-
headers.append("Cross-Origin-Embedder-Policy: require-corp\r\n");
|
|
289
|
-
headers.append("\r\n");
|
|
290
|
-
|
|
291
|
-
output.write(headers.toString().getBytes("UTF-8"));
|
|
292
|
-
output.write(body);
|
|
293
|
-
output.flush();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
private byte[] readAllBytes(InputStream inputStream) throws IOException {
|
|
297
|
-
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
298
|
-
byte[] chunk = new byte[4096];
|
|
299
|
-
int bytesRead;
|
|
300
|
-
while ((bytesRead = inputStream.read(chunk)) != -1) {
|
|
301
|
-
buffer.write(chunk, 0, bytesRead);
|
|
302
|
-
}
|
|
303
|
-
return buffer.toByteArray();
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// We only serve HTML, JS, and WASM files
|
|
307
|
-
private String getMimeType(String path) {
|
|
308
|
-
String lowerPath = path.toLowerCase();
|
|
309
|
-
if (lowerPath.endsWith(".html") || lowerPath.endsWith(".htm")) {
|
|
310
|
-
return "text/html";
|
|
311
|
-
} else if (lowerPath.endsWith(".js")) {
|
|
312
|
-
return "application/javascript";
|
|
313
|
-
} else if (lowerPath.endsWith(".wasm")) {
|
|
314
|
-
return "application/wasm";
|
|
315
|
-
}
|
|
316
|
-
return "application/octet-stream";
|
|
317
|
-
}
|
|
318
|
-
}
|