const MAX_REQUESTS_PER_MINUTE = 50 ; import express from "express" ; import { writeFile } from "fs" ; import { callback } from "awaiting" ; import { once } from "@cocalc/util/async-utils" ; import { endswith , split , meta_file } from "@cocalc/util/misc" ; import { json , urlencoded } from "body-parser" ; const { client_db } = require ( "@cocalc/util/db-schema" ) ; const RateLimit = require ( "express-rate-limit" ) ; import { apiServerPortFile } from "@cocalc/project/data" ; const theClient = require ( "@cocalc/project/client" ) ; export default async function init ( ) { const client = theClient . client ; if ( client == null ) throw Error ( "client must be defined" ) ; const dbg = client . dbg ( "api_server" ) ; const app = express ( ) ; dbg ( "configuring server..." ) ; configure ( client , app , dbg ) ; const server = app . listen ( 0 , "localhost" ) ; await once ( server , "listening" ) ; const address = server . address ( ) ; if ( address == null || typeof address == "string" ) { throw Error ( "failed to assign a port" ) ; } const { port } = address ; dbg ( `writing port to file "${ apiServerPortFile }"` ) ; await callback ( writeFile , apiServerPortFile , `${ port }` ) ; dbg ( `express server successfully listening at http://localhost:${ port }` ) ; } function configure ( client , server , dbg ) { server . use ( json ( { limit : "3mb" } ) ) ; server . use ( urlencoded ( { extended : true , limit : "3mb" } ) ) ; rate_limit ( server ) ; server . get ( "/" , handle_get ) ; server . post ( "/api/v1/*" , async ( req , res ) => { dbg ( `POST to ${ req . path }` ) ; try { handle_auth ( req , client . secret_token ) ; await handle_post ( req , res , client ) ; } catch ( err ) { dbg ( `failed handling POST ${ err }` ) ; res . status ( 400 ) . send ( { error : `${ err }` } ) ; } } ) ; } function rate_limit ( server ) { const limiter = new RateLimit ( { windowMs : 1 * 60 * 1000 , max : MAX_REQUESTS_PER_MINUTE , } ) ; server . use ( limiter ) ; } function handle_get ( _req , res ) { res . send ( { status : "ok" , mesg : "use a POST requesty" } ) ; } function handle_auth ( req , secret_token ) { const h = req . header ( "Authorization" ) ; if ( h == null ) { throw Error ( "you MUST authenticate all requests via secret_token" ) ; } let provided_token ; const [ type , user ] = split ( h ) ; switch ( type ) { case "Bearer" : provided_token = user ; break ; case "Basic" : const x = Buffer . from ( user , "base64" ) ; provided_token = x . toString ( ) . split ( ":" ) [ 0 ] ; break ; default : throw Error ( `unknown authorization type '${ type }'` ) ; } if ( secret_token != provided_token ) { throw Error ( "incorrect secret_token" ) ; } } async function handle_post ( req , res , client ) { const endpoint = req . path . slice ( req . path . lastIndexOf ( "/" ) + 1 ) ; try { switch ( endpoint ) { case "get_syncdoc_history" : res . send ( await get_syncdoc_history ( req . body , client ) ) ; return ; default : throw Error ( "unknown endpoint" ) ; } } catch ( err ) { throw Error ( `handling api endpoint ${ endpoint } -- ${ err }` ) ; } } async function get_syncdoc_history ( body , client ) { const dbg = client . dbg ( "get_syncdoc_history" ) ; let path = body . path ; dbg ( `path="${ path }"` ) ; if ( typeof path != "string" ) { throw Error ( "provide the path as a string" ) ; } if ( endswith ( path , ".ipynb" ) ) { path = meta_file ( path , "jupyter2" ) ; } const string_id = client_db . sha1 ( client . project_id , path ) ; return await client . get_syncdoc_history ( string_id , ! ! body . patches ) ; }