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

Serve Static Files with Custom Headers in React Native App #99

Closed arjun-digi closed 6 months ago

arjun-digi commented 6 months ago

Problem The current implementation in the React Native application involves serving static files from a local/static server, including an index.html file with embedded JavaScript code. Although the React Native application successfully connects to the server and fetches the index.html file, it fails to properly handle the custom headers required for communication with the stockfish.js file.

Expected Behavior The React Native application should serve static files, including the index.html file, with the necessary custom headers to facilitate communication with the stockfish.js file and retrieve the expected data.

Current Setup The React Native application connects to a local/static server. Utilizes the JavaScript fetch method to retrieve the index.html file. The index.html file contains embedded JavaScript code, dependent on two additional files: stockfish.js and stockfish.wasm.

The index.html file contains JavaScript code for initializing and communicating with a stockfish worker. Dependencies: stockfish.js and stockfish.wasm.

server.js

const fs = require('fs');
const path = require('path');
const http = require('http');

http.createServer((req, res) => {
    let filename = decodeURI(req.url);
    if (req.url === '/') {
        filename = 'example.html';  
    }
    const mime = {
    ".html": "text/html",
    ".js":   "application/javascript",
    ".wasm": "application/wasm",
    };
    fs.readFile(path.join('.', filename), (err, data) => {
        if (err) {
            res.writeHead(404);
            res.end();
        } else {
            res.writeHead(200, {
                "Cross-Origin-Embedder-Policy": "require-corp",
                "Cross-Origin-Opener-Policy": "same-origin",
                "Content-Type": mime[path.extname(filename)] || 'application/octet-steam',
            });
            res.end(data);
        }
    });
}).listen(8080);

index.html

<script>
    const stockfish = new Worker('stockfish.js#stockfish.wasm');
    stockfish.onmessage = (ev) => console.log(ev.data);
    stockfish.postMessage('ucinewgame');
    stockfish.postMessage('isready');
    stockfish.postMessage('setoption name Threads value 8');
    stockfish.postMessage('go depth 20');
</script>

App.tsx

import React, { useEffect, useRef, useState } from 'react';

import { Platform, StyleSheet, Text, useColorScheme } from 'react-native';

import { Colors } from 'react-native/Libraries/NewAppScreen';

import {
  copyFileAssets,
  readFile,
  readFileAssets,
  unlink,
} from '@dr.pogodin/react-native-fs';

import { WebView } from 'react-native-webview';

import Server, {
  STATES,
  resolveAssetsPath,
} from '@dr.pogodin/react-native-static-server';

export default function App() {
  useEffect(() => {
    const fileDir = resolveAssetsPath('webroot');

    // In our example, `server` is reset to null when the component is unmount,
    // thus signalling that server init sequence below should be aborted, if it
    // is still underway.
    let server: null | Server = new Server({
      fileDir,

      // Note: Inside Android emulator the IP address 10.0.2.15 corresponds
      // to the emulated device network or ethernet interface, which can be
      // connected to from the host machine, following instructions at:
      // https://developer.android.com/studio/run/emulator-networking#consoleredir
      // hostname: '10.0.2.15', // Android emulator ethernet interface.
      hostname: '127.0.0.1', // This is just the local loopback address.

      // The fixed port is just more convenient for library development &
      // testing.
      port: 3000,

      stopInBackground: false,

      // These settings enable all available debug options for Lighttpd core,
      // to facilitate library development & testing with the example app.
      errorLog: {
        conditionHandling: true,
        fileNotFound: true,
        requestHandling: true,
        requestHeader: true,
        requestHeaderOnError: true,
        responseHeader: true,
        timeouts: true,
      },

      // This is to enable WebDAV for /dav... routes. To use, you should also
      // opt-in for building the library with WebDAV support enabled
      // (see README for details).
      // webdav: ['^/dav($|/)'],

      extraConfig: `
        server.modules += ("mod_alias", "mod_rewrite")
        alias.url = (
          "/some/path" => "${fileDir}"
        )
        url.rewrite-once = ( "/bad/path/(.*)" => "/$1" )
      `,
    });
    const serverId = server.id;

    (async () => {
      // On Android we should extract web server assets from the application
      // package, and in many cases it is enough to do it only on the first app
      // installation and subsequent updates. In our example we'll compare
      // the content of "version" asset file with its extracted version,
      // if it exist, to deside whether we need to re-extract these assets.
      if (Platform.OS === 'android') {
        let extract = true;
        try {
          const versionD = await readFile(`${fileDir}/version`, 'utf8');
          const versionA = await readFileAssets('webroot/version', 'utf8');
          if (versionA === versionD) {
            extract = false;
          } else {
            await unlink(fileDir);
          }
        } catch {
          // A legit error happens here if assets have not been extracted
          // before, no need to react on such error, just extract assets.
        }
        if (extract) {
          console.log('Extracting web server assets...');
          await copyFileAssets('webroot', fileDir);
        }
      }

      server?.addStateListener((newState, details, error) => {
        // Depending on your use case, you may want to use such callback
        // to implement a logic which prevents other pieces of your app from
        // sending any requests to the server when it is inactive.

        // Here `newState` equals to a numeric state constant,
        // and `STATES[newState]` equals to its human-readable name,
        // because `STATES` contains both forward and backward mapping
        // between state names and corresponding numeric values.
        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();
      const mime = {
        '.html': 'text/html',
        '.js': 'application/javascript',
        '.wasm': 'application/wasm',
      };
      if (res && server) {
        fetch('http://127.0.0.1:3000/index.html', {
          headers: {
            'Cross-Origin-Embedder-Policy': 'require-corp',
            'Cross-Origin-Opener-Policy': 'same-origin',
            'Content-type': 'application/octet-steam',
            // Add more headers if needed
          },
        })
          .then((response) => {
            console.log(JSON.stringify(response, null, 2));
          })
          .catch((error) => {
            console.log(error);
          });
      }
    })();
    return () => {
      (async () => {
        // In our example, here is no need to wait until the shutdown completes.
        server?.stop();

        server = null;
      })();
    };
  }, []);

  const webView = useRef<WebView>(null);

  return <></>;
}

const styles = StyleSheet.create({
  text: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: '400',
  },
  title: {
    fontSize: 24,
    fontWeight: '600',
  },
  webview: {
    borderColor: 'black',
    borderWidth: 1,
    flex: 1,
    marginTop: 12,
  },
});

Attached Screenshot of current output while fetching index.html

Screenshot 2024-02-13 at 3 29 53 PM

Any help would be highly appreciated.

birdofpreyru commented 6 months ago

Hey @arjun-digi , I had a brief look at Lighttpd configuration docs, if I got it right to set custom headers one needs the mod_setenv module, which seems to be a built-in. Thus to use it you need to add its init here prior to building your project, and then use extraConfig option of the server constructor to load & configure it as you need. If you can try it locally and confirm it works for you, I'll add the PLUGIN_INIT(mod_setenv) to the CMakeList.txt in the next release.

arjun-digi commented 6 months ago

Hey @birdofpreyru,

Thank you for your quick response on this. I've implemented the changes as per your instructions, and it seems to be working for me! If I got you right, below are the changes I made in the CMakeLists.txt and the extraConfig option:

In CMakeLists.txt:

set(PLUGIN_STATIC
  PLUGIN_INIT(mod_alias)\n
  PLUGIN_INIT(mod_dirlisting)\n
  PLUGIN_INIT(mod_h2)\n
  PLUGIN_INIT(mod_indexfile)\n
  PLUGIN_INIT(mod_rewrite)\n
  PLUGIN_INIT(mod_staticfile)\n
  PLUGIN_INIT(mod_setenv)\n
)

in extraConfig block:

extraConfig: `
        server.modules += ("mod_alias", "mod_rewrite", "mod_setenv")
        setenv.add-response-header = (
        "Cross-Origin-Embedder-Policy" => "require-corp" 
        "Cross-Origin-Opener-Policy" => "same-origin"
        )
        alias.url = (
          "/some/path" => "${fileDir}"
        )
        url.rewrite-once = ( "/bad/path/(.*)" => "/$1" )
      `,

After making these changes, the server is running smoothly with the custom headers set as expected. Thank you again for your guidance! Let me know if you need any further information or if there's anything else I can assist with.

However I am still looking how to communicate with stockfish.js using static server.

birdofpreyru commented 6 months ago

After making these changes, the server is running smoothly with the custom headers set as expected.

Great! As I told, I'll make this update to CMakeList.txt in the next library release (not sure when exactly, probably not right away).

However I am still looking how to communicate with stockfish.js using static server.

Well... good luck with it, I guess... because if it is a question, I don't know how to communicate with stockfish.js, I don't know what exactly stockfish.js is, and I see no reason for me to learn about it now :)