0.1.3Updated 6 months ago
import type { InfinityContext } from "@infinity-beyond/modules/networking/infinity_context.ts";
import { randomUUID } from "node:crypto";
import * as cookie from "https://deno.land/std@0.224.0/http/cookie.ts";

import { RequestAnalytics } from "@infinity-beyond/modules/analytics/request_analytics.ts";
import { ResponseAnalytics } from "@infinity-beyond/modules/analytics/response_analytics.ts";
import { Entity } from "@infinity-beyond/classes/entity.ts";
import State from "@infinity-beyond/modules/state.ts";
import { CustomEventEmitter } from "@infinity-beyond/classes/custom_event_emitter.ts";
import InfinityHeaders from "@infinity-beyond/modules/networking/infinity_headers.ts";
import { LoginRequest } from "@infinity-beyond/modules/security/draco/login_request.ts";
import { GetDracoUser } from "@infinity-beyond/modules/security/draco/get_draco_user.ts";
import { DefaultPermissions } from "@infinity-beyond/modules/security/default_permissions.ts";
import { InfinityRequest } from "@infinity-beyond/modules/analytics/infinity_request.ts";

import { AnnouncePermissions } from "@infinity-beyond/modules/security/draco/announce_permissions.ts";
import { FetchInfinityClasses } from "@infinity-beyond/modules/delegation/entity_ref.ts";

const COOKIE_NAME = `${(Deno.env.get('INFINITY_APP')?.toLowerCase().replace(/\s+/g, '-').replace(/_/g, '-') || '')}-session`;

export class Infinity extends CustomEventEmitter<InfinityEvents> {
  readonly app_name: string
  constructor(app_name: string, import_meta_dirname: string) {
    super();

    this.app_name = app_name;

    Infinity.announce(import_meta_dirname);
  }

  async Middleware(request: Request, ctx: InfinityContext) {
    const { pathname } = new URL(request.url);

    switch(pathname) {
      case '/ping': return new Response('pong');
      case '/__worker__/handshake': return this.handshake(request, ctx);
    }

    ctx.state.request = new InfinityRequest();

    const IS_EXTERNAL_API = /\/api\/:v\d+\//.test(pathname);

    if(!IS_EXTERNAL_API) {
      const cookie_uuid = cookie.getCookies(request.headers)[COOKIE_NAME] as string | undefined;
      if(cookie_uuid) {
        ctx.state.cookie_uuid = cookie_uuid;
        ctx.state.cookie_existed = true;
      } else {
        ctx.state.cookie_uuid = randomUUID();
        ctx.state.cookie_existed = false;
      }

      await this.draco_setup(ctx);
      await this.notification_setup(ctx);

      // Special requests
      switch(pathname) {
        case '/__worker__/handshake':
          return this.handshake(request, ctx);
        case '/logout':
          return this.logout();
        case '/login': {
          if(!ctx.state.user) return this.login(ctx);
          return new Response(undefined, {
            status: 307,
            headers: {
              Location: '/'
            }
          })
        }
      }
    }

    // Handle standard request

    this.emit('request', request, ctx);

    await RequestAnalytics.Process(request, ctx);

    const response = await ctx.next();

    const extension = request.url.replace(/^.+(\.\w+)$/, '$1');

    switch(extension) {
      case '.png':
      case '.js':
      case '.css':
      case '.svg':
        ctx.state.request.finish_without_saving();
        break;

      default: {
        ctx.state.request.finish();
      }
    }

    await ResponseAnalytics.Process(response, ctx)

    this.emit('response', response, ctx);

    InfinityHeaders.apply(response.headers, {
      'x-request-id': ctx.state.request.request_id,
      'x-time-taken': ctx.state.request.duration.toString(),
    }, InfinityHeaders.Default);

    if(!IS_EXTERNAL_API) {
      if(!ctx.state.cookie_existed) {
        cookie.setCookie(response.headers, {
          name: COOKIE_NAME,
          value: ctx.state.cookie_uuid,
          maxAge: 60 * 60 * 24 * 7, // 7 days
          secure: new URL(request.url).protocol == 'https:',
        });
      }
    }

    return response;
  }

  handshake(request: Request, _ctx: InfinityContext) {
    const signature = request.headers.get('sec-websocket-protocol')?.replace(/^SIGNATURE, (.+$)$/, '$1');

    if(!signature || signature !== State.SIGNATURE) {
      return new Response('Invalid signature', { status: 403 });
    }

    const { socket, response } = Deno.upgradeWebSocket(request);

    const query: Record<string, string> = new URL(request.url).search
      ?.replace(/^\?/, '')
      .split('&')
      .reduce((p, query) => {
        const [key, value] = query.split('=');
        p[key] = value;
        return p;
      }, {} as Record<string, string>) ?? {};

    const { uuid } = query;
    if(!uuid) {
      socket.close(1, 'No provided uuid');
      return new Response('', { status: 403 });
    }

    socket.onopen = () => {
      Entity.__AddWorker(uuid, socket);
    }

    return response;
  }

  async draco_setup(ctx: InfinityContext) {
    ctx.state.user = await GetDracoUser(ctx.state.cookie_uuid);
  }

  async notification_setup(ctx: InfinityContext) {
    ctx.state.notifications = [{
      id: 1,
      title: "Testing",
      body: "This is a test notification",
      read: false,
    }];
  }

  async login(ctx: InfinityContext) {
    return await LoginRequest(ctx.state.cookie_uuid);
  }

  async logout() {
    const response = new Response(undefined, {
      status: 307,
      headers: {
        Location: '/',
      }
    });

    cookie.deleteCookie(response.headers, COOKIE_NAME);

    return response;
  }

  static get Permissions() {
    return DefaultPermissions;
  }

  private static HAS_ANNOUNCED = false;
  private static announce(import_meta_dirname: string) {
    if(this.HAS_ANNOUNCED) return;
    this.HAS_ANNOUNCED = true;

    setTimeout(async () => {
      const Entities = await FetchInfinityClasses(import_meta_dirname);
  
      const EntityPermissions: string[] = [];
  
      Object.values(Entities).forEach(entity => {
        if(entity instanceof Entity) {
          EntityPermissions.push(...Object.keys((entity as any).__permissions));
        }
      });
  
      AnnouncePermissions(EntityPermissions);
    }, 100);
  }
}

type InfinityEvents = {
  request: [request: Request, ctx: InfinityContext],
  response: [response: Response, ctx: InfinityContext],
}