diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..464ec01 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# ![Icon](media/kirb.gif "Dancing krib") Kirbe + +> A lightweight and fast Node.js HTTP server library + +> [GitHub](https://www.github.com/PassTheWessel/Kirbe) **|** [NPM](https://www.npmjs.com/package/kirbe) + +## Installing +```sh +$ yarn add kirbe # Install w/ Yarn (the superior package manager) +$ npm i kirbe # Install w/ NPM +``` + +## Usage +#### Start a HTTP(s) server on port 8080 and add some routes +```js +const kirbe = require( 'kirbe' ); // Define kirbe +const app = new kirbe(); // Make your kirbe client + +app.route( '/bear', 'GET' ( req, res ) => res.status( 200 ).body({ 'bear': 'cop' }) ); +app.route( ( req, res ) => res.status( 404 ).body( 'Error: Content not found!' ).end() ); +app.get( '/kirb', ( req, res ) => { + res.writeHead( 201, { 'test': 'hi' }); + res.end({ 'key': 'hi' }); +}); + +// HTTP +app.listen( 8080, () => console.log( 'Listening on port 8080!' ) ); +// HTTPS +const https = require( 'https' ); // This should be at the top of your code +https.createServer( app.externalHandler ).listen( 8080 ); +``` + +## Default extensions ( [/extensions](extensions) ) +#### Static +> Host static files on your website + +##### Usage +```js +const path = require( 'path' ); // Define path +const kirbe = require( 'kirbe' ); // Define kirbe +const app = new kirbe(); // Make your kirbe client + +app.use( kirbe.static( path.join( __dirname, 'static' ) ) ); +``` + +### Why use kirbe? +Kirbe is a lightweight and fast HTTP server library, especially comparing to express which is around 1mb. If you want any featuers that aren't inside of Kirbe yet, you can open an issue or pull request. + +You can join [https://discord.gg/SV7DAE9](https://discord.gg/SV7DAE9) if you need any support using kirbe! \ No newline at end of file diff --git a/extensions/mimes.json b/extensions/mimes.json new file mode 100644 index 0000000..db30936 --- /dev/null +++ b/extensions/mimes.json @@ -0,0 +1 @@ +{".ez":"application/andrew-inset",".aw":"application/applixware",".atom":"application/atom+xml",".atomcat":"application/atomcat+xml",".atomsvc":"application/atomsvc+xml",".bdoc":"application/bdoc",".ccxml":"application/ccxml+xml",".cdmia":"application/cdmi-capability",".cdmic":"application/cdmi-container",".cdmid":"application/cdmi-domain",".cdmio":"application/cdmi-object",".cdmiq":"application/cdmi-queue",".cu":"application/cu-seeme",".mpd":"application/dash+xml",".davmount":"application/davmount+xml",".dbk":"application/docbook+xml",".dssc":"application/dssc+der",".xdssc":"application/dssc+xml",".ecma":"application/ecmascript",".emma":"application/emma+xml",".epub":"application/epub+zip",".exi":"application/exi",".pfr":"application/font-tdpfr",".woff":"application/font-woff",".woff2":"application/font-woff2",".geojson":"application/geo+json",".gml":"application/gml+xml",".gpx":"application/gpx+xml",".gxf":"application/gxf",".gz":"application/gzip",".stk":"application/hyperstudio",".ink":"application/inkml+xml",".inkml":"application/inkml+xml",".ipfix":"application/ipfix",".jar":"application/java-archive",".war":"application/java-archive",".ear":"application/java-archive",".ser":"application/java-serialized-object",".class":"application/java-vm",".js":"application/javascript",".mjs":"application/javascript",".json":"application/json",".map":"application/json",".json5":"application/json5",".jsonml":"application/jsonml+json",".jsonld":"application/ld+json",".lostxml":"application/lost+xml",".hqx":"application/mac-binhex40",".cpt":"application/mac-compactpro",".mads":"application/mads+xml",".webmanifest":"application/manifest+json",".mrc":"application/marc",".mrcx":"application/marcxml+xml",".ma":"application/mathematica",".nb":"application/mathematica",".mb":"application/mathematica",".mathml":"application/mathml+xml",".mbox":"application/mbox",".mscml":"application/mediaservercontrol+xml",".metalink":"application/metalink+xml",".meta4":"application/metalink4+xml",".mets":"application/mets+xml",".mods":"application/mods+xml",".m21":"application/mp21",".mp21":"application/mp21",".mp4s":"application/mp4",".m4p":"application/mp4",".doc":"application/msword",".dot":"application/msword",".mxf":"application/mxf",".bin":"application/octet-stream",".dms":"application/octet-stream",".lrf":"application/octet-stream",".mar":"application/octet-stream",".so":"application/octet-stream",".dist":"application/octet-stream",".distz":"application/octet-stream",".pkg":"application/octet-stream",".bpk":"application/octet-stream",".dump":"application/octet-stream",".elc":"application/octet-stream",".deploy":"application/octet-stream",".exe":"application/octet-stream",".dll":"application/octet-stream",".deb":"application/octet-stream",".dmg":"application/octet-stream",".iso":"application/octet-stream",".img":"application/octet-stream",".msi":"application/octet-stream",".msp":"application/octet-stream",".msm":"application/octet-stream",".buffer":"application/octet-stream",".oda":"application/oda",".opf":"application/oebps-package+xml",".ogx":"application/ogg",".omdoc":"application/omdoc+xml",".onetoc":"application/onenote",".onetoc2":"application/onenote",".onetmp":"application/onenote",".onepkg":"application/onenote",".oxps":"application/oxps",".xer":"application/patch-ops-error+xml",".pdf":"application/pdf",".pgp":"application/pgp-encrypted",".asc":"application/pgp-signature",".sig":"application/pgp-signature",".prf":"application/pics-rules",".p10":"application/pkcs10",".p7m":"application/pkcs7-mime",".p7c":"application/pkcs7-mime",".p7s":"application/pkcs7-signature",".p8":"application/pkcs8",".ac":"application/pkix-attr-cert",".cer":"application/pkix-cert",".crl":"application/pkix-crl",".pkipath":"application/pkix-pkipath",".pki":"application/pkixcmp",".pls":"application/pls+xml",".ai":"application/postscript",".eps":"application/postscript",".ps":"application/postscript",".pskcxml":"application/pskc+xml",".rdf":"application/rdf+xml",".rif":"application/reginfo+xml",".rnc":"application/relax-ng-compact-syntax",".rl":"application/resource-lists+xml",".rld":"application/resource-lists-diff+xml",".rs":"application/rls-services+xml",".gbr":"application/rpki-ghostbusters",".mft":"application/rpki-manifest",".roa":"application/rpki-roa",".rsd":"application/rsd+xml",".rss":"application/rss+xml",".rtf":"application/rtf",".sbml":"application/sbml+xml",".scq":"application/scvp-cv-request",".scs":"application/scvp-cv-response",".spq":"application/scvp-vp-request",".spp":"application/scvp-vp-response",".sdp":"application/sdp",".setpay":"application/set-payment-initiation",".setreg":"application/set-registration-initiation",".shf":"application/shf+xml",".smi":"application/smil+xml",".smil":"application/smil+xml",".rq":"application/sparql-query",".srx":"application/sparql-results+xml",".gram":"application/srgs",".grxml":"application/srgs+xml",".sru":"application/sru+xml",".ssdl":"application/ssdl+xml",".ssml":"application/ssml+xml",".tei":"application/tei+xml",".teicorpus":"application/tei+xml",".tfi":"application/thraud+xml",".tsd":"application/timestamped-data",".vxml":"application/voicexml+xml",".wgt":"application/widget",".hlp":"application/winhlp",".wsdl":"application/wsdl+xml",".wspolicy":"application/wspolicy+xml",".xaml":"application/xaml+xml",".xdf":"application/xcap-diff+xml",".xenc":"application/xenc+xml",".xhtml":"application/xhtml+xml",".xht":"application/xhtml+xml",".xml":"application/xml",".xsl":"application/xml",".xsd":"application/xml",".rng":"application/xml",".dtd":"application/xml-dtd",".xop":"application/xop+xml",".xpl":"application/xproc+xml",".xslt":"application/xslt+xml",".xspf":"application/xspf+xml",".mxml":"application/xv+xml",".xhvml":"application/xv+xml",".xvml":"application/xv+xml",".xvm":"application/xv+xml",".yang":"application/yang",".yin":"application/yin+xml",".zip":"application/zip",".adp":"audio/adpcm",".au":"audio/basic",".snd":"audio/basic",".mid":"audio/midi",".midi":"audio/midi",".kar":"audio/midi",".rmi":"audio/midi",".m4a":"audio/mp4",".mp4a":"audio/mp4",".mpga":"audio/mpeg",".mp2":"audio/mpeg",".mp2a":"audio/mpeg",".mp3":"audio/mpeg",".m2a":"audio/mpeg",".m3a":"audio/mpeg",".oga":"audio/ogg",".ogg":"audio/ogg",".spx":"audio/ogg",".s3m":"audio/s3m",".sil":"audio/silk",".wav":"audio/wav",".weba":"audio/webm",".xm":"audio/xm",".otf":"font/otf",".apng":"image/apng",".bmp":"image/bmp",".cgm":"image/cgm",".g3":"image/g3fax",".gif":"image/gif",".ief":"image/ief",".jpeg":"image/jpeg",".jpg":"image/jpeg",".jpe":"image/jpeg",".ktx":"image/ktx",".png":"image/png",".sgi":"image/sgi",".svg":"image/svg+xml",".svgz":"image/svg+xml",".tiff":"image/tiff",".tif":"image/tiff",".webp":"image/webp",".eml":"message/rfc822",".mime":"message/rfc822",".gltf":"model/gltf+json",".glb":"model/gltf-binary",".igs":"model/iges",".iges":"model/iges",".msh":"model/mesh",".mesh":"model/mesh",".silo":"model/mesh",".wrl":"model/vrml",".vrml":"model/vrml",".x3db":"model/x3d+binary",".x3dbz":"model/x3d+binary",".x3dv":"model/x3d+vrml",".x3dvz":"model/x3d+vrml",".x3d":"model/x3d+xml",".x3dz":"model/x3d+xml",".appcache":"text/cache-manifest",".manifest":"text/cache-manifest",".ics":"text/calendar",".ifb":"text/calendar",".coffee":"text/coffeescript",".litcoffee":"text/coffeescript",".css":"text/css",".csv":"text/csv",".hjson":"text/hjson",".html":"text/html",".htm":"text/html",".shtml":"text/html",".jade":"text/jade",".jsx":"text/jsx",".less":"text/less",".markdown":"text/markdown",".md":"text/markdown",".mml":"text/mathml",".n3":"text/n3",".txt":"text/plain",".text":"text/plain",".conf":"text/plain",".def":"text/plain",".list":"text/plain",".log":"text/plain",".in":"text/plain",".ini":"text/plain",".rtx":"text/richtext",".sgml":"text/sgml",".sgm":"text/sgml",".slim":"text/slim",".slm":"text/slim",".stylus":"text/stylus",".styl":"text/stylus",".tsv":"text/tab-separated-values",".t":"text/troff",".tr":"text/troff",".roff":"text/troff",".man":"text/troff",".me":"text/troff",".ms":"text/troff",".ttl":"text/turtle",".uri":"text/uri-list",".uris":"text/uri-list",".urls":"text/uri-list",".vcard":"text/vcard",".vtt":"text/vtt",".yaml":"text/yaml",".yml":"text/yaml",".3gp":"video/3gpp",".3gpp":"video/3gpp",".3g2":"video/3gpp2",".h261":"video/h261",".h263":"video/h263",".h264":"video/h264",".jpgv":"video/jpeg",".jpm":"video/jpm",".jpgm":"video/jpm",".mj2":"video/mj2",".mjp2":"video/mj2",".ts":"video/mp2t",".mp4":"video/mp4",".mp4v":"video/mp4",".mpg4":"video/mp4",".mpeg":"video/mpeg",".mpg":"video/mpeg",".mpe":"video/mpeg",".m1v":"video/mpeg",".m2v":"video/mpeg",".ogv":"video/ogg",".qt":"video/quicktime",".mov":"video/quicktime",".webm":"video/webm"} \ No newline at end of file diff --git a/extensions/static.js b/extensions/static.js new file mode 100644 index 0000000..befe2c6 --- /dev/null +++ b/extensions/static.js @@ -0,0 +1,47 @@ +const fs = require( 'fs' ); +const url = require( 'url' ); +const { join, extname } = require( 'path' ); + +const mimes = JSON.parse( fs.readFileSync( join( __dirname, 'mimes.json' ) ) ); + +module.exports = ( baseDir, indexFile ) => { + if (!baseDir) throw new Error('The argument "baseDir" is required for the extension "kirbe:static"'); + + indexFile = typeof indexFile === 'string' ? indexFile : 'index.html' + return( req, res, next ) => { + if( req.method !== 'GET' ) { next(); return; } + + let requestedPath = req.parsedUrl.pathname.replace( /\/.\.\//g, '' ); + let requestedExt = extname( requestedPath ); + + const filePath = join( baseDir, requestedPath ); + + fs.stat( filePath, ( err, stats ) => { + if( err ) { next(); return; } + + if( stats.isFile() ) { + stats.mtime.setMilliseconds( 0 ); + if ( stats.mtime <= new Date( req.headers[ 'if-modified-since' ] ) ) res.status( 304 ).end(); + else fs.createReadStream( filePath ).pipe( res.status( 200 ).coreRes ); + } else { + if ( req.parsedUrl.pathname.charAt( req.parsedUrl.pathname.length -1 ) !== '/' ) { + res.status(302).header({ 'Location': `${req.parsedUrl.pathname}/` }).end(); + return; + } + + requestedPath = join( filePath, indexFile ); + requestedExt = extname( requestedPath ); + + fs.readFile( requestedPath, ( err, data ) => { + if (err) next(); + else { + res.body( data ).status(200).header({ + 'Content-Type': ( mimes.hasOwnProperty( requestedExt ) ? mimes[ requestedExt ] : 'application/octet-stream' ), + 'Last-Modified': stats.mtime.toString() + }).end(); + } + }); + } + }); + }; +}; \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..dda2926 --- /dev/null +++ b/index.js @@ -0,0 +1,4 @@ +const kirbe = exports; + +module.exports = require( './model/KirbeServer' ); +kirbe.static = require( './extensions/static' ); \ No newline at end of file diff --git a/media/kirb.gif b/media/kirb.gif new file mode 100644 index 0000000..18b2a2a Binary files /dev/null and b/media/kirb.gif differ diff --git a/model/KirbeRequest.js b/model/KirbeRequest.js new file mode 100644 index 0000000..2696de8 --- /dev/null +++ b/model/KirbeRequest.js @@ -0,0 +1,16 @@ +const { parse } = require( 'url' ); + +module.exports = class KirbeRequest { + constructor( req, body ) { + this.url = req.url; + this.req = req; + this.body = body; + this.from = req.connection.remoteAddress; + this.method = req.method; + this.headers = req.headers; + this.parsedUrl = parse( this.url, true ); + } + + json() { return JSON.parse( this.body ); } + query ( name ) { return this.parsedUrl.query[ name ]; } +}; \ No newline at end of file diff --git a/model/KirbeResponse.js b/model/KirbeResponse.js new file mode 100644 index 0000000..d14b4f6 --- /dev/null +++ b/model/KirbeResponse.js @@ -0,0 +1,50 @@ +module.exports = class KirbeResponse { + constructor( res ) { + this.coreRes = res; + this.headers = {}; + + this.statusCode = 200; + this.statusMessage = null; + + this.data = Buffer.alloc( 0 ); + } + + body( body ) { + if ( typeof body === 'object' && !Buffer.isBuffer( body ) ) { + if ( !this.headers[ 'content-type' ] ) this.headers[ 'content-type' ] = 'application/json'; + + this.data = JSON.stringify( body ); + } else this.data = body; + + return this; + } + + header( a, b ) { + if ( typeof a === 'object' ) Object.keys( a ).forEach( ( v ) => this.headers[ v.toLowerCase() ] = a[ v ] ); + else this.headers[ a.toLowerCase() ] = b; + + return this; + } + + status( code, message ) { + this.statusCode = code; + this.statusMessage = message; + + return this; + } + + writeHead( status, headers ) { + if ( status ) this.status( status ); + if ( headers ) this.header( headers ); + } + + end( data ) { + if ( data ) this.body( data ); + + if (this.statusMessage) this.coreRes.writeHead( this.statusCode, this.statusMessage, this.headers ); + else this.coreRes.writeHead( this.statusCode, this.headers ); + this.coreRes.end( this.data ); + + return this; + } +}; \ No newline at end of file diff --git a/model/KirbeServer.js b/model/KirbeServer.js new file mode 100644 index 0000000..d143ae8 --- /dev/null +++ b/model/KirbeServer.js @@ -0,0 +1,71 @@ +const { join } = require( 'path' ); +const { createServer } = require( 'http' ); + +const KirbeRequest = require( join( __dirname, 'KirbeRequest.js' ) ); +const KirbeResponse = require( join( __dirname, 'KirbeResponse.js' ) ); + +const methods = [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH' ]; +const isUrl = ( c ) => (typeof c === 'string' && !methods.includes( c ) ) || c instanceof RegExp; + +module.exports = class KirbeServer { + constructor() { + this.internalServer = createServer( ( req, res ) => this.handler.apply( this, [ req, res ] ) ); + this.externalHandler = ( req, res ) => this.handler( req, res ); + + this.routes = []; + this.extensions = []; + + methods.forEach( ( v ) => this[ v.toLowerCase() ] = ( a, b ) => this.route( v, a, b ) ); + } + + use( extension ) { this.extensions.push( extension ); } + + route( a, b, c ) { + this.routes.push({ + 'target': { + 'path' : isUrl( a ) ? a : ( isUrl( b ) ? b : null ), + 'method': methods.includes( a ) ? a : null + }, + 'handler': c || b || a + }); + } + + handler( req, res ) { + let body = Buffer.alloc( 0 ); + + req.on( 'data', ( c ) => body = Buffer.concat([ body, c ]) ); + req.on( 'end', () => { + const request = new KirbeRequest( req, body ); + const response = new KirbeResponse( res ); + + const start = () => { + for( let i = 0; i < this.routes.length; i++ ) { + const current = this.routes[ i ]; + + if ( current.target.method && request.method !== current.target.method ) continue; + if ( current.target.path ) { + if ( current.target.path instanceof RegExp && !request.parsedUrl.pathname.match( current.target.path ) ) continue; + else if ( current.target.path !== request.parsedUrl.pathname ) continue; + } + + current.handler( request, response ); + break; + } + }; + + let currentExt = 0; + const nextExt = () => { + if ( this.extensions.length >= currentExt + 1 ) { + currentExt++; + this.extensions[ currentExt -1 ]( request, response, nextExt ); + } else start(); + }; + + nextExt(); + }); + } + + listen( a, b, c ) { + this.internalServer.listen( typeof a === 'number' ? a : 80, typeof b === 'string' ? b : null, typeof b === 'function' ? b : null ); + } +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..0615e60 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "kirbe", + "version": "0.0.1", + "description": "👻 A powerful and lightweight Node.js HTTP server library", + "main": "index.js", + "author": "Wessel \"wesselgame\" T ", + "license": "MIT", + "private": false, + "files": [ + "index.js", + "model", + "LICENSE" + ], + "bugs": { + "url": "https://www.github.com/PassTheWessel/kirbe/issues" + }, + "homepage": "https://www.github.com/PassTheWessel/kirbe#readme", + "repository": { + "type": "git", + "url": "https://www.github.com/PassTheWessel/kirbe" + }, + "keywords": [ + "framework", + "fast", + "web", + "rest", + "restful", + "router", + "app", + "api", + "lightweight" + ], + "devDependencies": { "wumpfetch": "^0.0.4" } +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..f1e7750 --- /dev/null +++ b/test.js @@ -0,0 +1,28 @@ +const w = require( 'wumpfetch' ); +const Kirbe = require( './index' ); + +const app = new Kirbe(); + +app.use( ( req, res, next ) => req.url === '/testExtension' ? res.status( 200 ).end() : next() ); + +app.post( '/parse', ( req, res ) => res.body({ 'sent': req.json() }).end() ); +app.route( 'GET', '/statusMsg', ( req, res ) => res.status( 200, 'kirbe won' ).end() ); +app.route( 'POST', ( req, res ) => res.body( 'Gotta catch em all!' ).end() ); +app.route( '/compatibility', ( req, res ) => { + res.writeHead( 201, { 'test': 'hi' }); + res.end({ 'key': 'hi' }); +}); +app.route( ( req, res ) => res.status( 404 ).body( 'Error: Content not found!' ).end() ); + +;( async() => { + const res = []; + res.push( await w( 'http://127.0.0.1:4040/bear' ).send() ); + res.push( await w( 'http://127.0.0.1:4040/statusMsg' ).send() ); + res.push( await w( 'http://127.0.0.1:4040/compatibility' ).send() ); + res.push( await w( 'http://127.0.0.1:4040/testExtension' ).send() ); + res.push( await w( 'http://127.0.0.1:4040/parse', 'POST' ).body({ 'hello': 123 }).send() ); + + res.forEach( ( v, i ) => console.log( `${i}: ${v.statusCode}` ) ); +})(); + +app.listen( 4040, '127.0.0.1' ); \ No newline at end of file