Files
BotanSS/net/Http.js
T
2026-06-13 04:22:12 +08:00

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;