fails-components / webtransport

Http/3 webtransport support for node
Other
146 stars 20 forks source link

one session for multiple tabs? #243

Open pavelthq opened 7 months ago

pavelthq commented 7 months ago

Trying to send WRITE for BidirectionalStream it works during the same browser tab, however I'm trying to achieve that second tab READER receives the WRITE from first tab as well, not only server.

It seems that session is different for every browser tab and thus it cannot "write" for both of the sessions, is there any idea how to achieve this logic for multiple tabs?

pavelthq commented 7 months ago

SERVER:

import fs from "fs";
import path from "path";
import mime from "mime";
import http from "http";
import https from "https";
import { Http3Server } from "@fails-components/webtransport";
import { generateWebTransportCertificate } from './mkcert';
import { readFile } from "node:fs/promises";

// node events
import { EventEmitter } from "node:events";

type StreamType = "datagram" | "bidirectional" | "unidirectional";

const isProduction = process.env.NODE_ENV === "production";
const PORT = 4433;
const HOST = (isProduction)
  ? "demo.web-transport.io"
  : "gpt.pavels.lv";

console.log(HOST);
/**
 * Proxy to serve local development server (:5173) on HTTPS (:4433)
 */
const proxy = http.createServer((clientReq, clientRes) => {
  const options = {
    hostname: 'gpt.pavels.lv',
    port: 5173,
    path: clientReq.url,
    method: clientReq.method,
    headers: clientReq.headers
  };

  const proxyReq = http.request(options, (proxyRes) => {
    clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
    proxyRes.pipe(clientRes, {
      end: true
    });
  });

  clientReq.pipe(proxyReq, {
    end: true
  });

  proxyReq.on('error', (err) => {
    console.error('Proxy request error:', err);
    clientRes.end();
  });
});

let stack = [];
async function readData(readable, writable) {
  const reader = readable.getReader();
  const writer = writable.getWriter();
  console.log("readData", reader)
  while (true) {
    const { value, done } = await reader.read();
    if (done) {
      break;
    }
    // value is a Uint8Array.
    console.log(value);
    writer.write(value);
  }
}

async function writeData(writable) {
  const writer = writable.getWriter();
  const data1 = new Uint8Array([65, 66, 67]);
  const data2 = new Uint8Array([68, 69, 70]);
  writer.write(data1);
  writer.write(data2);
}

async function receiveBidirectional(stream) {
  const reader = stream.getReader();
  while (true) {
    const { done, value: bidi } = await reader.read();
    if (done) {
      break;
    }
    // value is an instance of WebTransportBidirectionalStream
    console.log('calling readData');
    readData(bidi.readable,bidi.writable);
    // await writeData(bidi.writable);
  }
}
async function main() {
  const certificate =  {
    /**
     * Replace with your own certificate.
     */
    private: fs.readFileSync("/etc/letsencrypt/live/gpt.pavels.lv/privkey.pem"),
    cert: fs.readFileSync("/etc/letsencrypt/live/gpt.pavels.lv/fullchain.pem"),
    fingerprint: "" // not used in production

  }

  /**
   * Create a HTTPS server to serve static files
   */
  https.createServer({
    cert: certificate?.cert,
    key: certificate?.private,
  }, async function (req, res) {
    const filename = req.url?.substring(1) || "index.html"; // fallback to "index.html"

    if (filename === "fingerprint") {
      const fingerprint = certificate?.fingerprint!.split(":").map((hex) => parseInt(hex, 16));
      res.writeHead(200, { "content-type": "application/json" });
      res.end(JSON.stringify(fingerprint));
      return;
    }

    if (process.env.NODE_ENV !== "production") {
      /**
       * DEVELOPMENT:
       * Use proxy to serve local development server
       */
      proxy.emit('request', req, res);

    } else {
      /**
       * PRODUCTION:
       * Serve static files from "client/dist"
       */

      const filepath = path.join(__dirname, "..", "client", "dist", filename);
      if (fs.existsSync(filepath)) {
        res.writeHead(200, {
          "content-type": (mime.getType(filename) || "text/plain"),
          "Alt-Svc": `h3=":${PORT}"`
        });
        res.end((await readFile(filepath)));

      } else {
        res.writeHead(404);
        res.end('Not found');
      }
    }

  }).listen(PORT);

  // https://github.com/fails-components/webtransport/blob/master/test/testsuite.js

  const h3Server = new Http3Server({
    host: HOST,
    port: PORT,
    secret: "mysecret",
    cert: certificate?.cert,
    privKey: certificate?.private,
  });

  h3Server.startServer();
  // h3Server.updateCert(certificate?.cert, certificate?.private);

  let isKilled: boolean = false;

  function handle(e: any) {
    console.log("SIGNAL RECEIVED:", e);
    isKilled = true;
    h3Server.stopServer();
  }

  process.on("SIGINT", handle);
  process.on("SIGTERM", handle);

  try {
    const sessionStream = await h3Server.sessionStream("/");
    const sessionReader = sessionStream.getReader();
    sessionReader.closed.catch((e: any) => console.log("session reader closed with error!", e));

    while (!isKilled) {
      console.log("sessionReader.read() - waiting for session...");
      const { done, value: webtransportSession } = await sessionReader.read();
      if (done) {
        console.log("done! break loop.");
        break;
      }

      webtransportSession.closed.then(() => {
        console.log("Session closed successfully!");
      }).catch((e: any) => {
        console.log("Session closed with error! " + e);
      });

      webtransportSession.ready.then(async () => {
        console.log("session ready!");
        // WebTransportReceiveStream
        await receiveBidirectional(webtransportSession.incomingBidirectionalStreams);

      }).catch((e: any) => {
        console.log("session failed to be ready!", e);
      });
    }

  } catch (e) {
    console.error("error:", e);
  }
}

main();

Client:


async function readData(reader, type) {
    console.log("Reader created", reader, type);
    while (true) {
        console.log("Reading data", reader);
      const { value, done } = await reader.read();
      if (done) {
        console.log("Stream is done");
        break;
      }
      // value is a Uint8Array.
      console.log('readData', value);
    }
  }
  async function receiveBidirectional(stream) {
    const reader = stream.getReader();
    console.log('receiveBidirectional')
    while (true) {
      const { done, value: bidi } = await reader.read();
      if (done) {
        break;
      }
      console.log("receiveBidirectional readData"), bidi;

      // value is an instance of WebTransportBidirectionalStream
      await readData(bidi.readable.getReader());
    // await writeData(bidi.writable);
    }
  }

const ENDPOINT = `https://gpt.pavels.lv:4433`;
let transport = new WebTransport(ENDPOINT);
var bidirectionalDataWriterRef = {}
var bidirectionalDataReaderRef = {}
transport.ready.then(async () => {
    console.log("Transport ready");

    const bidi = await transport.createBidirectionalStream();
    console.log("Bidirectional stream create start");
    const reader = bidi.readable.getReader();
    bidirectionalDataReaderRef = reader;

    //reader.closed.catch(e => console.log("bidi readable closed", e.toString()));
    readData(reader, 'readable.reader');
    receiveBidirectional(transport.incomingBidirectionalStreams);
    const writer = bidi.writable.getWriter();
    bidirectionalDataWriterRef = writer;
    writer.closed.catch(e => console.log("bidi writable closed", e.toString()));
});

To repeat open two tabs, and in first tab in chrome developer tools write: bidirectionalDataWriterRef.write(new Uint8Array([8, 8, 8]));

server will receive it and immediately write it (inside readData), and client#1 will receive response back. However client#2 never receives any, it seems I need to somehow propogate it or combine sessions,..

martenrichter commented 7 months ago

rying to send WRITE for BidirectionalStream it works during the same browser tab, however I'm trying to achieve that second tab READER receives the WRITE from first tab as well, not only server.

It seems that session is different for every browser tab and thus it cannot "write" for both of the sessions, is there any idea how to achieve this logic for multiple tabs?

Of course, the session is different for every tab. Anything else would be a security problem, and such a mechanism is handled by the browser. To make both sessions receive the same data, you must implement your own routing code, which of the sources should include authentication, and so on. So, something like a global map of sessions or streams and code that distributed the data. At least socket.io already has a how-to for using this lib. As it provides a routing mechanism, this may be a way if you do not want to write it yourself.

achingbrain commented 6 months ago

If you want to share a single WebTransport session between multiple browsing contexts, you should run it in a SharedWorker.

Note that browsers will enforce the same-origin policy, similar to how you'd access other web resources.

pavelthq commented 6 months ago

My fault, I absolutely forgot that nodejs works as a single application and doesn't not die, so each connection can be handled with events and stored in variables that later can be forwared further, so when ever client X sends message, we forward and client Y also sees it.. So technically it will be "same" session, just need to deal with auth.