💾 Archived View for iich.space › src › modules › gemini › request.ts captured on 2022-03-01 at 16:04:18.

View Raw

More Information

⬅️ Previous capture (2021-12-03)

-=-=-=-=-=-=-

import { TLSSocket } from 'tls';
import { URL } from 'url';

import { createLogger } from '@/log';

import { MAX_FILE_SIZE } from '~/constants';
import { generateHash } from '~/util/hash';

export interface File {
  mime: string;
  size: number;
  data: Buffer;
  token: string;
}

const log = createLogger();

export class Request {
  url: URL;
  fingerprint: string | null;
  remote: string;
  query: string | null;
  file: File | null = null;

  constructor(
    url: URL,
    remote: string,
    fingerprint: string | null,
    file: File | null,
  ) {
    this.url = url;
    this.remote = remote;
    this.fingerprint = fingerprint;
    this.file = file;

    const query = decodeURIComponent(url.search.slice(1));
    this.query = query !== '' ? query : null;
  }

  static async fromTitanRequest(
    socket: TLSSocket,
    url: URL,
    remote: string,
    fingerprint: string | null,
    initialBuffer: Buffer,
  ): Promise<Request> {
    const meta = {
      mime: '',
      size: 0,
      token: '',
    };

    const [href, ...vars] = url.href.split(';');

    vars
      .map((item) => item.split('='))
      .forEach(([key, value]) => {
        switch (key) {
          case 'token': {
            meta.token = decodeURIComponent(value);
            break;
          }
          case 'mime': {
            meta.mime = value;
            break;
          }
          case 'size':
            meta.size = parseInt(value, 10);
            break;
        }
      });

    if (meta.size >= MAX_FILE_SIZE) {
      const size = (MAX_FILE_SIZE / 1024 / 1024).toFixed(2);
      return Promise.reject(new Error(`File Too Large (${size}mb Limit)`));
    }

    const data = await new Promise<Buffer>((ok, err) => {
      let buffer = initialBuffer;
      let timeout: NodeJS.Timer;

      const clearTimer = () => {
        if (timeout !== undefined) {
          clearTimeout(timeout);
        }
      };

      const resetTimer = () => {
        clearTimer();
        timeout = setTimeout(() => err(new Error('Timeout')), 2000);
      };

      const onData = (data: Buffer) => {
        resetTimer();
        buffer = Buffer.concat([buffer, data]);

        if (buffer.length === meta.size) {
          socket.removeListener('data', onData);
          clearTimer();
          ok(buffer);
        } else if (buffer.length > meta.size) {
          err(new Error(`Upload Size Mismatch ${buffer.length}/${meta.size}`));
        }
      };

      socket.on('data', onData);
      onData(Buffer.from([]));
    });

    const file = { ...meta, data };

    return new Request(new URL(href), remote, fingerprint, file);
  }

  static async fromSocket(socket: TLSSocket): Promise<Request> {
    let buffer = Buffer.from([]);

    const url = await new Promise<URL>((ok) => {
      const onData = (data: Buffer) => {
        buffer = Buffer.concat([buffer, data]);
        const eol = buffer.indexOf('\r\n');

        if (eol >= 0) {
          const url = buffer.slice(0, eol);
          buffer = buffer.slice(eol + 2);
          socket.removeListener('data', onData);
          ok(new URL(url.toString()));
        }
      };

      socket.on('data', onData);
    });

    const remote = generateHash(socket.remoteAddress!);
    const certificate = socket.getPeerCertificate(true);
    let fingerprint: string | null = null;

    if (certificate && certificate.fingerprint) {
      fingerprint = certificate.fingerprint;
    }

    log.info(remote, fingerprint, url.toString());

    if (url.protocol === 'titan:') {
      return this.fromTitanRequest(socket, url, remote, fingerprint, buffer);
    }

    return new Request(url, remote, fingerprint, null);
  }
}