import fs from"node:fs";import http from"node:http";import https from"node:https";import os from"node:os";import path from"node:path";import compression from"compression";import express from"express";import fileUpload from"express-fileupload";import{engine as handlebars}from"express-handlebars";import{Server as SocketServer}from"socket.io";import sessions from"../sessions.mjs";import*as sockets from"./sockets.mjs";import{getRoute}from"../../common/utils/helpers.mjs";import{fetchJsonWithTimeout}from"../../common/utils/http.mjs";import AuthView from"./views/auth.mjs";import PlayersView from"./views/players.mjs";import View from"./views/view.mjs";import ErrorView from"./views/error.mjs";import QuitView from"./views/quit.mjs";import StreamView from"./views/stream.mjs";import APIView from"./views/api.mjs";import FileUploadView from"./views/upload.mjs";import GameView from"./views/game.mjs";import JoinView from"./views/join.mjs";import SetupView from"./views/setup.mjs";import LicenseView from"./views/license.mjs";import ApplicationUpdateView from"./views/update.mjs";import{TIMEOUTS}from"../../common/constants.mjs";export default class Express{constructor(e,s){this.paths=s,process.env.NODE_ENV="production",this.hostname=e.hostname,this.routePrefix=e.routePrefix,this.port=e.port,this.proxyPort=e.proxyPort,this.ssl=e.isSSL,this.debug=e.debug,this.noIPDiscovery=e.noIPDiscovery,this.views=[new AuthView,new PlayersView,new GameView,new StreamView,new ErrorView,new JoinView,new SetupView,new LicenseView,new ApplicationUpdateView,new FileUploadView,new QuitView,new APIView],this.app=this._createApp({isProxy:e.proxyPort||e.proxySSL,compressStatic:e.compressStatic}),this.server=this._createServer({app:this.app,isSSL:this.ssl,sslKey:e.sslKey,sslCert:e.sslCert}),this.sessions=sessions,this.io=new SocketServer(this.server,{path:getRoute("socket.io",{prefix:this.routePrefix}),origins:"*:*",pingInterval:2e4,pingTimeout:6e5,cookie:!1,maxHttpBufferSize:1e8,perMessageDeflate:e.compressSocket}),this.addresses=null}static get CORE_VIEW_SCRIPTS(){return["scripts/jquery.min.js","scripts/handlebars.min.js","scripts/handlebars-intl.min.js","scripts/pixi.min.js","scripts/particle-emitter.min.js","scripts/pixi-graphics-smooth.js","scripts/basis.min.js","scripts/socket.io.min.js","scripts/tinymce.min.js","scripts/clipper/clipper.js","scripts/earcut-edges/earcut-edges.js","scripts/showdown.js","scripts/spark-md5.min.js"]}static get CORE_VIEW_STYLES(){return["fonts/fontawesome/css/all.min.css","css/style.css"]}static get CORE_VIEW_MODULES(){return[]}get address(){return`${this.ssl?"https":"http"}://localhost:${this.port}`+getRoute("",{prefix:this.routePrefix})}_createApp({compressStatic:e=!0,isProxy:s=!1}={}){const t=express();s&&t.set("trust proxy",!0),t.set("views",path.join(this.paths.root,"templates","views")),t.engine("hbs",handlebars({extname:".hbs",layoutsDir:path.join(this.paths.root,"templates","views","layouts"),defaultLayout:"main"})),t.set("view engine","hbs"),t.use(express.json()),t.use(express.urlencoded({extended:!0})),t.use(fileUpload({defParamCharset:"utf8"})),t.use((function(e,s,t){const{options:i}=global;if("/"===e.path.substr(-1)&&e.path.length>1){const t=e.url.slice(e.path.length),r=`${e.protocol}://${e.hostname}:${i.proxyPort??i.port}`;s.redirect(301,r+e.path.slice(0,-1)+t)}else t()}));const i=express.Router();return t.use(getRoute("",{prefix:this.routePrefix}),i),i.get(/^\/?$/,View.home),this._staticFiles(i,e),this._middleware(i),this._defineRoutes(i),t}_createServer({app:e,isSSL:s,sslKey:t,sslCert:i}={}){return s?https.createServer({key:fs.readFileSync(t,"utf8"),cert:fs.readFileSync(i,"utf8")},e):http.createServer(e)}_middleware(e){e.use(((e,s,t)=>{if(s.locals={bodyClass:"vtt game",release:global.release},global.fatalError)return View.error(e,s,t,global.fatalError);t()})),e.use(this._viewDataMiddleware.bind(this)),e.use(this.constructor._userSessionMiddleware),e.use((function(e,s,t){s.header("Access-Control-Allow-Origin","*"),s.header("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type, Accept"),s.header("X-Frame-Options","DENY"),t()}))}static _userSessionMiddleware(e,s,t){const{game:i}=global,r=sessions.getOrCreate(e,s);i.ready&&(e.user=r.worlds[i.world.id]||null),t()}_viewDataMiddleware(e,s,t){const{config:i}=global,r=e.url.slice(1).split("/")[0];s.locals={bodyClass:["vtt",r,game&&game.ready?`system-${game.system.id}`:null].filterJoin(" "),bodyStyle:"",pageDescription:"Foundry Virtual Tabletop - A Self-Hosted & Modern Role-playing Platform",pageTitle:"Foundry Virtual Tabletop",title:"Foundry Virtual Tabletop",release:global.release,eula:!i.license.needsSignature,addresses:this.addresses,routePrefix:this.routePrefix,commons:this.constructor.CORE_VIEW_MODULES,scripts:this.constructor.CORE_VIEW_SCRIPTS,styles:this.constructor.CORE_VIEW_STYLES,watermark:global.release.shortDisplay},t()}_defineRoutes(e){for(const s of this.views)s.hasGet&&e.get(s.route,s.handleGet.bind(s)),s.hasPost&&e.post(s.route,s.handlePost.bind(s));e.get("/license.html",((e,s)=>s.sendFile(path.join(this.paths.root,"license.html"))))}_staticFiles(e,s){const t=[/worlds\/(.*)\.db$/,/\/signature.json$/,/\/([0-9]{6}).ldb$/,/\/([0-9]{6}).log$/,/\/MANIFEST-([0-9]{6})$/,/\/CURRENT$/,/\/LOCK$/,/\/LOG$/,/\/LOG.old$/];e.use(((e,s,i)=>{for(const i of t)if(i.test(e.url))return s.status(403).end("403 Forbidden");i()})),s&&e.use(compression({threshold:"10KB"})),e.use(((e,s,t)=>{s.set("Cache-Control","GET"===e.method?"no-cache":"no-store"),t()})),e.use(express.static(this.paths.public)),e.use("/common",express.static(this.paths.common)),e.use(express.static(this.paths.data,{redirect:!1}));const i=["handlebars/dist","handlebars-intl/dist","jquery/dist","pixi.js/dist","@pixi/particle-emitter/dist","@pixi/graphics-smooth/dist","@pixi/basis/dist","@pixi/basis/assets","simple-peer","socket.io-client/dist","tinymce","showdown/dist","spark-md5"];for(let s of i)e.use("/scripts",express.static(this.paths.root+`/node_modules/${s}`));e.use("/scripts/pdfjs",express.static(`${this.paths.root}/node_modules/@foundryvtt/pdfjs`))}async listen(e){e=e||this.port;const{logger:s,options:t}=config;return new Promise(((i,r)=>{const o=async()=>{this.io.on("connection",(e=>sockets.activate(e,this.views))),s.info(`Server started and listening on port ${this.server.address().port}`),this.addresses=await this.getIPAddresses(),i(this)};this.server.on("error",r),t.protocol?this.server.listen(e,4===t.protocol?"0.0.0.0":"::",o):this.server.listen(e,o)}))}async getIPAddresses(){let e=Object.values(os.networkInterfaces()).reduce(((e,s)=>e.concat(s.filter((e=>"IPv4"===e.family&&!e.internal)))),[]);if(e=e.length?e[0].address:null,this.noIPDiscovery)return this.addresses||(this.addresses={local:config.options.localHostname||e,external:null,remote:null,remoteIsAccessible:!1}),this.addresses;let s=`https://api.foundryvtt.com/ip?port=${this.proxyPort??this.port}`;this.hostname&&(s+=`&ip_address=${this.hostname}`);let t=this.hostname,i=null;try{let e=await fetchJsonWithTimeout(s,{method:"GET",headers:{"Content-Type":"application/json","x-api-key":"zouM9GWWDG8LRlz0ZibdC4i3dymvRWoq3wZrKu29"}},{timeoutMs:TIMEOUTS.IP_DISCOVERY});t=e.ip_address,i=e.is_open}catch(e){logger.warn("Could not reach IP discovery service")}return t!==this.hostname&&t.match(/[A-z|:]/g)&&(i=void 0),{local:config.options.localHostname||e,external:t,remote:t,remoteIsAccessible:i}}async refreshAddresses(){this.addresses=await this.getIPAddresses()}getInvitationLinks(){const e=config.options,s=this.ssl||e.proxySSL?"https":"http",t=Number.isFinite(e.proxyPort)?e.proxyPort:this.port,i={};for(let e of["local","remote"]){if(!this.addresses[e]){i[e]=null;continue}let r=`${s}://${this.addresses[e]}`;[80,443].includes(t)||(r+=`:${t}`),i[e]=r+getRoute("",{prefix:this.routePrefix})}return i.remoteIsAccessible=this.addresses.remoteIsAccessible,i}}