From 0f2ca2e16205479b318c4d2a71c5eb034ae6ca5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Fri, 12 Feb 2016 00:16:42 +0800 Subject: [PATCH] Submoduling BotanSS, Reg Draft --- .gitignore | 3 + .gitmodules | 3 + botanss | 1 + config/global.js | 1 + config/log.js | 6 + index.js | 68 ++++++++++ notify-term/Notis.js | 64 +++++++++ notify-term/WNSAuth.js | 208 +++++++++++++++++++++++++++++ notify-term/app.js | 93 +++++++++++++ notify-term/schema.js | 77 +++++++++++ notifysrv/eventargs/eventargs.js | 15 +++ notifysrv/eventargs/postrequest.js | 23 ++++ notifysrv/postframe.js | 91 +++++++++++++ notifysrv/utils/querystr.js | 14 ++ notifysrv/utils/random.js | 15 +++ notifysrv/utils/string.js | 13 ++ package.json | 20 +++ 17 files changed, 715 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 160000 botanss create mode 100644 config/global.js create mode 100644 config/log.js create mode 100644 index.js create mode 100644 notify-term/Notis.js create mode 100644 notify-term/WNSAuth.js create mode 100644 notify-term/app.js create mode 100644 notify-term/schema.js create mode 100644 notifysrv/eventargs/eventargs.js create mode 100644 notifysrv/eventargs/postrequest.js create mode 100644 notifysrv/postframe.js create mode 100644 notifysrv/utils/querystr.js create mode 100644 notifysrv/utils/random.js create mode 100644 notifysrv/utils/string.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93e69ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +auth.js +node_modules +*.swp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..002c6d1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "botanss"] + path = botanss + url = AstroGit:BotanSS.git diff --git a/botanss b/botanss new file mode 160000 index 0000000..a83d3d9 --- /dev/null +++ b/botanss @@ -0,0 +1 @@ +Subproject commit a83d3d9469844e6530d56382c97c64d3386ea965 diff --git a/config/global.js b/config/global.js new file mode 100644 index 0000000..f85d6dc --- /dev/null +++ b/config/global.js @@ -0,0 +1 @@ +global.debug = true; diff --git a/config/log.js b/config/log.js new file mode 100644 index 0000000..2e39811 --- /dev/null +++ b/config/log.js @@ -0,0 +1,6 @@ +var fs = require( "fs" ); +var path = "logs/access.log"; + +module.exports = { + handler: { write: function( e ) { process.stdout.write( e ); } } +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..0ac6bb5 --- /dev/null +++ b/index.js @@ -0,0 +1,68 @@ +require( "./botanss/package" ); +require( "./config/global" ); + +var cl = global.botanLoader; + +var Dragonfly = cl.load( "botanss.Dragonfly" ); +Dragonfly.defaultSphere = 999; + + +var cluster = require('cluster'); +var numCPUs = 2; + + +if( cluster.isMaster ) +{ + var clog = require( "./config/log" ); + var Masterfly = new Dragonfly( clog.handler ); + + var procFock = function( c ) + { + // fork and bind the message bus from masterfly + c.fork().addListener( "message", Masterfly.messageBus ); + }; + + var clusterDisconnect = function( worker ) + { + if( worker.suicide === true ) + { + Masterfly.Info( "Worker committed suicide" ); + Masterfly.Info( "Forking process ..." ); + procFock( cluster ); + } + else + { + Masterfly.Info( "Worker died" ); + } + }; + + for( var i = 0; i < numCPUs; i ++ ) procFock( cluster ); + + cluster.addListener( "disconnect", clusterDisconnect ); +} +else +{ + Dragonfly = new Dragonfly(); + global.Dragonfly = Dragonfly; + + GLOBAL.X_SERVER_CLUSTER = cluster; + + var AppDomain = cl.load( "botanss.net.AppDomain" ); + var Httph = cl.load( "botanss.net.Http" ); + + //* Host App + var WebFrame = cl.load( "botanss.net.WebFrame" ); + + // Define AppNS + cl.rootNS( "notifyterm", "./notify-term" ); + cl.rootNS( "notifysrv", "./notifysrv" ); + + var App = cl.load( "notifyterm.app" ); + + new AppDomain( function( req, res ) + { + var h = new Httph( req, res ); + new App( h ).run(); + }, 5000 ); + //*/ +} diff --git a/notify-term/Notis.js b/notify-term/Notis.js new file mode 100644 index 0000000..1e1d497 --- /dev/null +++ b/notify-term/Notis.js @@ -0,0 +1,64 @@ +"use strict"; + +var cl = global.botanLoader; +var Dragonfly = global.Dragonfly; + +var HttpRequest = cl.load( "botanss.net.HttpRequest" ); +var ustr = cl.load( "notifysrv.utils.string" ); + +class Notis +{ + constructor( query ) + { + this.__valid = false; + this.__error = null; + + try + { + if( !query.id ) + { + throw new Error( "ID is not specified" ); + } + + this.id = query.id; + + if( query.xml ) + { + this.Xml = query.xml; + } + else + { + this.Xml = Notis.ToastText02 + .replace( "{TITLE}", ustr.encodeHtml( query.title ) ) + .replace( "{MESG}", ustr.encodeHtml( query.message ) ); + } + + this.__valid = true; + } + catch( ex ) + { + Dragonfly.Error( ex ); + this.__error = ex.message; + } + } + + get Valid() { return this.__valid; } + get Error() { return this.__error; } + + static get ToastText02() + { + return ` + + + + {TITLE} + {MESG} + + + +`; + } + +} + +module.exports = Notis; diff --git a/notify-term/WNSAuth.js b/notify-term/WNSAuth.js new file mode 100644 index 0000000..e72f0ca --- /dev/null +++ b/notify-term/WNSAuth.js @@ -0,0 +1,208 @@ +"use strict"; + +var cl = global.botanLoader; +var Dragonfly = global.Dragonfly; + +var EventEmitter = require( "events" ).EventEmitter; + +var HttpRequest = cl.load( "botanss.net.HttpRequest" ); +var Rand = cl.load( "notifysrv.utils.random" ); +var Notis = cl.load( "notifyterm.Notis" ); +var Model = cl.load( "notifyterm.schema" ); + +// private static var +var AuthTokenName = "WNSAuthToken"; +var AuthToken = false; + +class WNSAuth extends EventEmitter +{ + constructor() + { + super(); + this.__inAuth = false; + } + + get IsAuthenticated() { return Boolean( AuthToken ); } + + Authenticate() + { + if( this.IsAuthenticated ) + { + this.__emitAuthComplete(); + return; + } + + if( this.__inAuth ) return; + + this.__inAuth = true; + var _self = this; + + Model.Tokens.findOne({ name: AuthTokenName }) + .exec( ( err, data ) => { + if( err || !( data && data.token ) ) + { + Dragonfly.Info( "Database does not contain access token, authenticating" ); + _self.__authWNS(); + } + else + { + Dragonfly.Info( "Access token found in database, using it" ); + AuthToken = data.token; + _self.__emitAuthComplete(); + } + } ); + } + + Register( ChannelUri, handler ) + { + var _self = this; + var VerifyChannel = () => + { + var N = new Notis({ + id: "Null" + , title: "Channel Registration" + , message: "Registration success" + }); + + var uuid = Rand.uuid(); + + _self.__send( ChannelUri, N, ( sender, e ) => { + if( e.statusCode == 200 ) + { + Model.Tokens.update( + { name: uuid } + , { name: uuid, token: ChannelUri } + , { upsert: true } + ) + .exec( ( err, data ) => { + if( err ) + { + Dragonfly.Error( err ); + handler( _self, "Server Error: Cannot save channel information" ); + return; + } + handler( _self, uuid ); + } ); + return; + } + + handler( _self, e.statusCode + " Server Error: Unable to push message to channel" ); + + } ); + }; + + if( !this.Authenticated ) + { + this.once( "AuthComplete", VerifyChannel ); + this.Authenticate(); + } + else + { + VerifyChannel(); + } + } + + Deliver( NotisQ ) + { + Model.Tokens + .findOne({ name: NotisQ.id }) + .exec( ( err, data ) => { + if( err ) + { + Dragonfly.Error( err ); + return; + } + + if( data && data.token ) + { + this.__send( data.token, NotisQ, ( sender, e ) => { + Dragonfly.Debug( e.Data ); + } ); + } + else + { + Dragonfly.Info( "Channel not found: " + NotisQ.id ); + } + } ); + } + + __send( ChannelUri, NotisQ, handler ) + { + if( !ChannelUri ) + { + handler( this, "Channel is undefined" ); + return; + } + + var Request = new HttpRequest( ChannelUri, { + "Authorization": "Bearer " + AuthToken + , "X-WNS-RequestForStatus": "true" + , "X-WNS-Type": "wns/toast" + } ); + + if( !Request.Hostname.match( /.*\.notify\.windows\.com$/ ) ) + { + handler( this, "Malicious hostname: " + Request.Hostname ); + return; + } + + Request.PostData( NotisQ.Xml ); + Request.Headers[ "Content-Type" ] = "text/xml"; + + Request.addListener( "RequestComplete", handler ); + + Request.Send(); + } + + __authWNS() + { + var serviceAuth = cl.load( "notifyterm.config.auth" ); + + var Request = new HttpRequest( serviceAuth.Uri ); + + Request.PostData( + "grant_type=client_credentials" + + "&client_id=" + serviceAuth.Id + + "&client_secret=" + encodeURIComponent( serviceAuth.Secret ) + + "&scope=notify.windows.com" + ); + + Request.addListener( "RequestComplete", this.__requestComplete.bind( this ) ); + + Request.Send(); + } + + __requestComplete( sender, e ) + { + var _self = this; + let JResponse = JSON.parse( e.ResponseString ); + + if( JResponse && JResponse.access_token ) + { + AuthToken = JResponse.access_token; + Dragonfly.Info( "Authorization Success" ); + + Model.Tokens + .update( + { name: AuthTokenName } + , { name: AuthTokenName, token: AuthToken } + , { upsert: true } + ) + .exec( ( err, data ) => _self.__emitAuthComplete() ); + } + else + { + Dragonfly.Error( "Unable to authenticate: " + e.ResponseString ); + _self.__emitAuthComplete(); + } + } + + __emitAuthComplete() + { + this.__inAuth = false; + this.emit( "AuthComplete", this ); + } + +} + +module.exports = WNSAuth; diff --git a/notify-term/app.js b/notify-term/app.js new file mode 100644 index 0000000..d832b0f --- /dev/null +++ b/notify-term/app.js @@ -0,0 +1,93 @@ +"use strict"; + +var cl = global.botanLoader; +var Dragonfly = global.Dragonfly; + +var HttpRequest = cl.load( "botanss.net.HttpRequest" ); +var Base = cl.load( "notifysrv.postframe" ); + +var WNSAuth = cl.load( "notifyterm.WNSAuth" ); +var Model = cl.load( "notifyterm.schema" ); +var NotisQ = cl.load( "notifyterm.Notis" ); + +class App extends Base +{ + constructor( Http ) + { + super( Http ); + this.result = "Hello there! This is a notify-term server.\nFor more information please head to https://github.com/tgckpg/notify-term"; + + this.OAuth = new WNSAuth(); + this.OAuth.addListener( "AuthComplete", this.OnAuthed.bind( this ) ); + this.OAuth.Authenticate(); + this.RequestQueue = []; + + this.addListener( "PostRequest", this.PostRequest ); + } + + PostRequest( sender, e ) + { + e.Handled = true; + + var _self = this; + var query = e.Data; + + switch( query.action ) + { + case "register": + this.OAuth.Register( + query.uri, ( sender, mesg ) => { + _self.result = mesg; + _self.plantResult(); + } ); + break; + + case "deliver": + this.__sendMesg( query ); + break; + default: + this.result = "Invalid Action"; + this.plantResult(); + } + } + + __sendMesg( query ) + { + var N = new NotisQ( query ); + if( N.Valid ) + { + this.RequestQueue.push( N ); + this.result = "Your message has been queued"; + } + else + { + this.result = "Invalid message format"; + } + + this.plantResult(); + this.OAuth.Authenticate(); + } + + OnAuthed( OAuth ) + { + if( !OAuth.IsAuthenticated ) + { + Dragonfly.Error( "Unable to authenticate" ); + } + + this.ProcessQueue(); + } + + ProcessQueue() + { + if( !this.RequestQueue.length ) return; + + var Request = this.RequestQueue.shift(); + + Dragonfly.Info( "Processing Request: " + Request ); + + this.OAuth.Deliver( Request ); + } +} + +module.exports = App; diff --git a/notify-term/schema.js b/notify-term/schema.js new file mode 100644 index 0000000..137af18 --- /dev/null +++ b/notify-term/schema.js @@ -0,0 +1,77 @@ +var Dragonfly = global.Dragonfly; +var cl = global.botanLoader; +var util = require( "util" ); +var events = require( "events" ); +var mongoose = require( "mongoose" ); + var Schema = mongoose.Schema; + +var options = cl.load( "notifyterm.config.db" ); + +var throwEverything = function( err ) { + if( err ) { + throw err; + } +}; + +var db = mongoose.connection; +db.on( "error", throwEverything ); + +mongoose.connect( options.host, options.auth ); + +/* Schema Heads */ +var R_Tokens = { type: Schema.Types.ObjectId, ref: "Tokens" }; +/* End Schema Heads */ + +var M_Tokens = new Schema({ + name: { type: String, unique: true } + , token: { type: String } +}); + + +var DB = function () +{ + events.EventEmitter.call( this ); + var Models = [ + { name: "Tokens" , schema: M_Tokens , hasKey: true } + ]; + + var l = Models.length; + + var _widx = 0; + var _widxl = 0; + + var _self = this; + + var ready = function() + { + _self.ready = true; + _self.emit( "ready", _self ); + }; + + var waitIndex = function( err ) + { + _widx ++; + throwEverything( err ); + if( _widx == _widxl ) ready(); + }; + + this.ready = false; + + for( var i = 0; i < l; i ++ ) + { + var mod = Models[i]; + var b = mongoose.model( mod.name, mod.schema ); + if( mod.hasKey ) + { + _widxl ++; + b.on( "index", waitIndex ); + } + this[ mod.name ] = b; + } + + if( !_widxl ) ready(); +}; + +util.inherits( DB, events.EventEmitter ); + +module.exports = new DB(); diff --git a/notifysrv/eventargs/eventargs.js b/notifysrv/eventargs/eventargs.js new file mode 100644 index 0000000..72d7ce5 --- /dev/null +++ b/notifysrv/eventargs/eventargs.js @@ -0,0 +1,15 @@ +"use strict"; + +var cl = global.botanLoader; +var Dragonfly = global.Dragonfly; +var qstr = cl.load( "notifysrv.utils.querystr" ); + +class EventArgs +{ + constructor() + { + this.Handled = false; + } +} + +module.exports = EventArgs; diff --git a/notifysrv/eventargs/postrequest.js b/notifysrv/eventargs/postrequest.js new file mode 100644 index 0000000..4b348cb --- /dev/null +++ b/notifysrv/eventargs/postrequest.js @@ -0,0 +1,23 @@ +"use strict"; + +var cl = global.botanLoader; +var Dragonfly = global.Dragonfly; +var qstr = cl.load( "notifysrv.utils.querystr" ); + +var EventArgs = cl.load( "notifysrv.eventargs.eventargs" ); + +class PostRequestEventArgs extends EventArgs +{ + constructor( QueryString ) + { + super(); + this.Raw = QueryString; + } + + get Data() + { + return qstr.queryStr( this.Raw ); + } +} + +module.exports = PostRequestEventArgs; diff --git a/notifysrv/postframe.js b/notifysrv/postframe.js new file mode 100644 index 0000000..4b387e0 --- /dev/null +++ b/notifysrv/postframe.js @@ -0,0 +1,91 @@ +"use strict"; + +var cl = global.botanLoader; +var Dragonfly = global.Dragonfly; + +var PostRequestEventArgs = cl.load( "notifysrv.eventargs.postrequest" ); +var EventEmitter = require( "events" ).EventEmitter; + +class PostFrame extends EventEmitter +{ + constructor( Http ) + { + super(); + this.HTTP = Http; + this.result = "Error: PostFrame is unhandled"; + this.planted = false; + } + + run() + { + var _self = this; + var requestStr = ""; + + if( this.HTTP.request.isPost ) + { + var Req = this.HTTP.request.raw; + + var ReceiveData = function( data ) + { + requestStr += data + ""; + if( 51200 < requestStr.length ) + { + _self.result = "The size of request is too big ( 500KB < )"; + Req.removeListener( "data", ReceiveData ); + Req.removeListener( "end", ReceiveEnd ); + + _self.plantResult(); + } + }; + + var ReceiveEnd = function() + { + var EventArgs = new PostRequestEventArgs( requestStr ); + + _self.emit( "PostRequest", this, EventArgs ); + if( !EventArgs.Handled ) + { + _self.result = "Error: Unhandled Request"; + _self.plantResult(); + } + }; + + Req.addListener( "data", ReceiveData ); + Req.addListener( "end", ReceiveEnd ); + return; + } + else + { + Dragonfly.Info( + "GET: " + encodeURI( this.HTTP.request.raw.url ) + + " - " + this.HTTP.request.raw.headers["user-agent"] + , Dragonfly.Visibility.VISIBLE + ); + } + + this.plantResult(); + } + + plantResult() + { + if( !this.planted ) + { + this.planted = true; + if( this.HTTP ) + { + if( !( this.result instanceof Buffer ) ) + { + this.result = String( this.result ); + } + + this.HTTP.response.headers["Content-Type"] = "text/plain"; + this.HTTP.response.headers["Content-Length"] = this.result.length; + this.HTTP.response.write( this.result ); + this.HTTP.response.end(); + } + + } + } +} + +module.exports = PostFrame; diff --git a/notifysrv/utils/querystr.js b/notifysrv/utils/querystr.js new file mode 100644 index 0000000..9f19f96 --- /dev/null +++ b/notifysrv/utils/querystr.js @@ -0,0 +1,14 @@ +module.exports = { + queryStr: function( qstr ) + { + var qObj = {}; + + qstr.split( "&" ).forEach( function( val ) + { + val = val.split( "=" ); + qObj[ val[0] ] = val[1] ? decodeURIComponent( val[1].replace( /\+/g, " " ) ) : true; + } ); + + return qObj; + } +}; diff --git a/notifysrv/utils/random.js b/notifysrv/utils/random.js new file mode 100644 index 0000000..8fdcfc5 --- /dev/null +++ b/notifysrv/utils/random.js @@ -0,0 +1,15 @@ +var lut = []; for ( var i=0; i<256; i++ ) { lut[i] = (i<16?'0':'')+(i).toString(16); } + +module.exports = { + uuid: function() + { + var d0 = Math.random()*0xffffffff|0; + var d1 = Math.random()*0xffffffff|0; + var d2 = Math.random()*0xffffffff|0; + var d3 = Math.random()*0xffffffff|0; + return lut[d0&0xff]+lut[d0>>8&0xff]+lut[d0>>16&0xff]+lut[d0>>24&0xff]+'-'+ + lut[d1&0xff]+lut[d1>>8&0xff]+'-'+lut[d1>>16&0x0f|0x40]+lut[d1>>24&0xff]+'-'+ + lut[d2&0x3f|0x80]+lut[d2>>8&0xff]+'-'+lut[d2>>16&0xff]+lut[d2>>24&0xff]+ + lut[d3&0xff]+lut[d3>>8&0xff]+lut[d3>>16&0xff]+lut[d3>>24&0xff]; + } +}; diff --git a/notifysrv/utils/string.js b/notifysrv/utils/string.js new file mode 100644 index 0000000..bfe30cd --- /dev/null +++ b/notifysrv/utils/string.js @@ -0,0 +1,13 @@ +module.exports = { + encodeHtml: function ( str, br ) + { + str = ( str + "" ).replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + ; + if( br ) str = str.replace( /\n/g, "
" ); + return str; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f7e02cb --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "notify-term" + , "exportName": "notify-term" + , "version": "0.0.1" + , "dependencies": { + "mongoose": "latest" + } + , "description": "The notify-term push notification server" + , "main": "index.js" + , "scripts": { + } + , "keywords": [ + "notification" + , "terminal" + , "notifier" + ] + , "author": "Penguin " + , "license": "MIT" + , "main": "index.js" +}