175 lines
3.4 KiB
JavaScript
175 lines
3.4 KiB
JavaScript
"use strict";
|
|
|
|
const cl = global.botanLoader;
|
|
const Dragonfly = global.Dragonfly;
|
|
|
|
const Cookie = cl.load( "botanss.net.components.Cookie" );
|
|
|
|
class ContentSecurityPolicy
|
|
{
|
|
constructor()
|
|
{
|
|
this.sources = {};
|
|
}
|
|
|
|
any()
|
|
{
|
|
return 0 < Object.keys( this.sources ).length;
|
|
}
|
|
|
|
add( src, scope )
|
|
{
|
|
this.sources[ src ] ||= {};
|
|
this._add( this.sources[ src ], scope, src );
|
|
}
|
|
|
|
_add( s, scope, _name )
|
|
{
|
|
if( scope.startsWith( "'nonce-" ) && "'unsafe-inline'" in s )
|
|
{
|
|
Dragonfly.Warning( `Removing 'unsafe-inline' from ${_name} for ${scope}` );
|
|
delete s[ "'unsafe-inline'" ];
|
|
}
|
|
s[ scope ] = true;
|
|
}
|
|
|
|
merge( cspStr )
|
|
{
|
|
for( let src of cspStr.split( ";" ) )
|
|
{
|
|
src = src.trim();
|
|
if( !src )
|
|
continue;
|
|
|
|
var d = src.indexOf( " " );
|
|
|
|
var name = src.substr( 0, d );
|
|
|
|
this.sources[ name ] ||= {};
|
|
|
|
for( let val of src.substr( d + 1 ).split( " " ) )
|
|
{
|
|
this.sources[ name ][ val ] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
toString()
|
|
{
|
|
var s = "";
|
|
for( let name in this.sources )
|
|
{
|
|
if( s )
|
|
s += " ";
|
|
s += `${name} ${Object.keys( this.sources[ name ] ).join( " " )};`;
|
|
}
|
|
return s;
|
|
}
|
|
}
|
|
|
|
class CResponse
|
|
{
|
|
constructor( res, Http )
|
|
{
|
|
this.raw = res;
|
|
this.canExit = true;
|
|
|
|
this.statusCode = 200;
|
|
this.contentSecurityPolicy = new ContentSecurityPolicy();
|
|
this.headers = {
|
|
"Content-Type": "text/html; charset=utf-8"
|
|
, "Powered-By": "Botanical Framework (Node.js)"
|
|
, "Content-Security-Policy": this.contentSecurityPolicy
|
|
};
|
|
|
|
this.content = "";
|
|
this.cookie = new Cookie( "", Http );
|
|
}
|
|
|
|
mergeHeader( key, value )
|
|
{
|
|
switch( key )
|
|
{
|
|
case "Content-Security-Policy":
|
|
this.headers[ key ] = this.headers[ key ] + ' ' + value;
|
|
break;
|
|
default:
|
|
throw new Error( `Merge header not implemented: ${key}` );
|
|
}
|
|
}
|
|
|
|
end()
|
|
{
|
|
if( this.canExit )
|
|
{
|
|
this.canExit = false;
|
|
|
|
this.raw.writeHead( this.statusCode, this.headers );
|
|
this.raw.end( this.content );
|
|
|
|
if( this.raw._rejection )
|
|
{
|
|
process.removeListener( "unhandledRejection", this.raw._rejection );
|
|
}
|
|
|
|
if( this.raw._uncaught )
|
|
{
|
|
process.removeListener( "uncaughtException", this.raw._uncaught );
|
|
}
|
|
}
|
|
}
|
|
|
|
write( str ) { this.content = str }
|
|
writeLine( str ) { this.content += str + "\n"; }
|
|
|
|
}
|
|
|
|
class CRequest
|
|
{
|
|
get isPost() { return this.raw.method == 'POST'; }
|
|
get remoteAddr() { return this.raw.socket.remoteAddress; }
|
|
|
|
compat_parse_url( url, base = "http://example.invalid" )
|
|
{
|
|
var u = new URL( url, base );
|
|
var isRelative = !/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url);
|
|
|
|
return {
|
|
protocol: isRelative ? null : u.protocol,
|
|
slashes: isRelative ? null : true,
|
|
auth: u.username || u.password
|
|
? `${decodeURIComponent(u.username)}:${decodeURIComponent(u.password)}`
|
|
: null,
|
|
host: isRelative ? null : u.host,
|
|
port: isRelative ? null : (u.port || null),
|
|
hostname: isRelative ? null : u.hostname,
|
|
hash: u.hash || null,
|
|
search: u.search || null,
|
|
query: u.search ? u.search.slice(1) : null,
|
|
pathname: u.pathname,
|
|
path: u.pathname + u.search,
|
|
href: isRelative ? u.pathname + u.search + u.hash : u.href,
|
|
};
|
|
}
|
|
|
|
constructor( req, Http )
|
|
{
|
|
this.raw = req;
|
|
this.headers = req.headers;
|
|
this.uri = this.compat_parse_url( req.url );
|
|
this.cookie = new Cookie( req.headers.cookie, Http );
|
|
}
|
|
}
|
|
|
|
class Http
|
|
{
|
|
constructor( req, res )
|
|
{
|
|
// Simple Http Model
|
|
this.response = new CResponse( res, this );
|
|
this.request = new CRequest( req, this );
|
|
}
|
|
}
|
|
|
|
module.exports = Http;
|