@ -1,15 +1,27 @@
'use strict' ;
'use strict' ;
const path = require ( 'path' ) ;
const bcrypt = require ( 'bcryptjs' ) ;
const bcrypt = require ( 'bcryptjs' ) ;
const crypto = require ( 'node:crypto' ) ;
const crypto = require ( 'node:crypto' ) ;
const { createServer } = require ( 'node:http' ) ;
const { stat , readFile } = require ( 'node:fs/promises' ) ;
const { join } = require ( 'node:path' ) ;
const express = require ( 'express' ) ;
const expressSession = require ( 'express-session' ) ;
const expressSession = require ( 'express-session' ) ;
const debug = require ( 'debug' ) ( 'Server' ) ;
const debug = require ( 'debug' ) ( 'Server' ) ;
const Util = require ( './Util' ) ;
const {
const ServerError = require ( './ServerError' ) ;
createApp ,
createError ,
createRouter ,
defineEventHandler ,
fromNodeMiddleware ,
getRouterParam ,
toNodeListener ,
readBody ,
setHeader ,
serveStatic ,
} = require ( 'h3' ) ;
const WireGuard = require ( '../services/WireGuard' ) ;
const WireGuard = require ( '../services/WireGuard' ) ;
const {
const {
@ -19,38 +31,50 @@ const {
PASSWORD ,
PASSWORD ,
LANG ,
LANG ,
UI_TRAFFIC_STATS ,
UI_TRAFFIC_STATS ,
UI_CHART_TYPE ,
} = require ( '../config' ) ;
} = require ( '../config' ) ;
module . exports = class Server {
module . exports = class Server {
constructor ( ) {
constructor ( ) {
// Express
const app = createApp ( ) ;
this . app = express ( )
this . app = app ;
. disable ( 'etag' )
. use ( '/' , express . static ( path . join ( __ dirname , '..' , 'www' ) ) )
app . use ( fromNodeMiddleware ( expressSession ( {
. use ( express . json ( ) )
secret : crypto . randomBytes ( 256 ) . toString ( 'hex' ) ,
. use ( expressSession ( {
resave : true ,
secret : crypto . randomBytes ( 256 ) . toString ( 'hex' ) ,
saveUninitialized : true ,
resave : true ,
} ) ) ) ;
saveUninitialized : true ,
} ) )
const router = createRouter ( ) ;
app . use ( router ) ;
. get ( '/api/release' , ( Util . promisify ( async ( ) => {
router
. get ( '/api/release' , defineEventHandler ( ( event ) => {
setHeader ( event , 'Content-Type' , 'application/json' ) ;
return RELEASE ;
return RELEASE ;
} ) ) )
} ) )
. get ( '/api/lang' , ( Util . promisify ( async ( ) => {
. get ( '/api/lang' , defineEventHandler ( ( event ) => {
return LANG ;
setHeader ( event , 'Content-Type' , 'application/json' ) ;
} ) ) )
return ` " ${ LANG } " ` ;
. get ( '/api/ui-traffic-stats' , ( Util . promisify ( async ( ) => {
} ) )
return UI_TRAFFIC_STATS === 'true' ;
} ) ) )
// Authentication
. get ( '/api/ui-traffic-stats' , defineEventHandler ( ( event ) => {
. get ( '/api/session' , Util . promisify ( async ( req ) => {
setHeader ( event , 'Content-Type' , 'application/json' ) ;
return ` " ${ UI_TRAFFIC_STATS } " ` ;
} ) )
. get ( '/api/ui-chart-type' , defineEventHandler ( ( event ) => {
setHeader ( event , 'Content-Type' , 'application/json' ) ;
return ` " ${ UI_CHART_TYPE } " ` ;
} ) )
// Authentication
. get ( '/api/session' , defineEventHandler ( ( event ) => {
const requiresPassword = ! ! process . env . PASSWORD ;
const requiresPassword = ! ! process . env . PASSWORD ;
const authenticated = requiresPassword
const authenticated = requiresPassword
? ! ! ( req . session && req . session . authenticated )
? ! ! ( event . node . req . session && event . node . req . session . authenticated )
: true ;
: true ;
return {
return {
@ -58,28 +82,35 @@ module.exports = class Server {
authenticated ,
authenticated ,
} ;
} ;
} ) )
} ) )
. post ( '/api/session' , Util . promisify ( async ( req ) => {
. post ( '/api/session' , defineEventHandler ( async ( event ) => {
const {
const { password } = await readBody ( event ) ;
password ,
} = req . body ;
if ( typeof password !== 'string' ) {
if ( typeof password !== 'string' ) {
throw new ServerError ( 'Missing: Password' , 401 ) ;
throw createError ( {
status : 401 ,
message : 'Missing: Password' ,
} ) ;
}
}
if ( password !== PASSWORD ) {
if ( password !== PASSWORD ) {
throw new ServerError ( 'Incorrect Password' , 401 ) ;
throw createError ( {
status : 401 ,
message : 'Incorrect Password' ,
} ) ;
}
}
req . session . authenticated = true ;
event . node . req . session . authenticated = true ;
req . session . save ( ) ;
event . node . req . session . save ( ) ;
debug ( ` New Session: ${ req . session . id } ` ) ;
debug ( ` New Session: ${ event . node . req . session . id } ` ) ;
} ) )
return { succcess : true } ;
} ) ) ;
// WireGuard
// WireGuard
. use ( ( req , res , next ) => {
app . use (
if ( ! PASSWORD ) {
fromNodeMiddleware ( ( req , res , next ) => {
if ( ! PASSWORD || ! req . url . startsWith ( '/api/' ) ) {
return next ( ) ;
return next ( ) ;
}
}
@ -87,7 +118,7 @@ module.exports = class Server {
return next ( ) ;
return next ( ) ;
}
}
if ( req . path . startsWith ( '/api/' ) && req . headers [ 'authorization' ] ) {
if ( req . url . startsWith ( '/api/' ) && req . headers [ 'authorization' ] ) {
if ( bcrypt . compareSync ( req . headers [ 'authorization' ] , bcrypt . hashSync ( PASSWORD , 10 ) ) ) {
if ( bcrypt . compareSync ( req . headers [ 'authorization' ] , bcrypt . hashSync ( PASSWORD , 10 ) ) ) {
return next ( ) ;
return next ( ) ;
}
}
@ -99,25 +130,32 @@ module.exports = class Server {
return res . status ( 401 ) . json ( {
return res . status ( 401 ) . json ( {
error : 'Not Logged In' ,
error : 'Not Logged In' ,
} ) ;
} ) ;
} )
} ) ,
. delete ( '/api/session' , Util . promisify ( async ( req ) => {
) ;
const sessionId = req . session . id ;
req . session . destroy ( ) ;
const router2 = createRouter ( ) ;
app . use ( router2 ) ;
router2
. delete ( '/api/session' , defineEventHandler ( ( event ) => {
const sessionId = event . node . req . session . id ;
event . node . req . session . destroy ( ) ;
debug ( ` Deleted Session: ${ sessionId } ` ) ;
debug ( ` Deleted Session: ${ sessionId } ` ) ;
return { success : true } ;
} ) )
} ) )
. get ( '/api/wireguard/client' , Util . promisify ( async ( req ) => {
. get ( '/api/wireguard/client' , defineEventHandler ( ( ) => {
return WireGuard . getClients ( ) ;
return WireGuard . getClients ( ) ;
} ) )
} ) )
. get ( '/api/wireguard/client/:clientId/qrcode.svg' , Util . promisify ( async ( req , res ) => {
. get ( '/api/wireguard/client/:clientId/qrcode.svg' , defineEventHandler ( async ( event ) => {
const { clientId } = req . params ;
const clientId = getRouterParam ( event , 'clientId' ) ;
const svg = await WireGuard . getClientQRCodeSVG ( { clientId } ) ;
const svg = await WireGuard . getClientQRCodeSVG ( { clientId } ) ;
res . header ( 'Content-Type' , 'image/svg+xml' ) ;
setHeader ( event , 'Content-Type' , 'image/svg+xml' ) ;
res . send ( svg ) ;
return svg ;
} ) )
} ) )
. get ( '/api/wireguard/client/:clientId/configuration' , Util . promisify ( async ( req , res ) => {
. get ( '/api/wireguard/client/:clientId/configuration' , defineEventHandler ( async ( event ) => {
const { clientId } = req . params ;
const clientId = getRouterParam ( event , 'clientId' ) ;
const client = await WireGuard . getClient ( { clientId } ) ;
const client = await WireGuard . getClient ( { clientId } ) ;
const config = await WireGuard . getClientConfiguration ( { clientId } ) ;
const config = await WireGuard . getClientConfiguration ( { clientId } ) ;
const configName = client . name
const configName = client . name
@ -125,52 +163,85 @@ module.exports = class Server {
. replace ( /(-{2,}|-$)/g , '-' )
. replace ( /(-{2,}|-$)/g , '-' )
. replace ( /-$/ , '' )
. replace ( /-$/ , '' )
. substring ( 0 , 32 ) ;
. substring ( 0 , 32 ) ;
res . header ( 'Content-Disposition' , ` attachment; filename=" ${ configName || clientId } .conf" ` ) ;
setHeader ( event , 'Content-Disposition' , ` attachment; filename=" ${ configName || clientId } .conf" ` ) ;
res . header ( 'Content-Type' , 'text/plain' ) ;
setHeader ( event , 'Content-Type' , 'text/plain' ) ;
res . send ( config ) ;
return config ;
} ) )
} ) )
. post ( '/api/wireguard/client' , Util . promisify ( async ( req ) => {
. post ( '/api/wireguard/client' , defineEventHandler ( async ( event ) => {
const { name } = req . body ;
const { name } = await readBody ( event ) ;
return WireGuard . createClient ( { name } ) ;
await WireGuard . createClient ( { name } ) ;
return { success : true } ;
} ) )
} ) )
. delete ( '/api/wireguard/client/:clientId' , Util . promisify ( async ( req ) => {
. delete ( '/api/wireguard/client/:clientId' , defineEventHandler ( async ( event ) => {
const { clientId } = req . params ;
const clientId = getRouterParam ( event , 'clientId' ) ;
return WireGuard . deleteClient ( { clientId } ) ;
await WireGuard . deleteClient ( { clientId } ) ;
return { success : true } ;
} ) )
} ) )
. post ( '/api/wireguard/client/:clientId/enable' , Util . promisify ( async ( req , res ) => {
. post ( '/api/wireguard/client/:clientId/enable' , defineEventHandler ( async ( event ) => {
const { clientId } = req . params ;
const clientId = getRouterParam ( event , 'clientId' ) ;
if ( clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype' ) {
if ( clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype' ) {
res . end ( 403 ) ;
throw createError ( { status : 403 } ) ;
}
}
return WireGuard . enableClient ( { clientId } ) ;
await WireGuard . enableClient ( { clientId } ) ;
return { success : true } ;
} ) )
} ) )
. post ( '/api/wireguard/client/:clientId/disable' , Util . promisify ( async ( req , res ) => {
. post ( '/api/wireguard/client/:clientId/disable' , defineEventHandler ( async ( event ) => {
const { clientId } = req . params ;
const clientId = getRouterParam ( event , 'clientId' ) ;
if ( clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype' ) {
if ( clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype' ) {
res . end ( 403 ) ;
throw createError ( { status : 403 } ) ;
}
}
return WireGuard . disableClient ( { clientId } ) ;
await WireGuard . disableClient ( { clientId } ) ;
return { success : true } ;
} ) )
} ) )
. put ( '/api/wireguard/client/:clientId/name' , Util . promisify ( async ( req , res ) => {
. put ( '/api/wireguard/client/:clientId/name' , defineEventHandler ( async ( event ) => {
const { clientId } = req . params ;
const clientId = getRouterParam ( event , 'clientId' ) ;
if ( clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype' ) {
if ( clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype' ) {
res . end ( 403 ) ;
throw createError ( { status : 403 } ) ;
}
}
const { name } = req . body ;
const { name } = await readBody ( event ) ;
return WireGuard . updateClientName ( { clientId , name } ) ;
await WireGuard . updateClientName ( { clientId , name } ) ;
return { success : true } ;
} ) )
} ) )
. put ( '/api/wireguard/client/:clientId/address' , Util . promisify ( async ( req , res ) => {
. put ( '/api/wireguard/client/:clientId/address' , defineEventHandler ( async ( event ) => {
const { clientId } = req . params ;
const clientId = getRouterParam ( event , 'clientId' ) ;
if ( clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype' ) {
if ( clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype' ) {
res . end ( 403 ) ;
throw createError ( { status : 403 } ) ;
}
}
const { address } = req . body ;
const { address } = await readBody ( event ) ;
return WireGuard . updateClientAddress ( { clientId , address } ) ;
await WireGuard . updateClientAddress ( { clientId , address } ) ;
} ) )
return { success : true } ;
} ) ) ;
// Static assets
const publicDir = '/app/www' ;
app . use (
defineEventHandler ( ( event ) => {
return serveStatic ( event , {
getContents : ( id ) => readFile ( join ( publicDir , id ) ) ,
getMeta : async ( id ) => {
const stats = await stat ( join ( publicDir , id ) ) . catch ( ( ) => { } ) ;
if ( ! stats || ! stats . isFile ( ) ) {
return ;
}
if ( id . endsWith ( '.html' ) ) setHeader ( event , 'Content-Type' , 'text/html' ) ;
if ( id . endsWith ( '.js' ) ) setHeader ( event , 'Content-Type' , 'application/javascript' ) ;
if ( id . endsWith ( '.json' ) ) setHeader ( event , 'Content-Type' , 'application/json' ) ;
if ( id . endsWith ( '.css' ) ) setHeader ( event , 'Content-Type' , 'text/css' ) ;
if ( id . endsWith ( '.png' ) ) setHeader ( event , 'Content-Type' , 'image/png' ) ;
return {
size : stats . size ,
mtime : stats . mtimeMs ,
} ;
} ,
} ) ;
} ) ,
) ;
. listen ( PORT , WEBUI_HOST , ( ) => {
createServer ( toNodeListener ( app ) ) . listen ( PORT , WEBUI_HOST ) ;
debug ( ` Listening on http:// ${ WEBUI_HOST } : ${ PORT } ` ) ;
debug ( ` Listening on http:// ${ WEBUI_HOST } : ${ PORT } ` ) ;
} ) ;
}
}
} ;
} ;