import http from 'http';
import fs from 'fs';
import { extname } from 'path';
import * as pages from './oauth.js';
import dashboard from './guilds.js';
import { posts } from './functions.js';
import { db, sessionData, settingsData } from './util.js';
import Lang from './i18n.js';
const allLangs = Lang.allLangs();
const files = new Map([
...fs.readdirSync( './dashboard/src' ).map( file => {
return [`/src/${file}`, `./dashboard/src/${file}`];
} ),
...fs.readdirSync( './i18n/widgets' ).map( file => {
return [`/src/widgets/${file}`, `./i18n/widgets/${file}`];
} ),
...( fs.existsSync('./RcGcDb/start.py') ? fs.readdirSync( './RcGcDb/locale/widgets' ).map( file => {
return [`/src/widgets/RcGcDb/${file}`, `./RcGcDb/locale/widgets/${file}`];
} ) : [] )
].map( ([file, filepath]) => {
let contentType = 'text/html';
switch ( extname(file) ) {
case '.css':
contentType = 'text/css';
break;
case '.js':
contentType = 'text/javascript';
break;
case '.json':
contentType = 'application/json';
break;
case '.svg':
contentType = 'image/svg+xml';
break;
case '.png':
contentType = 'image/png';
break;
case '.jpg':
contentType = 'image/jpg';
break;
}
return [file, {path: filepath, contentType}];
} ));
const server = http.createServer( (req, res) => {
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
if ( req.method === 'POST' && req.headers['content-type'] === 'application/x-www-form-urlencoded' && ( req.url.startsWith( '/guild/' ) || req.url === '/user' ) ) {
let args = req.url.split('/');
let state = req.headers.cookie?.split('; ')?.filter( cookie => {
return cookie.split('=')[0] === 'wikibot' && /^"([\da-f]+(?:-\d+)*)"$/.test(( cookie.split('=')[1] || '' ));
} )?.map( cookie => cookie.replace( /^wikibot="([\da-f]+(?:-\d+)*)"$/, '$1' ) )?.join();
if ( state && sessionData.has(state) && settingsData.has(sessionData.get(state).user_id) &&
( ( args.length === 5 && ['settings', 'verification', 'rcscript'].includes( args[3] ) && /^(?:default|new|notice|\d+)$/.test(args[4])
&& settingsData.get(sessionData.get(state).user_id).guilds.isMember.has(args[2]) ) || req.url === '/user' ) ) {
let body = [];
req.on( 'data', chunk => {
body.push(chunk);
} );
req.on( 'error', () => {
console.log( '- Dashboard: ' + error );
res.end('error');
} );
return req.on( 'end', () => {
if ( process.env.READONLY ) return save_response(`${req.url}?save=failed`);
var settings = {};
Buffer.concat(body).toString().split('&').forEach( arg => {
if ( arg ) {
let setting = decodeURIComponent(arg.replace( /\+/g, ' ' )).split('=');
if ( setting[0] && setting.slice(1).join('=').trim() ) {
if ( settings[setting[0]] ) {
settings[setting[0]] += '|' + setting.slice(1).join('=').trim();
}
else settings[setting[0]] = setting.slice(1).join('=').trim();
}
}
} );
if ( isDebug ) console.log( '- Dashboard:', req.url, settings, sessionData.get(state).user_id );
if ( req.url === '/user' ) {
let setting = Object.keys(settings);
if ( setting.length === 1 && setting[0].startsWith( 'oauth_' ) && setting[0].split('_').length >= 3 ) {
setting = setting[0].split('_');
return posts.user(save_response, sessionData.get(state).user_id, setting[1], setting.slice(2).join('_'));
}
}
else return posts[args[3]](save_response, settingsData.get(sessionData.get(state).user_id), args[2], args[4], settings);
/**
* @param {String} [resURL]
* @param {String} [action]
* @param {String[]} [actionArgs]
*/
function save_response(resURL = '/', action, ...actionArgs) {
if ( action === 'REDIRECT' && resURL.startsWith( 'https://' ) ) {
res.writeHead(303, {Location: resURL});
return res.end();
}
var themeCookie = ( req.headers?.cookie?.split('; ')?.find( cookie => {
return cookie.split('=')[0] === 'theme' && /^"(?:light|dark)"$/.test(( cookie.split('=')[1] || '' ));
} ) || 'dark' ).replace( /^theme="(light|dark)"$/, '$1' );
var langCookie = ( req.headers?.cookie?.split('; ')?.filter( cookie => {
return cookie.split('=')[0] === 'language' && /^"[a-z\-]+"$/.test(( cookie.split('=')[1] || '' ));
} )?.map( cookie => cookie.replace( /^language="([a-z\-]+)"$/, '$1' ) ) || [] );
var dashboardLang = new Lang(...langCookie, ...( req.headers?.['accept-language']?.split(',')?.map( lang => {
lang = lang.split(';')[0].toLowerCase();
if ( allLangs.map.hasOwnProperty(lang) ) return lang;
lang = lang.replace( /-\w+$/, '' );
if ( allLangs.map.hasOwnProperty(lang) ) return lang;
lang = lang.replace( /-\w+$/, '' );
if ( allLangs.map.hasOwnProperty(lang) ) return lang;
return '';
} ) || [] ));
dashboardLang.fromCookie = langCookie;
return dashboard(res, dashboardLang, themeCookie, sessionData.get(state), new URL(resURL, process.env.dashboard), action, actionArgs);
}
} );
}
}
var reqURL = new URL(req.url, process.env.dashboard);
if ( req.method === 'HEAD' && files.has(reqURL.pathname) ) {
let file = files.get(reqURL.pathname);
res.writeHead(200, {'Content-Type': file.contentType});
return res.end();
}
if ( req.method !== 'GET' ) {
let body = '
' + http.STATUS_CODES[418] + '';
res.writeHead(418, {
'Content-Type': 'text/html',
'Content-Length': Buffer.byteLength(body)
});
res.write( body );
return res.end();
}
if ( reqURL.pathname === '/favicon.ico' ) reqURL.pathname = '/src/icon.png';
if ( files.has(reqURL.pathname) ) {
let file = files.get(reqURL.pathname);
res.writeHead(200, {'Content-Type': file.contentType});
return fs.createReadStream(file.path).pipe(res);
}
res.setHeader('Content-Type', 'text/html');
var themeCookie = ( req.headers?.cookie?.split('; ')?.find( cookie => {
return cookie.split('=')[0] === 'theme' && /^"(?:light|dark)"$/.test(( cookie.split('=')[1] || '' ));
} ) || 'dark' ).replace( /^theme="(light|dark)"$/, '$1' );
var langCookie = ( req.headers?.cookie?.split('; ')?.filter( cookie => {
return cookie.split('=')[0] === 'language' && /^"[a-z\-]+"$/.test(( cookie.split('=')[1] || '' ));
} )?.map( cookie => cookie.replace( /^language="([a-z\-]+)"$/, '$1' ) ) || [] );
var dashboardLang = new Lang(...langCookie, ...( req.headers?.['accept-language']?.split(',')?.map( lang => {
lang = lang.split(';')[0].toLowerCase();
if ( allLangs.map.hasOwnProperty(lang) ) return lang;
lang = lang.replace( /-\w+$/, '' );
if ( allLangs.map.hasOwnProperty(lang) ) return lang;
lang = lang.replace( /-\w+$/, '' );
if ( allLangs.map.hasOwnProperty(lang) ) return lang;
return '';
} ) || [] ));
dashboardLang.fromCookie = langCookie;
res.setHeader('Content-Language', [dashboardLang.lang]);
var lastGuild = req.headers?.cookie?.split('; ')?.filter( cookie => {
return cookie.split('=')[0] === 'guild' && /^"(?:user|\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new|notice))?)"$/.test(( cookie.split('=')[1] || '' ));
} )?.map( cookie => cookie.replace( /^guild="(user|\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new|notice))?)"$/, '$1' ) )?.join();
if ( lastGuild ) res.setHeader('Set-Cookie', ['guild=""; SameSite=Lax; Path=/; Max-Age=0']);
var state = req.headers.cookie?.split('; ')?.filter( cookie => {
return cookie.split('=')[0] === 'wikibot' && /^"([\da-f]+(?:-\d+)*)"$/.test(( cookie.split('=')[1] || '' ));
} )?.map( cookie => cookie.replace( /^wikibot="([\da-f]+(?:-\d+)*)"$/, '$1' ) )?.join();
if ( reqURL.pathname === '/login' ) {
let action = '';
if ( reqURL.searchParams.get('action') === 'failed' ) action = 'loginfail';
return pages.login(res, dashboardLang, themeCookie, state, action);
}
if ( reqURL.pathname === '/logout' ) {
sessionData.delete(state);
res.setHeader('Set-Cookie', [
...( res.getHeader('Set-Cookie') || [] ),
'wikibot=""; HttpOnly; SameSite=Lax; Path=/; Max-Age=0'
]);
return pages.login(res, dashboardLang, themeCookie, state, 'logout');
}
if ( reqURL.pathname === '/oauth/mw' ) {
return pages.verify(res, reqURL.searchParams, sessionData.get(state)?.user_id);
}
if ( !state ) {
let action = '';
if ( reqURL.pathname !== '/' ) action = 'unauthorized';
if ( reqURL.pathname.startsWith( '/guild/' ) ) {
let pathGuild = reqURL.pathname.split('/').slice(2, 5).join('/');
if ( /^\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new|notice))?$/.test(pathGuild) ) {
res.setHeader('Set-Cookie', [`guild="${pathGuild}"; SameSite=Lax; Path=/`]);
}
}
else if ( reqURL.pathname === '/user' ) {
if ( reqURL.searchParams.get('oauth') === 'success' ) action = 'oauth';
if ( reqURL.searchParams.get('oauth') === 'failed' ) action = 'oauthfail';
if ( reqURL.searchParams.get('oauth') === 'verified' ) action = 'oauthverify';
if ( reqURL.searchParams.get('oauth') === 'other' ) action = 'oauth';
res.setHeader('Set-Cookie', ['guild="user"; SameSite=Lax; Path=/']);
}
return pages.login(res, dashboardLang, themeCookie, state, action);
}
if ( reqURL.pathname === '/oauth' ) {
return pages.oauth(res, state, reqURL.searchParams, lastGuild);
}
if ( !sessionData.has(state) || !settingsData.has(sessionData.get(state).user_id) ) {
let action = '';
if ( reqURL.pathname !== '/' ) action = 'unauthorized';
if ( reqURL.pathname.startsWith( '/guild/' ) ) {
let pathGuild = reqURL.pathname.split('/').slice(2, 5).join('/');
if ( /^\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new|notice))?$/.test(pathGuild) ) {
res.setHeader('Set-Cookie', [`guild="${pathGuild}"; SameSite=Lax; Path=/`]);
}
}
else if ( reqURL.pathname === '/user' ) {
if ( reqURL.searchParams.get('oauth') === 'success' ) action = 'oauth';
if ( reqURL.searchParams.get('oauth') === 'failed' ) action = 'oauthfail';
if ( reqURL.searchParams.get('oauth') === 'verified' ) action = 'oauthverify';
if ( reqURL.searchParams.get('oauth') === 'other' ) action = 'oauth';
res.setHeader('Set-Cookie', ['guild="user"; SameSite=Lax; Path=/']);
}
return pages.login(res, dashboardLang, themeCookie, state, action);
}
if ( reqURL.pathname === '/refresh' ) {
let returnLocation = reqURL.searchParams.get('return');
if ( !/^\/(?:user|guild\/\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new|notice))?)$/.test(returnLocation) ) {
returnLocation = '/';
}
return pages.refresh(res, sessionData.get(state), returnLocation);
}
if ( reqURL.pathname === '/api' ) {
let wiki = reqURL.searchParams.get('wiki');
if ( wiki ) return pages.api(res, wiki);
}
let action = '';
if ( reqURL.searchParams.get('refresh') === 'success' ) action = 'refresh';
if ( reqURL.searchParams.get('refresh') === 'failed' ) action = 'refreshfail';
if ( reqURL.pathname === '/user' ) {
if ( reqURL.searchParams.get('oauth') === 'success' ) action = 'oauth';
if ( reqURL.searchParams.get('oauth') === 'failed' ) action = 'oauthfail';
if ( reqURL.searchParams.get('oauth') === 'verified' ) action = 'oauthverify';
if ( reqURL.searchParams.get('oauth') === 'other' ) action = 'oauthother';
}
return dashboard(res, dashboardLang, themeCookie, sessionData.get(state), reqURL, action);
} );
server.listen( 8080, () => {
console.log( '- Dashboard: Server running at http://localhost:8080/' );
} );
String.prototype.replaceSave = function(pattern, replacement) {
return this.replace( pattern, ( typeof replacement === 'string' ? replacement.replace( /\$/g, '$$$$' ) : replacement ) );
};
/**
* End the process gracefully.
* @param {NodeJS.Signals} signal - The signal received.
*/
function graceful(signal) {
console.log( '- Dashboard: ' + signal + ': Closing the dashboard...' );
server.close( () => {
console.log( '- Dashboard: ' + signal + ': Closed the dashboard server.' );
db.end().then( () => {
console.log( '- Dashboard: ' + signal + ': Closed the database connection.' );
process.exit(0);
}, dberror => {
console.log( '- Dashboard: ' + signal + ': Error while closing the database connection: ' + dberror );
} );
} );
}
process.once( 'SIGINT', graceful );
process.once( 'SIGTERM', graceful );