import { PathResolverResult , resolvePath , ServerConfig , ServerEventEmitter , statWalkPath , StatPathResult , IStatPathResult , Resolved , getStatPathResult , } from "./server-types" ; import { StateObject } from "./state-object" ; import * as path from "path" ; import * as http from "http" ; import * as fs from "fs" ; import { EventEmitter } from "./event-emitter-types" ; import { parse , UrlWithStringQuery } from "url" ; import { inspect , promisify , debuglog } from "util" ; import { WikiInfo } from "./boot-startup-types" ; import * as WebSocket from "ws" ; import { RequestEvent } from "./request-event" ; import { OptionsConfig } from "./server-config" ; import { Writable } from "stream" ; import { tryParseJSON } from './utils-functions' ; import { ParsedUrlQuery } from 'querystring' ; import { dirname } from "../rootpath" ; import { factoryFileSystemLoader } from "./data-folder-loader" ; import { TiddlyWikiGlobal } from './types' ; import { runInNewContext , runInContext } from 'vm' ; interface Records < T > { [ k ] : T ; } const loadedFolders = { } ; const otherSocketPaths = { } ; const clientsList = { } ; type DataFolderEvents = { "ws-client-connect" : readonly [ WebSocket , DataFolderRequest , string ] ; "ws-client-preload" : readonly [ ( ) => void ] ; "reload" : readonly [ string , string ] ; "init-data-folder" : readonly [ DataFolder ] ; } ; export function handleDataFolderRequest ( result , state ) { const reload = ! ! state . url . query . reload ; let request = new DataFolderRequest ( result , state . statPath , state . url . pathname , state . settings . __serverTW , state . settings . datafolder , state . debugOutput ) ; DataFolder . trigger ( request , reload ) ; const { mount } = request ; const isFullpath = result . filepathPortion . length === state . statPath . index ; const redirect = isFullpath && state . pathOptions . noTrailingSlash !== ! state . url . pathname . endsWith ( "/" ) ; if ( redirect || reload ) { let redirect = mount + ( ! state . pathOptions . noTrailingSlash ? "/" : "" ) ; state . redirect ( redirect ) ; return ; } loadedFolders [ mount ] . handler ( state ) ; } class DataFolder { static tiddlywiki : Record < string , { server : TiddlyWikiServer ; $tw : any ; queue : any ; cache : any ; } > = { } ; syncadaptor : any ; syncer : any ; /** Creates a DataFolder if there is none on loadedFolders[mount] then loads the datafolder asyncly */ static async trigger ( request , reloadParam ) { let { mount , folder , target , vars } = request ; if ( ! loadedFolders [ mount ] || reloadParam ) { if ( reloadParam ) loadedFolders [ mount ] . events . emit ( "reload" , mount , folder ) ; loadedFolders [ mount ] = new DataFolder ( mount , folder ) ; if ( ! DataFolder . tiddlywiki [ target ] ) DataFolder . tiddlywiki [ target ] = await loadDataFolderServer ( target , vars ) ; promisify ( fs . readFile ) ( path . join ( folder , "tiddlywiki.info" ) , "utf8" ) . then ( data => { const wikiInfo = tryParseJSON < WikiInfo > ( data , e => { throw e ; } ) ; if ( ! wikiInfo . type || wikiInfo . type === "tiddlywiki" ) { loadDataFolderTiddlyWiki ( request ) ; } else if ( wikiInfo . type === "tiddlywiki-slim" ) { loadDataFolder ( request ) ; } } ) ; } } static doError ( debugOutput , mount , folder , err ) { const requests = loadedFolders [ mount ] . pending ; StateObject . DebugLoggerInner ( 3 , "DATA" , "error starting %s at %s: %s" , [ mount , folder , err . stack ] , debugOutput ) ; loadedFolders [ mount ] . handler = function ( state ) { state . respond ( 500 , "TW5 data folder failed" ) . string ( "The Tiddlywiki data folder failed to load. The error has been " + "logged to the terminal with priority level 2. To try again, " + "use ?reload=true after making any necessary corrections." ) ; } ; requests . forEach ( e => loadedFolders [ mount ] . handler ( e ) ) ; } wiki : any ; boot : any ; pending : StateObject [ ] = [ ] ; /**
    - "ws-client-connect": readonly [WebSocket, DataFolderRequest, subpath]
    - "ws-client-preload": readonly [() => void]
    - "reload": readonly [mount, folder]
    - "init-wiki": readonly [mount, folder, wiki]
     */ events = new EventEmitter < DataFolderEvents > ( ) ; handler : ( state : StateObject ) => void = ( state ) => { this . pending . push ( state ) ; } ; constructor ( public mount , public folder ) { } } class DataFolderRequest { mount : string ; folder : string ; constructor ( public result , public statPath , public pathname , public target , public vars , public debugOutput ) { let filepathPrefix = this . result . filepathPortion . slice ( 0 , this . statPath . index ) . join ( "/" ) ; let fullPrefix = [ "" , this . result . treepathPortion . join ( "/" ) ] ; if ( this . statPath . index > 0 ) fullPrefix . push ( filepathPrefix ) ; fullPrefix = fullPrefix . join ( "/" ) . split ( "/" ) ; this . mount = this . pathname . split ( "/" ) . slice ( 0 , fullPrefix . length ) . join ( "/" ) ; this . folder = this . statPath . statpath ; } } export async function handleWebsocketConnection ( data , result , treeOptions , statPath ) { const { client , settings , debugOutput } = data ; const debug = StateObject . DebugLogger ( "WEBSOCK" ) . bind ( { settings , debugOutput } ) ; let pathname = parse ( data . request . url as string ) . pathname ; if ( ! pathname ) return client . close ( 400 ) ; if ( statPath . itemtype === "datafolder" ) { if ( ! data . allow . datafolder ) return client . close ( 403 ) ; const request = new DataFolderRequest ( result , statPath , pathname , settings . __serverTW , settings . datafolder , data . debugOutput ) ; const { mount , folder } = request ; const subpath = pathname . slice ( mount . length ) ; const loadClient = ( ) => { debug ( - 1 , "ws-client-connect %s" , mount ) ; loadedFolders [ mount ] . events . emit ( "ws-client-connect" , client , request , subpath ) ; } ; if ( Array . isArray ( loadedFolders [ mount ] . handler ) ) { loadedFolders [ mount ] . events . once ( "ws-client-preload" , loadClient ) ; } else { loadClient ( ) ; } } else { console . log ( "add client" , pathname ) ; client . addEventListener ( "message" , event => { debug ( - 3 , "WS-MESSAGE %s" , inspect ( event . data ) ) ; clientsList [ pathname as string ] . forEach ( e => { if ( e !== client ) e . send ( event . data ) ; } ) ; } ) ; client . addEventListener ( "error" , event => { debug ( - 2 , "WS-ERROR %s %s" , pathname , event . type ) ; let index = clientsList [ pathname as string ] . indexOf ( client ) ; if ( index > - 1 ) clientsList [ pathname as string ] . splice ( index , 1 ) ; client . close ( ) ; } ) ; client . addEventListener ( "close" , event => { debug ( - 2 , "WS-CLOSE %s %s %s" , pathname , event . code , event . reason ) ; let index = clientsList [ pathname as string ] . indexOf ( client ) ; if ( index > - 1 ) clientsList [ pathname as string ] . splice ( index , 1 ) ; } ) ; if ( ! clientsList [ pathname ] ) clientsList [ pathname ] = [ ] ; clientsList [ pathname ] . push ( client ) ; } } declare const __non_webpack_require__ ; const nodeRequire = typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require ; function loadDataFolderTiddlyWiki ( request ) { const { debugOutput , mount , folder , target , vars } = request ; console . time ( "twboot-" + folder ) ; const tw = nodeRequire ( target + "/boot/boot.js" ) . TiddlyWiki ( nodeRequire ( target + "/boot/bootprefix.js" ) . bootprefix ( { packageInfo : nodeRequire ( target + "/package.json" ) , } ) ) ; tw . boot . argv = [ folder ] ; tw . preloadTiddler ( { text : "$protocol$//$host$" + mount + "/" , title : "$:/config/tiddlyweb/host" , } ) ; try { tw . boot . boot ( ( ) => { complete ( null , tw ) ; } ) ; } catch ( err ) { complete ( err , null ) ; } function complete ( err , $tw ) { console . timeEnd ( "twboot-" + folder ) ; if ( err ) { return DataFolder . doError ( debugOutput , mount , folder , err ) ; } loadedFolders [ mount ] . wiki = $tw . wiki ; let Server ; try { Server = $tw . modules . execute ( "$:/core/modules/server/server.js" ) . Server ; } catch ( e ) { console . log ( mount , folder , e ) ; return ; } let server = new Server ( { wiki : $tw . wiki , variables : { "path-prefix" : mount , "root-tiddler" : "$:/core/save/all" , gzip : "yes" , ... vars , } , } ) ; let queue = { } ; let auth = new TiddlyServerAuthentication ( server , ( sym ) => { let res = queue [ sym as any ] ; delete queue [ sym as any ] ; return res ; } ) ; auth . init ( ) ; server . authenticators . unshift ( auth ) ; $tw . hooks . invokeHook ( "th-server-command-post-start" , server , loadedFolders [ mount ] . events , "tiddlyserver" ) ; const requests = loadedFolders [ mount ] . pending ; loadedFolders [ mount ] . handler = ( state ) => { let req = new Object ( state . req ) as http . IncomingMessage & { tsstate : symbol ; } ; req . url += state . url . pathname === mount && ! state . url . pathname . endsWith ( "/" ) ? "/" : "" ; req . tsstate = Symbol ( "state object pointer" ) ; queue [ req . tsstate as any ] = state ; server . requestHandler ( req , state . res ) ; } ; loadedFolders [ mount ] . events . emit ( "ws-client-preload" ) ; requests . forEach ( e => loadedFolders [ mount ] . handler ( e ) ) ; loadedFolders [ mount ] . pending = [ ] ; } } function loadDataFolderServer ( target , vars ) { return new Promise ( ( resolve , reject ) => { console . time ( "twboot-server" ) ; let _wiki = undefined ; const tw = nodeRequire ( target + "/boot/boot.js" ) . TiddlyWiki ( nodeRequire ( target + "/boot/bootprefix.js" ) . bootprefix ( { packageInfo : nodeRequire ( target + "/package.json" ) , } ) ) ; tw . utils . evalSandboxed = tw . utils . evalGlobal ; tw . boot . argv = [ dirname + "/datafolder" ] ; tw . FileSystemLoader = factoryFileSystemLoader ( tw ) ; tw . loadTiddlersNode = function ( ) { new tw . FileSystemLoader ( tw . wiki , tw . boot , tw . boot . extraPlugins ) . loadTiddlersNode ( ) ; } ; try { tw . boot . boot ( ( ) => resolve ( tw ) ) ; } catch ( err ) { console . timeEnd ( "twboot-server" ) ; console . log ( err ) ; console . log ( err . stack ) ; reject ( ) ; } } ) . then ( ( $tw ) => { console . timeEnd ( "twboot-server" ) ; let Server ; try { Server = $tw . modules . execute ( "$:/core/modules/server/server.js" ) . Server ; } catch ( e ) { console . log ( e . stack || e ) ; return Promise . reject ( ) ; } let server = new Server ( { wiki : $tw . wiki , variables : { "path-prefix" : "" , "root-tiddler" : "$:/core/save/all" , "gzip" : "yes" , ... vars , } , } ) ; let queue = { } ; let auth = new TiddlyServerAuthentication ( server , ( sym ) => { let res = queue [ sym as any ] ; delete queue [ sym as any ] ; return res ; } ) ; auth . init ( ) ; server . authenticators . unshift ( auth ) ; $tw . adaptorClass = undefined ; $tw . modules . forEachModuleOfType ( "syncadaptor" , function ( title , module ) { if ( ! $tw . adaptorClass && module . adaptorClass ) { console . log ( "adaptor" , title ) ; $tw . adaptorClass = module . adaptorClass ; } } ) ; const cache = new $tw . Wiki ( ) ; cache . addTiddler ( $tw . wiki . getTiddler ( "$:/core" ) ) ; cache . addTiddler ( $tw . wiki . getTiddler ( "$:/plugins/tiddlywiki/codemirror" ) ) ; return { server , $tw , queue , cache } ; } ) ; } function swapTiddlers ( df , cache ) { const swap = ( title ) => { if ( df . wiki . tiddlerExists ( title ) ) df . wiki . addTiddler ( cache . getTiddler ( title ) ) ; } ; cache . allTitles ( ) . forEach ( e => { swap ( e ) ; } ) ; } function loadDataFolder ( request ) { let { mount , folder , target } = request ; let { server , queue , $tw , cache } = DataFolder . tiddlywiki [ target ] ; console . time ( "datafolder-" + folder ) ; const df = loadedFolders [ mount ] ; df . wiki = new $tw . Wiki ( ) ; df . boot = Object . create ( $tw . boot ) ; df . boot . files = { } ; df . boot . wikiPath = folder ; df . wiki . addTiddler ( { text : "$protocol$//$host$" + mount + "/" , title : "$:/config/tiddlyweb/host" , } ) ; new $tw . FileSystemLoader ( df . wiki , df . boot , [ ] ) . loadTiddlersNode ( ) ; swapTiddlers ( df , cache ) ; df . wiki . readPluginInfo ( ) ; df . wiki . registerPluginTiddlers ( "plugin" , undefined ) ; df . wiki . unpackPluginTiddlers ( ) ; if ( df . wiki . tiddlerExists ( "$:/plugins/tiddlywiki/filesystem" ) && $tw . adaptorClass ) { console . log ( "adding filesystem adaptor" ) ; df . syncadaptor = new $tw . adaptorClass ( { wiki : df . wiki , boot : df . boot } ) ; df . syncer = new $tw . Syncer ( { wiki : df . wiki , boot : df . boot , syncadaptor : df . syncadaptor } ) ; } $tw . hooks . invokeHook ( "th-server-command-post-start" , server , loadedFolders [ mount ] . events , "tiddlyserver-slim" ) ; loadedFolders [ mount ] . events . emit ( "init-data-folder" , [ loadedFolders [ mount ] ] ) ; console . timeEnd ( "datafolder-" + folder ) ; const requests = loadedFolders [ mount ] . pending ; loadedFolders [ mount ] . handler = ( state ) => { let req = new Object ( state . req ) as http . IncomingMessage & { tsstate : symbol ; pathprefix : string ; } ; req . url += state . url . pathname === mount && ! state . url . pathname . endsWith ( "/" ) ? "/" : "" ; req . tsstate = Symbol ( "state object pointer" ) ; queue [ req . tsstate as any ] = state ; server . requestHandler ( req , state . res , { wiki : loadedFolders [ mount ] . wiki , boot : loadedFolders [ mount ] . boot , pathPrefix : mount } ) ; } ; loadedFolders [ mount ] . events . emit ( "ws-client-preload" ) ; requests . forEach ( e => loadedFolders [ mount ] . handler ( e ) ) ; loadedFolders [ mount ] . pending = [ ] ; } type Handler = ( req : http . IncomingMessage , res : http . ServerResponse , state : TWState ) => void ; interface TWState { pathprefix : any ; params : string [ ] ; wiki : any ; server : TiddlyWikiServer ; urlInfo : UrlWithStringQuery ; queryParameters : ParsedUrlQuery ; allowAnon : boolean ; authenticatedUsername : string ; data : string | Buffer ; } interface Route { method : string ; path : RegExp ; handler : Handler ; bodyFormat ? : "stream" | "string" | "buffer" ; } declare class TiddlyWikiServer { wiki : any ; routes : any [ ] ; addAuthenticator : any ; authenticators : TiddlyServerAuthentication [ ] ; requestHandler : ( request : http . IncomingMessage , response : http . ServerResponse , options ? : any ) => void ; findMatchingRoute : ( request : http . IncomingMessage , state : any ) => Route ; variables : Record < string , any > ; constructor ( ... args ) ; } class TiddlyServerAuthentication { param server constructor ( private server , retrieve ) { this . authenticateRequest = ( request , response , state ) => { let tsstate = retrieve ( request . tsstate ) ; if ( ! tsstate . authAccountKey && state . allowAnon ) { return true ; } else if ( tsstate . authAccountKey ) { state . authenticatedUsername = tsstate . username ; return true ; } else { tsstate . throwReason ( 403 , "Unauthenticated users cannot access this wiki" ) ; return false ; } } ; } /**
     * Returns true if the authenticator is active, false if it is inactive,
     * or a string if there is an error
     */ init ( ) { return true ; } /**
     * Returns true if the request is authenticated and
     * assigns the "authenticatedUsername" state variable.
     *
     * Returns false if the request couldn't be authenticated,
     * having sent an appropriate response to the browser
     */ authenticateRequest : ( request : http . IncomingMessage & { tsstate : symbol ; } , response : http . ServerResponse , state ) => boolean ; }