UNPKG

sicarii

Version:

The zero dependency http2 nodejs multithreading framework

2,038 lines (1,505 loc) 131 kB
![](https://i.ibb.co/GcZZgw0/SICARII-1280.png) The zero dependency http2 nodejs multithreading framework ![cd-img] ![dep-img] ![sz-img] [![NPM Version][npm-img]][npm-url] ![lic-img] # [SICARII Website](https://angeal185.github.io/sicarii/) # [SICARII Dev Wiki](https://github.com/angeal185/sicarii/wiki) # Documentation - [Installation](#installation) - [About](#about) - [Initialization](#initialization) - [build](#build) - [server](#server) - [sync](#sync) - [router](#router) - [configuration](#configuration) - [stream](#stream) - [push handler](#push-handler) - [headers](#headers) - [app](#app) - [body parser](#body-parser) - [etags](#etags) - [cookie parser](#cookie-parser) - [template engines](#template-engines) - [botnet](#botnet) - [ip blacklist](#ip-blacklist) - [ip whitelist](#ip-whitelist) - [auth-token](#auth-token) - [cache](#cache) - [store](#store) - [sessions](#sessions) - [compression](#compression) - [static file server](#static-file-server) - [MIME types](#mime-types) - [logs](#logs) - [crypt](#crypt) - [backwards-compatibility](#backwards-compatibility) # Installation - [Back to index](#documentation) npm stable release ```sh $ npm install sicarii --save ``` dev release git ```sh $ git clone https://github.com/angeal185/sicarii.git ``` # About - [Back to index](#documentation) Sicarii is a nodejs http2 framework for projects of all sizes. * zero dependencies forever * multithreading by default * does not need or use the nodejs http2 compatibility layer api but can be extended to do so * non restrictive design in that you can use the sicarii api or vanilla nodejs most of the time * easily extendable # Initialization - [Back to index](#documentation) As sicarii is built for http2, SSL certificates are required. The default path for the ssl certificates is as follows: * `./cert/localhost.cert` * `./cert/localhost.key` These options be edited in the default `./config/config.json` file at `config.ssl`. * for using the `key/cert/pfx/ca` options, a path to the file should be provided as the arg. * `config.server` accepts all of the same default arguments as nodejs http2 server config. * sicarii will automatically combine `config.ssl` with `config.server` self signed certificates can be used for development and created as follows: ECDSA ```bash $ openssl ecparam -name secp384r1 -genkey -out localhost.key $ openssl req -new -x509 -key localhost.key -out localhost.cert -days 365 ``` RSA ```bash $ openssl req -x509 -new -x509 -sha256 -newkey rsa:4096 -nodes -keyout localhost.key -days 365 -out localhost.cert ``` # Build - [Back to index](#documentation) run the following line of code in any file inside your cwd to build sicarii. ```js require('sicarii/build')(); ``` Upon first run and if no config file is found, sicarii will attempt to generate the following. * `./config` ~ default config directory. * `./config/config.json` ~ default config file. * `./config/ip_config.json` ~ default ip whitelist/blacklist file. * `./render` ~ default render/document directory. * `./render/index.html` ~ starter html file. * `./static` ~ default static file directory. * `./static/css/main.css` ~ starter css file. * `./static/modules/main.mjs` ~ starter mjs file. * `./uploads` ~ default upload directory. * `./logs` ~ default logs directory. * `./store/cache` ~ default cache write dir. * `./store/session` ~ default session write dir. * `./store/store` ~ default store write dir. this action is sandboxed for security reasons. should you wish to, you can delete the associated build files: * `/sicarii/build.js` * `/sicarii/lib/utils/init.js` ```js const { app } = require('sicarii'); app.del_build() ``` # Server - [Back to index](#documentation) Sicarii is built to incorporate multi-threading by default. you can edit your thread count at `config.cluster.workers` Although many frameworks wrap the server object within their app, limiting your server actions to those they wish you to have access to, sicarii does not. sicarii extends the existing nodejs modules in place, leaving you full access to the nodejs server object. Most of these extensions can be either disabled, replaced, configured or extended. sicarii uses the modern nodejs http2 api and does not need or contain any code related to the http2 Compatibility API. Below is a 30 second minimal `rest-api`/`static server` example. ```js const { app, cluster } = require('sicarii'); // app is always called first if (cluster.isMaster) { const { sync, Cache, server, logs } = require('sicarii/master'); // Cache extentions here ~ if any // server extentions here ~ if any // logs extentions here ~ if any // * spawn workers // * synchronize master/worker communication // * respawn dead workers // * start cache on config.cache.port cache port // * automatically handle cache requests // * automatically handle log requests sync.init().respawn().listen(); } else { const { server, router, crypt } = require('sicarii/main'); // serve static router.get('/', function(stream, headers, flags){ stream.addHeaders({ header2: 'ok2', header3: 'ok3' }); //stream headers and and send static document stream.doc('index.html', 'text/html; charset=utf-8'); }); // json rest router.post('/', function(stream, headers, flags){ let body = stream.body.json; stream.addHeader('x-Static', 'ok'); // send headers & response stream.json({key: 'val'}); }); //start worker server at config.port server.listen(app.config.port); } ``` #### server.log_ip() refer to logs for a detailed explanation log ip address ```js /** * @server.log_ip(ip, path) * * @param {string} ip ~ ip address to log * @param {string} path ~ path to log **/ router.get('/login', function(stream, headers, flags){ server.log_ip(stream.ip, '/login') }); ``` #### server.log_history() refer to logs for a detailed explanation log history ```js /** * @server.log_history(data) * * @param {string} data ~ history data to log **/ router.get('/login', function(stream, headers, flags){ let his_log = [Date.now(), 'GET', '/login'].join('::::') server.log_history(his_log) }); ``` #### server.log_error() refer to logs for a detailed explanation log error ```js /** * @server.log_error(data) * * @param {string} data ~ error data to log **/ router.get('/someerror', function(stream, headers, flags){ let his_log = [Date.now(), 'GET', '/someerror', '400', 'some message'].join('::::') server.log_error(his_log) }); ``` #### server.pre_cache() server.pre_cache() will enable you to pre-cache your static files/docs. * this method can only be called once and upon doing so, it will remove itself * this method is apart of `sync` although you may have many workers, it will only be called once. * this method is for static files/docs only, it is not intended for rendered docs * `config.pre_cache` is the path to your pre_cache config file * `config.verbose` enabled will log to console the cache status of a streamed file the configuration file can be configured like so: ```js /* ./config/pre_cache.json */ { "render": [{ "ctype": "text/html", // file content-type 'only' "url": "/index.html" // file path relative to render path }], "static": [{ "ctype": "text/css", "url": "/css/main.css" // file path relative to static path },{ "ctype": "application/javascript", "url": "/modules/main.mjs" }] } ``` the method can be called like so: ```js const { app, cluster } = require('sicarii'); if(cluster.isMaster) { const { sync } = require('sicarii/master'); sync.init().respawn().listen(); } else { const { server, router } = require('sicarii/main'); router.get('/', function(stream, headers, flags){ // stream.doc() static files are located in the render folder // this file has been cached // [sicarii:GET] /index.html 200 [cache] stream.status(200).doc('index.html', 'text/html') // stream.render() files are located in the render folder but are not static // this has not been rendered/cached properly // do not pre-cache rendered files stream.status(200).render('index.html', {not: 'cached'}) }); // can be optionally called in chain // sync will ensure the method is only called by first worker server.pre_cache().listen(app.config.port); } ``` #### server.push_handler() refer to push handler server.push_handler() will enable/disable automatic stream push of static files. * this method takes priority over `config.push_handler.enabled` ```js const { app, cluster } = require('sicarii'); if(cluster.isMaster) { const { sync } = require('sicarii/master'); sync.init().respawn().listen(); } else { const { server, router } = require('sicarii/main'); router.get('/', function(stream, headers, flags){ stream.status(200).doc('index.html', 'text/html') }); //enable push_handler manually server.pre_cache().push_handler(true).listen(app.config.port); } ``` # Sync - [Back to index](#documentation) the sync object is is used to control and synchronize events between master/worker * sync is optionally responsible for all tasks related to the cluster module * sync will automatically handle spawning of new worker threads * sync will automatically handle respawning of crashed worker threads * sync will automatically handle inter-process messaging across all processes * sync will automatically initialize Cache and start the cache server * sync will handle inter-process ip/history/error logging * sync is a part of the master scope #### sync.init() ```js /** * sync.listen(settings) * @param {object} settings // optional worker settings overrides to config.cluster.settings **/ if (cluster.isMaster) { const { sync } = require('sicarii/master'); // * spawn workers // * synchronize master/worker communication sync.init(); } ``` #### sync.respawn() ```js if (cluster.isMaster) { const { sync } = require('sicarii/master'); // * respawn dead workers sync.respawn() // or sync.init().respawn() } ``` #### sync.listen() ```js /** * sync.listen(callback) * @param {function} callback // optional callback **/ if (cluster.isMaster) { const { sync } = require('sicarii/master'); // * start cache on config.cache.port cache port sync.listen() // or sync.init().respawn().listen() } ``` #### sync.kill() ```js /** * sync.kill(id) * @param {number} id // id of the worker to kill **/ if (cluster.isMaster) { const { sync } = require('sicarii/master'); sync.init().respawn().listen() // kill worker with id 1 then worker with id 2 // with sync.respawn() active these workers will be respawned setTimeout(function(){ sync.kill(1).kill(2) },5000) } ``` #### sync.kill_all() ```js if (cluster.isMaster) { const { sync } = require('sicarii/master'); sync.init().respawn().listen() // kill all workers // with sync.respawn() active these workers will be respawned setTimeout(function(){ sync.kill_all(1) },5000) } ``` # Router - [Back to index](#documentation) #### methods The default allowed router methods can and should be configured at `config.stream.methods`. * `config.stream.methods` accepts all compatible http methods. you should only add the ones you use. * `config.stream.method_body` contains all of the router methods that accept a body. * `config.stream.method_query` contains all of the router methods that accept a query string. * If you are not using a method in your app, you should remove it to improve both the security and performance of your app. the router also accepts all of the default nodejs stream methods. below listed are some basic router method examples: ```js /** * router[event](path, callback) * @param {string} path * @param {function} callback ~ function(stream, headers, flags) **/ router.get('/test', function(stream, headers, flags){ let query = stream.query; //json object // add header stream.addHeader('Content-Type', 'application/json'); // add cookie stream.cookie('name', 'value',{ Domain: 'localhost', Path: '/', Expires: Date.now(), MaxAge: 9999, HttpOnly: true, SameSite: 'Strict', Secure: true, Priority: 'High' }) // send headers & response stream.json({test: 'get'}); /* or using default nodejs methods */ stream.respond(stream.headers); stream.end(JSON.stringify({test: 'get'})) }); // head stream router.head('/test', function(stream, headers, flags){ let query = stream.query; }); // trace stream router.trace('/test', function(stream, headers, flags){ let query = stream.query; }); // post stream router.post('/', function(stream, headers, flags){ let body = stream.body.text; // stream.body.buffer / stream.body.json console.log(body) }); // delete stream router.delete('/', function(stream, headers, flags){ let body = stream.body.text; // stream.body.buffer / stream.body.json console.log(body) }); // patch stream router.patch('/', function(stream, headers, flags){ let body = stream.body.text; // stream.body.buffer / stream.body.json console.log(body) }); // put stream router.put('/', function(stream, headers, flags){ let body = stream.body.text; // stream.body.buffer / stream.body.json console.log(body) }); router.get('/', function(stream, headers, flags){ //serve headers and serve static document stream.doc('/index.html', 'text/html; charset=utf-8'); }); // send response headers and render static document router.get('/', function(stream, headers, flags){ stream.doc('index.html', 'text/html; charset=utf-8'); }); router.get('/', function(stream, headers, flags){ stream.addHeader('key', 'val'); stream.doc('index.html', 'text/html; charset=utf-8'); }); // send response headers and render with optional template engine installed router.get('/', function(stream, headers, flags){ // basic ~ default stream.render('index.html', {title: 'basic'}) // nunjucks stream.render('index.njk', {title: 'nunjucks'}) // pug stream.render('index.pug', {title: 'pug'}) }); ``` # Configuration - [Back to index](#documentation) sicarii has a tiny but powerful list of configurations the configuration file at `./config/config.json` is an essential part of sicarii. you MUST tweak it to your own requirements in order to maximize performance and security. ```js //defaults { "port": 8080, // server port "origin": "https://localhost", // server origin "verbose": true, // show log to console "dev": true, // log errors to console "proxy": false, // x-forwarded-for as ip address "ip_config": "/config/ip_config", // path to ip_config.json "pre_cache": "/config/pre_cache", // path to pre_cache.json "push_handler": { // automatic push handler "enabled": true, "accept": ["text/html"], // accept header document types to accept "path": "/config/push" // path to push config file }, "cluster": { "workers": 2 // worker count "settings": { //worker settings "serialization": "json" } }, "sync": { "respawn": true // auto-respawn dead workers }, "session": { "path": "/store/session/db.json", //read/write dir relative to cwd "maxage": 1000000, //maxage of sessions in ms "secret": "" //optional session secret }, "cache": { "url":"https://localhost:5000", // cache server url "timeout": 5000, //cache response timeout ms "proxy": false, // x-forwarded-for as ip address "authtoken": { //cache auth-token header "enabled": false, "header": "X-Authtoken", "token": "12345" }, "whitelist": { //cache server ip whitelist "enabled": true, "ip": ["::ffff:127.0.0.1"] //cache whitelisted ip addersses }, "server": { //cache server config ~ accepts all nodejs http2 server settings "rejectUnauthorized": false }, "headers": { //cache server outbound headers } }, "cookie_parser": { "enabled": true, //enable cookie parser "auto_parse": true, //enable auto cookie parse "sig": { "hmac": "secret", // cookie sign hmac "prefix": "sig" // cookie sig prefix } }, "stream": { "path_limit": 100, // stream path size limit ~ false to disable check "case_sensitive": true, // converts url pathnames to lowercase if false "param_limit": 1000, // stream url search size limit ~ false to disable check "body_limit": 5000, // stream body size limit ~ false to disable check "methods": [ // add all allowed http methods ~ remove if unused "get", "post", "connect", "put", "delete", "head" ], "querystring": true, // enable stream.qs "method_body": ["post", "delete", "patch", "put"], // methods return body "method_query": ["get","connect", "head", "options", "trace"],// methods return query params "content_types": [ // accepted body content-types ~ remove if unused "application/json", "text/plain", "multipart/form-data", "application/x-www-form-urlencoded" ] }, "blacklist": { //enable server ip blacklist "enabled": false, "msg": "your ip has been blacklisted, have a nice day" // unauth msg }, "whitelist": { //enable server ip whitelist "enabled": false, "msg": "Unauthorized" // unauth msg }, "authtoken": { //enable auth token header "enabled": false, "header": "X-Authtoken", "token": "xxxxxx", "msg": "server offline" // unauth msg }, "server": { // accepts all http2 nodejs server options }, "ssl": { "cert": "/cert/localhost.cert", // key/cert/pfx/ca as string path to file "key": "/cert/localhost.key" }, "store": { // sicarri store "path": "/store/store/db.json" // read/write path relative to cwd }, "uploads": { "enabled": true, "path": "/uploads", // uploads dir, relative to cwd() "recursive": true, //enable recursive folder creation "gzip": true, // compress file using gzip "brotli": false, // compress file using brotli "deflate": false, // compress file using deflate "mimetypes": { // accepted mimetypes }, "max_filename": 30, // max filename length "max_filesize": 50000 // max upload content length }, "static": { "path": "/static", // default static file path "blocked": [], "etag": { // etag header "enabled": true, // use etags on rendered files "digest": "sha3-256", //etag digest hash ~ crypto.getHashes(); "encode": "base64" //etag digest encoding hex/base64 }, "cache": { // static file server cache "enabled": true, // enable cache on static file server "maxage": 1000000 // cached items maxAge }, "headers": {} // default headers for static file server }, "render": { // render/tempate engine defaults "path": "/render", "blocked": [], "etag": { // etag header "enabled": true, // use etags on rendered files "digest": "sha3-256", //etag digest hash ~ crypto.getHashes(); "encode": "base64" //etag digest encoding hex/base64 }, "cache": { // rendered files cache "enabled": true, // enable cache on rendered files "maxage": 1000000 // cached items maxAge }, "headers": { // default headers for rendered files "X-Frame-Options": "DENY", "Referrer-Policy": "no-referrer", "Server": "Nodejs", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET", "X-DNS-Prefetch-Control": "on", "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "X-Content-Type-Options": "nosniff", "X-XSS-Protection": "1", "TK": "N" } }, "compression": { "gzip": { // gzip compression "enabled": true, "prezipped": false, // use pre-compressed files "ext": ".gz", // compressed file extention "setting": {} // accepts all nodejs gzip compression settings }, "brotli": { // brotli compression "enabled": false, "prezipped": false, // use pre-compressed files "ext": ".br", // compressed file extention "setting": {} // accepts all nodejs brotli compression settings }, "deflate": { // deflate compression "enabled": false, "prezipped": false, // use pre-compressed files "ext": ".dfl", // compressed file extention "setting": {} // accepts all nodejs deflate compression settings } }, "cors": { // default stream.cors fallback "origin": '', // string | Access-Control-Allow-Origin "methods": '', // string | Access-Control-Allow-Methods "allow_headers": '', // string | Access-Control-Allow-Headers "expose_headers": '',// string | Access-Control-Expose-Headers "credentials": true, // boolean | Access-Control-Allow-Credentials "maxage": 9999 // number | Access-Control-Max-Age }, "csp": { // content security policy object "default": "default-src 'self'" }, "feature_policy": { // feature policy object "default": "microphone 'none'; geolocation 'none'" }, "logs": { "path": "/logs", //path to log dir "separator": "|", // log separator "logs":["error", "history","ip"], "cron": 86400000, // logs cronjob interval "console_error": false, //log to console log-related errors "compression": "gzip", // backup compression ~ gzip/deflate/brotli "encodeURIComponent": false, // encode log entries "error": { "enabled": true, // enable auto error logs "max_size": 5000, // log max file size "base_name": "error", //log file base name "ext": ".txt" //log file base extension }, "history": { "enabled": true, // enable auto history logs "max_size": 5000, // log max file size "base_name": "history", //log file base name "ext": ".txt" //log file base extension }, "ip": { "enabled": true, // enable ip logging "max_size": 5000, // log max file size "base_name": "ip", //log file base name "ext": ".txt" //log file base extension "log_time": true, // add timestamp to log "log_path": true // add path to log } }, "template_engine": { // template engine config "engines": [ "basic", "poorboy", "nunjucks", "ejs", "pug", "mustache", "twig", "squirrelly", "ect", "eta", "liquidjs" ], "basic": { "enabled": true, "settings": { "pretty": false, "filters": {}, "cache": false } }, "squirrelly": { "enabled": false, "settings": {} }, "eta": { "enabled": false, "settings": {} }, "liquidjs": { "enabled": false, "settings": { "extname": ".liquid" } }, "ect": { "enabled": false, "settings": { "cache": false, "open": "<%", "close": "%>" } }, "poorboy": { "enabled": false, "settings": { "use_globals": false, "globals": {} } }, "nunjucks": { "enabled": false, "jinjacompat": true, "filters": "", "globals": { "enabled": false, "vars": {} }, "settings": { "autoescape": true, "noCache": true, "throwOnUndefined": false, "trimBlocks": false, "lstripBlocks": false, "tags": {} } }, "ejs": { "enabled": false, "settings": {} }, "pug": { "enabled": false, "settings": { "pretty": false, "filters": {}, "cache": false } }, "mustache": { "enabled": false, "tags": ["{{", "}}"], "settings": {} }, "twig": { "enabled": false, "settings": {} } }, "mimetypes": { // a list of all your allowed mimetypes }, "crypt": { "jwt":{ "secret": "secret", // jwt secret for hmac "digest": "sha256", // jwt digest for hmac "encode": "base64", // jwt encoding "separator": ":", // jwt token separator "header": { // jwt header "typ": "JWT", "alg": "HS256" }, "claims": { "iss": "token issuer", // optional jwt issuer "sub": "token subject", // optional jwt subject "aud": "token audience", // optional jwt audience "exp": 5000000, // mandatory ms till expires "nbf": 0 // optional ms till valid } }, "hmac": { "secret": "secret", // hmac secret "digest": "sha3-512", // hmac hash function "encode": "hex" // output encode }, "pbkdf2": { "digest": "sha3-512", // hash function "encode": "hex", // output encode "iterations": 50000 // kdf iterations }, "scrypt": { "encode": "hex", // output encode "cost": 16384, // scrypt cost "blockSize":8, // scrypt cost "parallelization": 1 // scrypt parallelization }, "encryption": { "secret": "", // encrypt/decrypt ~ app secret "secret_len": 32, // correct key length "iterations": 60000, // iterations to be used in keygen "digest": "sha3-512", // digest to be used in keygen "settings": { // THESE SETTINGS MUST BE VALID "cipher": "aes", // encrypt/decrypt cipher "bit_len": "256", // encrypt/decrypt bit "iv_len": 32, // encrypt/decrypt iv length "tag_len": 16, // encrypt/decrypt auth-tag length "encode": "hex", // encrypt/decrypt/keygen encoding "mode": "gcm" // encrypt/decrypt mode } }, "ecdsa": { "curve": "secp521r1", // ecdsa curve "encode": "hex", // ecdsa encoding "hash": "sha3-512", // ecdsa hash used "privateKey": { // accepts all nodejs ec privateKey settings "type": "sec1", "format": "der" }, "publicKey": { // accepts all nodejs ec publicKey settings "type": "spki", "format": "der" } }, "ecdh": { // ecdh key exchange "curve": "secp521r1", // ecdh curve "encode": "hex" // ecdh encoding }, "rsa": { // rsa encryption "length": 4096, // rsa modulusLength "publicExponent": 65537, "encode": "hex", "oaepHash": "sha512", // rsa oeap hash used "publicKey": { // accepts all nodejs rsa publicKey settings "type": "pkcs1", "format": "pem" }, "privateKey": { // accepts all nodejs rsa privateKey settings "type": "pkcs8", "format": "pem" } }, "otp": { // contains the one time pad defaults "rounds": 1, // otp encrypt/decrypt rounds count "iterations": 10000, // iteration count for generating a secure pad "digest": "sha512", // digest used for generating a secure pad "encode": "hex" // encoding used for otp } }, "bot": { "detect": { "items": ["Googlebot"] // manual detect bots via user-agent sub-string }, "block": { // automatically block bots via user-agent sub-string "enabled": false, "msg": "Unauthorized", // bot block msg "items": [] // blocked bots array } } } ``` # Stream - [Back to index](#documentation) accepts all nodejs methods and the following: #### stream.doc(src, content-type) stream doc will serve a document from the render folder * this method will use cache if available * this method will use compression if available * this method will stream respond headers * this method will send default headers from `config.render.headers` * this method will use etag settings from `config.render.etag` * this method will use cache settings from `config.render.cache` * this method will use gzip/brotli/deflate settings from `config.compression` ```js /** * stream.doc(path, contentType, callback) * @param {string} path // file path relative to render dir * @param {string} contentType // file content-type * @param {function} callback ~ optional **/ router.get('/', function(stream, headers, flags){ // send response headers and render static document from the render dir stream.doc('index.html', 'text/html; charset=utf-8'); }); ``` #### stream.render(src, data) stream render will serve a rendered document from the render folder. refer to template engines. * this method will use cache if available * this method will use compression if available * this method will stream respond headers * this method will send default headers from `config.render.headers` * this method will use etag settings from `config.render.etag` * this method will use cache settings from `config.render.cache` * this method will use gzip/brotli/deflate settings from `config.compression` ```js /** * stream.render(path, obj, callback) * @param {string} path // file path relative to render dir * @param {object} obj // data for rendered file * @param {function} callback ~ optional **/ router.get('/', function(stream, headers, flags){ // basic ~ default: uses template literals in html documents stream.render('index.html', {title: 'basic'}) // nunjucks ~ requires manual installation of nunjucks stream.render('index.njk', {title: 'nunjucks'}) // pug ~ requires manual installation of pug stream.render('index.pug', {title: 'pug'}) }); ``` #### stream.pushStatic(path, ctype) stream pushStatic will push a file or files from the static folder before requested. * this method can be chained to push multiple files * this method will use cache if available * this method will use compression if available * this method will send default headers from `config.static.headers` * this method will use etag settings from `config.static.etag` * this method will use cache settings from `config.static.cache` * this method will use gzip/brotli/deflate settings from `config.compression` * `config.verbose` will log the push state of a file * this method is asynchronous so the `stream` object is immediately returned * any errors are handled by sicarii in the same manner as the static file server ```js /** * stream.pushStatic(path, ctype) // single file * @param {string} path // file path relative to static dir as it would be requested as * @param {string} ctype // file content type as it would be requested as * * stream.pushStatic(obj) // multiple files * @param {object} obj // obj.path: file path, obj.ctype: content-type **/ router.get('/', function(stream, headers, flags){ /* push files to the browser before the browser requests them */ // push a file before it has been requested stream .pushStatic('/css/main.css', 'text/css') .status(200) .doc('index.html', 'text/html') // or push multiple files before they have been requested stream .pushStatic([{ path: '/css/main.css', // file path ctype: 'text/css' // file content type },{ path: '/favicon.ico', ctype: 'image/x-icon' }]) .status(200) .render('index.html', {test: 'push'}, function(err){ if(err){return console.error(err)} }) }); ``` #### stream.download(file, content-type) stream.download will initiate a file download upon browser navigation. * stream.download uses `config.static` settings * this method will use cache if available * this method will use compression if available * this method will stream respond headers * this method will send default headers from `config.static.headers` * this method will use etag settings from `config.static.etag` * this method will use cache settings from `config.static.cache` * this method will use gzip/brotli/deflate settings from `config.compression` * this method will Content-Disposition 'attachment; filename="the files name"' to the headers; ```js /** * stream.download(path, contentType, callback) * @param {string} path // file path relative to static dir * @param {string} contentType // file content-type * @param {function} callback ~ optional **/ router.get('/downloadpath', function(stream, headers, flags){ // main.mjs will download when /downloadpath is navigated to in the browser stream.download('modules/main.mjs', 'application/javascript'); }); ``` #### stream.upload(object, callbabk) stream.upload will upload a file to your uploads dir if enabled at `config.uploads.enable` * `config.uploads.gzip` will enable/disable gzip compression for uploads * `config.uploads.brotli` will enable/disable brotli compression for uploads * `config.uploads.deflate` will enable/disable deflate compression for uploads * `config.uploads.path` is your upload path relative to cwd() * `config.uploads.recursive` will enable recursive dir creation within `config.uploads.path`, * `config.uploads.mimetypes` should list all accepted upload mimetypes in the same format as `config.mimetypes` * `config.uploads.max_filename` max filename length * `config.uploads.max_filesize` max content length simple upload example: ```js /** * stream.upload(settings, callback) * @param {object} settings // upload settings * @param {function} callback ~ function(err,res) | optional **/ router.post('/upload', function(stream, headers, flags){ let ctype = headers.get('content-type'); if(ctype !== 'application/json'){ // do something } try { let body = JSON.stringify(stream.body.json); let upload_data = { path: '/test/index.json', // path relative to uploads dir ctype: ctype, // content type data: body, // data as string brotli: true, //override default brotli setting config.uploads.brotli ~ optional gzip: false //override default gzip setting config.uploads.gzip ~ optional //deflate: false //override default deflate setting config.uploads.deflate ~ optional } stream.upload(upload_data, function(err,res){ if(err){ // do something return; } stream.json({upload: 'success'}) }) } catch (err) { // do something } }); ``` #### stream.json(data) stream.json() performs the following actions: * add content-type 'application/json' to the headers; * stream the headers object. * send stringified json ```js /** * stream.json(obj, contentType, callback) * @param {array/object} obj // data to be stringified * @param {function} callback ~ optional **/ router.get('/', function(stream, headers, flags){ stream.json({send: 'json'}) }); ``` #### stream.redirect(dest) stream.redirect() performs the following actions: * add location destination to the headers; * stream the headers object. * send redirect ```js /** * stream.redirect(path) * @param {string} path // redirect anywhere path **/ router.get('/', function(stream, headers, flags){ //redirect to url stream.redirect('/test') }); ``` #### stream.ip stream.ip returns the client ip address * enable config.proxy to return ['x-forwarded-for'] ip ```js router.get('/', function(stream, headers, flags){ console.log(stream.ip) // xxx.xxx.x.x.x }); ``` #### stream.headers stream.headers will return an object containing all current and default outbound headers; this is not to be mistaken with the received `headers` object; ```js router.get('/', function(stream, headers, flags){ //log default received headers to console console.log(headers.all()) //log default outbound headers to console console.log(stream.headers) // add outbound header stream.addHeader('Content-Type', 'text/plain'); stream.respond(stream.headers); stream.end('headers sent') }) ``` #### stream.addHeader() stream.addHeader(key,val) will add a header to `stream.headers` ```js /** * stream.addHeader(key, val) * @param {string} key // header type * @param {string} val // header value **/ router.get('/', function(stream, headers, flags){ // add outbound header stream.addHeader('Content-Type','text/plain'); //stream.headers['Content-Type'] = 'text/plain'; stream.respond(stream.headers); stream.end('headers sent') }) ``` #### stream.addHeaders() stream.addHeaders(obj) will assign an object of headers to `stream.headers` ```js /** * stream.addHeaders(key, val) * @param {object} obj // headers object **/ router.get('/', function(stream, headers, flags){ // add outbound header stream.addHeaders({ 'content-type':'text/plain', 'content-encoding': 'gzip' }); stream.respond(stream.headers); stream.end('headers sent') }) ``` #### stream.cors() stream.cors() will add the included cors options to `stream.headers` * this method will override any default cors headers in `config.render.headers` || `config.static.headers` * this method will fallback to `config.cors` if no object is provided ```js /** * stream.cors(obj) * @param {object} obj // optional | cors entries || fallback to config.cors **/ router.get('/', function(stream, headers, flags){ // add all outbound cors headers stream.cors({ origin: '', // string | Access-Control-Allow-Origin methods: '', // string | Access-Control-Allow-Methods allow_headers: '', // string | Access-Control-Allow-Headers expose_headers: '',// string | Access-Control-Expose-Headers credentials: true, // boolean | Access-Control-Allow-Credentials maxage: 9999 // number | Access-Control-Max-Age }); // or only some stream.cors({ origin: '', methods: '', allow_headers: '' }); // or use config.cors stream.cors(); stream.respond(stream.headers) stream.end('text') }) ``` #### stream.ctype() stream.ctype(val) will add the Content-Type header to `stream.headers` ```js /** * stream.ctype(val) * @param {string} val // header value **/ router.get('/', function(stream, headers, flags){ // add outbound Content-Type header stream.ctype('text/plain'); stream.respond(stream.headers) stream.end('text') }) ``` #### stream.lang() stream.lang(val) will add the Content-Language header to `stream.headers` ```js /** * stream.lang(val) * @param {string} val // header value **/ router.get('/', function(stream, headers, flags){ // add outbound Content-Language header stream.lang('en-US'); stream.respond(stream.headers) stream.end('text') }) ``` #### stream.tk() stream.tk(val) will add the TK header to `stream.headers` ```js /** * stream.tk(val) * @param {string} val // header value **/ router.get('/', function(stream, headers, flags){ /* Tk: ! (under construction) Tk: ? (dynamic) Tk: G (gateway or multiple parties) Tk: N (not tracking) Tk: T (tracking) Tk: C (tracking with consent) Tk: P (potential consent) Tk: D (disregarding DNT) Tk: U (updated) */ // add outbound TK header stream.tk('N'); stream.respond(stream.headers) stream.end('text') }) ``` #### stream.csp() stream.csp(val) will add the Content-Security-Policy header to `stream.headers` * stream.csp will load the csp from `config.csp`; * this method enables you to store and use multiple pre-defined content security policies ```js /** * stream.csp(val) * @param {string} val // header value **/ router.get('/', function(stream, headers, flags){ // add outbound Content-Security-Policy header from `config.csp.default` stream.csp('default'); stream.respond(stream.headers) stream.end('text') }) ``` #### stream.feature() stream.feature(val) will add the Feature-Policy header to `stream.headers` * stream.feature will load the Feature-Policy from `config.feature_policy`; * this method enables you to store and use multiple pre-defined feature policies ```js /** * stream.feature(val) * @param {string} val // header value **/ router.get('/', function(stream, headers, flags){ // add outbound Feature-Policy header from `config.feature_policy.default` stream.feature('default'); stream.respond(stream.headers) stream.end('text') }) ``` #### stream.status() stream.status(val) will set the :status header to `stream.headers` ```js /** * stream.status(val) * @param {number} val // header value **/ router.get('/', function(stream, headers, flags){ // add outbound :status header stream.status(200).ctype('text/plain').respond(stream.headers); stream.end('text') }) ``` #### stream.query stream.query is part of `body parser`. if enabled, it will parse the given query to json. refer to body parses section. * `config.stream.method_query` controls the accepted router methods. * `config.stream.content_types` controls the accepted content types. ```js router.get('/test', function(stream, headers, flags){ let query = stream.query; console.log(query) }); ``` #### stream.qs stream.qs is similar to `stream.query` but returns the unparsed querystring. This method is intended for use with custom or complex querystrings; * `config.stream.querystring` enable/disable * the returned querystring is decoded with decodeURIComponent() ```js router.get('/test', function(stream, headers, flags){ let customquery = stream.qs; console.log(customquery) }); ``` #### stream.body.text stream.body.text is the default body parse format * returns `string` ```js router.post('/', function(stream, headers, flags){ let body = stream.body.text; console.log(body) }); ``` #### stream.body.buffer stream.body.buffer is part of `body parser`. * returns `buffer` ```js router.post('/', function(stream, headers, flags){ let buff = stream.body.buffer; console.log(buff) }); ``` #### stream.body.json stream.body.buffer is part of `body parser`. refer to body parses section. * returns `json` for supported content-types ```js router.post('/', function(stream, headers, flags){ let obj = stream.body.json; console.log(obj) }); ``` #### stream.cookies this method is a part of cookie parser refer to cookie parser stream.cookie will enable you to easily access all cookies in `headers` * this method automatically deserializes all cookies. * this method requires `config.cookie_parser.enabled` to be enabled * this method can be enabled/disabled at `config.cookie_parser.auto_parse` ```js router.get('/', function(stream, headers, flags){ // return cookies object ~ config.cookie_parser.auto_parse console.log(stream.cookies) }) ``` #### stream.cookie() this method is a part of cookie parser refer to cookie parser stream.cookie(name,val,obj) will enable you to easily add cookies to the `stream.response` * this method automatically adds the created cookie to `stream.headers` * this method can be enabled/disabled at `config.cookie_parser.enabled` * this method can create a separate signed cookie for tamper detection * `config.cookie_parser.sig.secret` is used to hmac the cookie * `config.cookie_parser.sig.suffix` is the signed cookies suffix * a signed cookie will be will use digest/encode settings from `config.crypt.hmac` ```js /** * stream.cookie(key, val, settings) * @param {string} key // cookie name * @param {string} val // cookie value * @param {object} settings // cookie settings **/ router.get('/', function(stream, headers, flags){ //create cookie and add to outbouheaders ~ config.cookie_parser.enabled stream.cookie('name', 'value',{ Domain: 'localhost', Path: '/', Expires: Date.now(), MaxAge: 9999, HttpOnly: true, SameSite: 'Lax', Secure: true, Priority: 'High', Signed: true // creates a separate suffixed signed cookie for validation }) stream.respond(stream.headers); stream.end() }) ``` # push handler - [Back to index](#documentation) the push handler will enable/disable automatic stream push of static files. upon stream, the server will search the accepted header for a match in `config.push_handler.accepted` and will push your selected files with the document * `config.push_handler.enabled` enables this method * this method is for static `files` only e.g js/css/png/jpg * this method is not for rendered/static `documents` e.g html/xhtml/xml * this method is for `GET` requests only. * `config.push_handler.accepted` should contain the `requested paths` content-type e.g text/html * `config.push_handler.accepted` should not contain the pushed items content-type e.g text/css * `config.push_handler.accepted` should only contain document content-types that you use * `config.push_handler.accepted` should be as small as possible * automatic stream push of static files is recommended only for push intensive sites the push configuration file can be configured like so: ```js /* ./config/push.json */ [{ "url": "/single_push", // the url path that the file is to be pushed for "ctype": "text/css", // file content-type 'only' "path": "/css/main.css" // file path relative to static path },{ "url": "/multi_push", "items": [{ // push multiple items at same url "ctype": "text/css", "path": "/css/main.css" },{ "ctype": "image/x-icon", "path": "/favicon.ico" }] }] ``` ```js router.get('/single_push', function(stream, headers, flags){ // will automatically push a static file and send headers/doc stream.status(200).doc('index.html', 'text/html') }); router.get('/multi_push', function(stream, headers, flags){ // will automatically push multiple static files and send headers/doc stream.status(200).doc('index.html', 'text/html') }); router.get('/manual_push', function(stream, headers, flags){ // will not automatically push multiple static files stream // manually push multiple static files and send headers/doc .pushStatic([{ path: '/css/main.css', // file path ctype: 'text/css' // file content type },{ path: '/favicon.ico', ctype: 'image/x-icon' }]) .status(200) .doc('index.html', 'text/html') }); ``` # Headers - [Back to index](#documentation) the headers object includes the following methods: #### headers.all() headers.all() will return a valid json object containing all received headers ```js router.get('/', function(stream, headers, flags){ // log all received headers console.log(headers.all()) }) ``` #### headers.get() headers.get() will return a header from the headers object in nodejs http2 format ```js /** * headers.get(key) * @param {string} key // header name **/ router.get('/', function(stream, headers, flags){ // return content-type header console.log(headers.get('content-type')) }) ``` #### headers.is() headers.is() will return a boolean if the header is equal to the comparison ```js /** * headers.is(key, val) * @param {string} key // header name * @param {string} val // value to compare **/ router.get('/admin', function(stream, headers, flags){ // check content-type if(!headers.is('x-token', 'secret')){ app.blacklist(stream.ip) } }) ``` #### headers.has() headers.has() will return a boolean if the header exists * will also return true for header that exists and has a value of false or 0 ```js /** * headers.has(key) * @param {string} key // header name **/ router.get('/', function(stream, headers, flags){ // check if cookie header exists if(headers.has('cookie')){ console.log('cookie header exists') } }) ``` #### headers.cookies() headers.cookies() will return a deserialized cookies json object ```js router.get('/', function(stream, headers, flags){ // return cookies object console.log(headers.cookies()) }) ``` #### headers.ctype() headers.ctype() will return the Content-type header if exists ```js router.get('/', function(stream, headers, flags){ console.log(headers.ctype()) // application/json ... }) ``` #### headers.agent() headers.agent() will return the User-agent header if exists ```js router.get('/', function(stream, headers, flags){ console.log(headers.agent()) // some browser user-agent ... }) ``` #### headers.bot() headers.bot() will return true if the user-agent detected is a bot * `config.bot.items` should contain an array of bots to check for * this feature could be used to perform targeted seo optimization ```js router.get('/', function(stream, headers, flags){ if(headers.bot()){ // render template containing seo data only let useragent = headers.ua(); if(useragent.includes('google')){ stream.render('index_seo_google.html', { data: { some: 'google data', specifically: 'relating', to: 'google seo' } }) } else if(useragent.includes('facebook')) { stream.render('index_seo_facebook.html', { data: { some: 'facebook data', specifically: 'relating', to: 'facebook seo' } }) } else { stream.render('index_seo_default.html', { data: { some: 'default data', specifically: 'relating', to: 'default seo' } }) } } else { // render normal template not polluted with seo stream.render('index.html', {title: 'basic'}) } }) ``` #### headers.encoding() headers.encoding() will return the accept-encoding header if exists * the returned value/s will be within a trimmed array ```js router.get('/', function(stream, headers, flags){ console.log(headers.encoding()) // ['accepted', 'encoding'] }) ``` #### headers.lang() headers.lang() will return the accept-language header if exists * the returned value/s will be within a trimmed array ```js router.get('/', function(stream, headers, flags){ console.log(headers.lang()) // ['accepted', 'language'] }) ``` #### headers.accept() headers.accept() will return the accept header if exists * the returned value/s will be within a trimmed array ```js router.get('/', function(stream, headers, flags){ console.log(headers.accept()) // ['accepted', 'content', 'types'] }) ``` #### headers.size() headers.size() length of the headers object ```js router.get('/', function(stream, headers, flags){ let len = headers.size(); // headers length if(len > 1000){