birdofpreyru / react-native-static-server

Embedded HTTP server for React Native
https://dr.pogodin.studio/docs/react-native-static-server
Other
135 stars 21 forks source link

How should I implement the proxy API #110

Closed KingerLeen closed 3 months ago

KingerLeen commented 3 months ago

Thank you for maintaining this project.

I can now use this project to load a local HTML (I only need to use it on an Android device), but using APIs for local HTML will result in CORS. This service is 127.0.0.1, but the requested API should be www.myserver.com.

There are two solutions to this problem

The first solution is: WebView allows CORS:

 <WebView
  allowsBackForwardNavigationGestures={true}
  originWhitelist={["*"]}
  mixedContentMode="always"
  domStorageEnabled={true}
  allowFileAccess={true}
  allowsFullscreenVideo
  forceDarkOn
  allowFileAccessFromFileURLs={true}
  allowUniversalAccessFromFileURLs={true}
  javaScriptEnabled
/>

This time, HTML Sent requests is: http://www.myserver.com/api/xxxxx But it doesn't work

The second solution is: StaticServer proxy API

extraConfig: `
  $HTTP["url"] =~ "^/api" {
    proxy.server  = ( "/api" => (( "host" => "www.myserver.com" )))
  }
`,

This time, HTML Sent requests is: /api/xxxxx But it doesn't work too.

Here is my source code

import React, { useEffect, useRef, useState } from "react";
import {
  Platform,
  SafeAreaView,
  StatusBar,
  StyleSheet,
  Linking,
  View,
} from "react-native";
import { Colors } from "react-native/Libraries/NewAppScreen";
import {
  copyFileAssets,
  readFile,
  readFileAssets,
  unlink,
} from "@dr.pogodin/react-native-fs";
import WebView, { WebViewNavigation } from "react-native-webview";
import Server, {
  STATES,
  resolveAssetsPath,
} from "@dr.pogodin/react-native-static-server";

export default function App() {
  const webView = useRef<WebView>(null);
  const [theme, setTheme] = useState<string>("light");
  const isDarkMode = theme === "dark";

  const backgroundStyle = {
    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
    flex: 1,
  };

  const [origin, setOrigin] = useState<string>("");
  console.log(origin);

  useEffect(() => {
    const fileDir = resolveAssetsPath("webroot");
    let server: null | Server = new Server({
      fileDir,
      hostname: "127.0.0.1",
      port: 3000,
      stopInBackground: true,
      errorLog: {
        conditionHandling: true,
        fileNotFound: true,
        requestHandling: true,
        requestHeader: true,
        requestHeaderOnError: true,
        responseHeader: true,
        timeouts: true,
      },
      extraConfig: `
        $HTTP["url"] =~ "^/api" {
          proxy.server  = ( "/api" => (( "host" => "www.myserver.com" )))
        }
      `,
    });
    const serverId = server.id;
    (async () => {
      if (Platform.OS === "android") {
        let extract = true;
        try {
          const versionD = await readFile(`${fileDir}/version.text`, "utf8");
          const versionA = await readFileAssets("webroot/version.text", "utf8");
          if (versionA === versionD) {
            extract = false;
          } else {
            await unlink(fileDir);
          }
        } catch {}
        if (extract) {
          console.log("Extracting web server assets...");
          await copyFileAssets("webroot", fileDir);
        }
      }

      server?.addStateListener((newState, details, error) => {
        console.log(
          `Server #${serverId}.\n`,
          `Origin: ${server?.origin}`,
          `New state: "${STATES[newState]}".\n`,
          `Details: "${details}".`
        );
        if (error) console.error(error);
      });
      const res = await server?.start();
      if (res && server) {
        setOrigin(res);
      }
    })();
    return () => {
      (async () => {
        server?.stop();
        server = null;
        setOrigin("");
      })();
    };
  }, []);

  const handleMessage = (str = "{}") => {
    let obj = {} as any;
    try {
      obj = JSON.parse(str);
    } catch {
      obj = {};
    }
    console.log("handleMessage", obj);
    switch (obj.key) {
      case "theme":
        setTheme(obj.value);
        break;
      default:
        break;
    }
  };

  return (
    <SafeAreaView style={backgroundStyle}>
      <StatusBar
        barStyle={isDarkMode ? "light-content" : "dark-content"}
        backgroundColor={backgroundStyle.backgroundColor}
      />
      <View style={styles.webview}>
        <WebView
          allowsBackForwardNavigationGestures={true}
          originWhitelist={["*"]}
          mixedContentMode="always"
          domStorageEnabled={true}
          allowFileAccess={true}
          allowsFullscreenVideo
          forceDarkOn
          allowFileAccessFromFileURLs={true}
          allowUniversalAccessFromFileURLs={true}
          javaScriptEnabled
          cacheMode="LOAD_NO_CACHE"
          onMessage={(event: any) => {
            const message = event.nativeEvent.data;
            handleMessage(message);
          }}
          onShouldStartLoadWithRequest={(request: any) => {
            const load = request.url.startsWith(origin);
            if (!load) {
              Linking.openURL(request.url);
            }
            return load;
          }}
          ref={webView}
          source={
            origin
              ? { uri: `${origin}/?insert=1&runerType=AndroidRN` }
              : { html: "" }
          }
        />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  text: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: "400",
  },
  title: {
    fontSize: 24,
    fontWeight: "600",
  },
  webview: {
    flex: 1,
  },
});
birdofpreyru commented 3 months ago

Hey @KingerLeen , perhaps you should revisit how CORS work — the browser behaves according to CORS (HTTP) headers of the main HTML document, thus I guess you just should use SetEnv module to set correct CORS headers to whitelist your API domain for JS scripts in your page.