UNPKG

chocolate

Version:

A full stack Node.js web framework built using Coffeescript

572 lines (479 loc) 28.7 kB
# Here is the Interface service of the Chocolate system. # As all non-intentional services, it resides in the *system* sub-section of the *Chocolate* section. # It manages the exchanges the Chocolate server *world* # by providing an **exchange** service that can receive a request and produce a response. # It requires **Node.js events** to communicate with asynchronous functions Events = require 'events' Path = require 'path' Fs = require 'fs' Crypto = require 'crypto' File = require './file' Formidable = require 'formidable' _ = require '../general/chocodash' Chocokup = require '../general/chocokup' Chocodown = require '../general/chocodown' Highlight = require '../general/highlight' Interface = require '../general/locco/interface' #### Cook # `cook` serves a cookies object containing cookies from the sent request exports.cook = (request) -> cookies = {} (cookie_pair = http_cookie.split '=' ; cookies[cookie_pair[0].trim()] = cookie_pair[1].trim()) for http_cookie in http_cookies.split ';' if (http_cookies = request.headers['cookie'])? cookies #### Exchange # `exchange` operates the interface exports.exchange = (bin, send) -> {space, workflow, so, what, how, where, region, params, sysdir, appdir, datadir, backdoor_key, request, response, session, websocket, websockets} = bin config = require('./config')(datadir) where = where.replace(/\.\.[\/]*/g, '') console_ = log: -> console.log.apply console, arguments try websocket?.send JSON.stringify console:log:(if arguments.length is 1 then arguments[0] else arguments) context = {space, workflow, request, response, region, where, what, params, arguments:[], websocket, websockets, session, sysdir, appdir, datadir, config, console:console_} config = config.clone() # `respond` will send the computed result as an Http Response. respond = (result, as = how) -> return if result instanceof Interface.Reaction and result.piped type = 'text' status = 200 subtype = switch as when 'web', 'edit', 'help' then 'html' when 'manifest' then 'cache-manifest' when 'pwa_worker' then 'javascript' else 'plain' unless as is 'raw' switch request.headers['accept']?.split(',')[0] when 'application/json' then as = 'json' when 'application/json-late' then as = 'json-late' response_headers = { "Content-Type":"#{type}/#{subtype}; charset=utf-8" } switch as when 'manifest', 'pwa_worker' response_headers['Cache-Control'] = 'max-age=0' response_headers['Expires'] = new Date().toUTCString() when 'raw', 'json', 'json-late' if result instanceof Interface.Reaction then delete result.props unless as is 'raw' and Object.prototype.toString.call(result) is '[object String]' result = if as is 'json-late' then _.stringify(result) else JSON.stringify(result) when 'web', 'edit', 'help' # render if instance of Chocokup if result instanceof Interface.Reaction if result.redirect status = 303 response_headers['Location'] = result.redirect result = '' else result = result.bin ? '' if result instanceof Chocokup try result = result.render { backdoor_key } catch error return has500 error # Quirks mode in ie6 if /msie 6/i.test request.headers['user-agent'] result = '<?xml version="1.0" encoding="iso-8859-1"?>\n' + result # Defaults to Unicode unless request.headers['x-requested-with'] is 'XMLHttpRequest' if result?.indexOf?('</head>') > 0 result = result.replace('</head>', '<meta http-equiv="content-type" content="text/html; charset=utf-8" /></head>') else if result?.indexOf?('<body') > 0 result = result.replace('<body', '<head><meta http-equiv="content-type" content="text/html; charset=utf-8" /></head><body') else result = '<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8" /></head><body>' + result + '</body></html>' send {status, headers:response_headers, body:result} # `has500` will send an HTTP 500 error has500 = (error) -> if error? if config.displayErrors source = if (info = error.source)? then "Error in Module:" + info.module + ", with Function :" + info.method + '\n' else '' line = if (info = error.location)? then "Coffeescript error at line:" + info.first_line + ", column:" + info.first_column + '\n' else '' send status : 500, headers : {"Content-Type": "text/plain"}, body : source + line + (error.stack ? error.toString()) + "\n" else send status : 500, headers : {"Content-Type": "text/plain"}, body : "\n" true else false # `hasSofkey` checks if requestor has the system sofkey which gives full rights access hasSofkey = -> hasKeypass = config.keypass is on and File.hasWriteAccess(appdir) is no hashed_backdoor_key = if backdoor_key isnt '' then Crypto.createHash('sha256').update(backdoor_key).digest('hex') else '' if config.sofkey in [hashed_backdoor_key, backdoor_key] or config.sofkey of session.keys or hasKeypass then true else false # `getMethodInfo` retrieve a method and its parameters from a required module getMethodInfo = ({required, action, instanciate}) -> try self = klass = property = undefined method = node_module = require required if action? if node_module?.prototype? method = method[name] for name in ('prototype.' + action).split('.') when method? if method? self = null ; klass = node_module ; property = name if instanciate is on self = new klass self.hydrate?.call self, args if node_module? and method is node_module or not method? method ?= node_module method = method[name] for name in action.split('.') when method? if action? and method instanceof Function infos = method.toString().match(/function(\s+\w*)*\s*\((.*?)\)/) args = infos[2].split(/\s*,\s*/) if infos? else if method instanceof Interface self = method method = method.submit args = ['{__}'] else throw new Error("Can't find '#{action}' in #{required}") {method, args, self, klass, property} catch error error.source = module:required, method:action {method:undefined, args:undefined, self:undefined, klass:undefined, property:undefined, error} # `respondStatic` will send an HTTP response respondStatic = (status, headers, body) -> send { status, headers, body } # `exchangeStatic` is an interface with static web files exchangeStatic = () -> # Asking for a *static* resource (image, css, javascript...) if so is 'go' returns_empty = -> respondStatic 200, {}, '' returns = (required) -> extension = Path.extname where Fs.stat required, (error, stats) -> return if has500 error headers = 'Date' : (new Date()).toUTCString() 'Etag' : [stats.ino, stats.size, Date.parse(stats.mtime)].join '-' 'Last-Modified' : (new Date(stats.mtime)).toUTCString() 'Cache-Control' : 'max-age=0' 'Expires' : new Date().toUTCString() if Date.parse(stats.mtime) <= Date.parse(request.headers['if-modified-since']) or request.headers['if-none-match'] is headers['Etag'] return respondStatic 304, headers else Fs.readFile required, null, (error, file) -> return if has500 error headers["Content-Type"] = switch extension when '.css' then "text/css" when '.js' then "text/javascript" when '.manifest' then "text/cache-manifest" when '.ttf' then "font/ttf" when '.html', '.md', '.markdown', '.cd', '.chocodown', '.ck', '.chocokup' then "text/html" when '.pdf' then "application/pdf" when '.gif' then "image/gif" when '.png' then "image/png" when '.jpg', '.jpeg' then "image/jpeg" respondStatic 200, headers, switch extension when '.md', '.markdown', '.cd', '.chocodown', '.ck', '.chocokup' try if extension in ['.ck', '.chocokup'] try html = new Chocodown.Chocokup.Panel(file.toString()).render() catch e then html = e.message else html = new Chocodown.converter().makeHtml file.toString() if html.indexOf('<body') < 0 then html = '<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8" /></head><body>' + html + '</body></html>' else html catch error 'Error loading ' + where + ': ' + error else file dirs = [] # check in appdir dirs.push dir:(appdir ? '.') + '/' # check in extensions dirs if config.extensions? then for extension, mounting_point of config.extensions dirs.push {dir:(appdir ? '.') + "/node_modules/#{extension}/", mounting_point} # check in sysdir dirs.push dir:__dirname + '/../' for {dir, mounting_point} in dirs required = Path.resolve dir + (unless mounting_point? and mounting_point isnt '' then where else where.replace(mounting_point + '/', '')) if where.indexOf('static/' + (unless mounting_point? then '' else mounting_point)) is 0 if Fs.existsSync required then returns required ; return returns_empty() # `canExchange` check if current user has rights to operate exchange canExchange = () -> return hasSofkey() or (so is 'go' and where is 'ping') or (where is 'server/interface' and so is 'do' and what in ['register_key', 'forget_key']) # `exchangeSystem` is an interface with system files exchangeSystem = () -> # When authorized to access system resource -- if canExchange() then exchangeClassic() else respond '' # `canExchangeClassic` checks if a file can be required at the specified path # so that a classic exchange can occur canExchangeClassic = (path) -> result = required: '../' + (if region is 'system' or appdir is '.' then '' else appdir + '/' ) + path found: yes return result if hasSofkey() and so is 'move' try require.resolve result.required catch error result.found = no if config.extensions? then for extension, mounting_point of config.extensions if (path.indexOf(mounting_point) is 0) or (path.indexOf('client/' + mounting_point) is 0) or (path.indexOf('general/' + mounting_point) is 0) or (path.indexOf('server/' + mounting_point) is 0) result.required = '../' + (if appdir is '.' then '' else appdir + '/' ) + "node_modules/#{extension}/" + (if mounting_point is '' then path else path.replace(mounting_point + '/', '')) try require.resolve result.required catch error then continue result.found = yes result.extension = extension break result # `exchangeClassic` is an interface with classic files (coffeescript or javascript) exchangeClassic = (required, extension) -> required ?= '../' + (if region is 'system' or appdir is '.' then '' else appdir + '/' ) + where #__ = if region is 'system' or appdir is '.' then undefined else context __ = context what_is_public = no is_classic_web_request = do -> return no if request.headers['x-requested-with'] is 'XMLHttpRequest' return yes if so is 'do' and how is 'web' return yes if so is 'go' and how is 'web' and where isnt 'ping' no switch so # if so is 'do' and what is public then authorize access when 'do' {method, args, self, klass, property, error} = getMethodInfo { required, action:what } return if has500 error if method? and region isnt 'secure' what_is_public = yes # if so is 'go' and where has a public interface then do use interface when 'go' if how is 'web' and where isnt 'ping' {method, args, self, klass, property, error} = getMethodInfo { required } unless error if method? then so = 'do' ; what = undefined ; what_is_public = yes else {method, args, self, klass, property, error} = getMethodInfo { required, action:'interface' } return if has500 error if method? # TODO - should implement security checking so = 'do' ; what = 'interface' unless (so is 'do' and (what is 'interface' or what_is_public)) or canExchange() then respond ''; return switch so # Take care of the `go` action when 'go' # Answer to ping request if where is 'ping' respond '{"status":"Ok"}' return # Get the system resource and check how to return it resource_path = require.resolve required switch how # When `How` is 'web' or 'edit' when 'web', 'edit' File.access(where, backdoor_key, __).on 'end', (html) -> respond html.error ? html # when `How` is 'raw' when 'raw', 'manifest', 'pwa_worker' # Read the file and returns it Fs.readFile resource_path, (err, data) -> respond data.toString() # when `How` is 'help' when 'help' # Ask **Doccolate** to generate a help page and returns it Fs.readFile resource_path, (err, data) -> respond require('../general/doccolate').generate resource_path, data.toString() # when `How` is unknown else # Returns an error message respond "Don't know how to respond as '" + how + "'" # Take care of the `move` action when 'move' respondOnMoveFile = (count) -> return respond '' if where is '' or count is 0 results = [] for index in [0...count ? 1] do -> filename = File.setFilenameSuffix where, if index > 0 then "_#{index}" else '' File.getModifiedDate(filename, __).on 'end', (modifiedDate) -> results.push {filename, modifiedDate} if modifiedDate? respond JSON.stringify results # Check if the `what` is empty if what is '' # If empty, take the input content from the POST data if request.method is 'POST' content_type = request.headers['content-type']?.split(';')[0] switch content_type when 'multipart/form-data' form = new Formidable.IncomingForm() form.keepExtensions = yes form.parse request, (err, fields, files) -> count = sent = received = 0 errs = [] count += 1 for name, file of files for name, file of files from = if __?.appdir? then Path.relative __.appdir, file.path else file.path if Path.extname(from) is '' Fs.renameSync from, from = from + '.tmp' to = unless err? then where else '' event = File.moveFile from, File.setFilenameSuffix(to, if sent > 0 then "_#{sent}" else ''), __ event.on 'end', (err) -> received += 1 errs.push err if err? if received is count if errs.length > 0 then respond errs.join('\n') else respondOnMoveFile count sent += 1 else source = chunks:[], length:0 request.on 'data', (chunk) -> source.chunks.push chunk source.length += chunk.length # test request.on 'end', () -> # When all POST data received, save new version File.writeToFile(where, Buffer.concat(source.chunks, source.length), __).on 'end', (err) -> if err? then respond err.toString() else respondOnMoveFile() # If no POST date, create the `where` file with content from first parameter else File.writeToFile(where, params._0, __).on 'end', (err) -> if err? then respond err.toString() else respondOnMoveFile() # If specified, move the `what` content to the `where` content else File.moveFile(what, where, __).on 'end', (err) -> if err? then respond err.toString() else respondOnMoveFile() # Take care of the `eval` and `do` actions when 'do', 'eval' if so is 'eval' args = [(if how is 'raw' then 'json' else 'html'), Path.resolve(__dirname + '/' + required), context] required = '../general/specolate' action = 'inspect' node_module = require required method = node_module[action] else if so is 'do' {method, args:expected_args, self, klass, property, error} = getMethodInfo { required, action:what, instanciate:on } return if has500 error if '__' in expected_args then params['__'] = context args = [] if is_classic_web_request and region is 'app' and config.masterInterfaces? for masterInterface in config.masterInterfaces doMasterInterface = no if _.type(masterInterface.directory) is _.Type.Array for directory in masterInterface.directory if where.indexOf(directory + '/') is 0 then doMasterInterface = yes ; break else if masterInterface.directory? is false or masterInterface.directory is '' or where.indexOf(masterInterface.directory + '/') is 0 then doMasterInterface = yes if doMasterInterface and masterInterface.exclude? for exclusion in masterInterface.exclude if where.indexOf(exclusion) is 0 doMasterInterface = no break if doMasterInterface if self instanceof Interface params[masterInterface.prop] = self else self_isnt_interface = true masterMethod = method params[masterInterface.prop] = new Interface.Web.Html use: -> {masterMethod} render: -> text @props.masterMethod.apply undefined, @props.__.arguments self = require('../' + (if region is 'system' or appdir is '.' then '' else appdir + '/' ) + (if masterInterface.extension? then "node_modules/#{masterInterface.extension}/" else '') + masterInterface.where)[masterInterface.what] self.embedded = params[masterInterface.prop] method = self.submit if expected_args[0] is '{__}' or self_isnt_interface bin = {__:context} bin[k] = v for k, v of params when k isnt '__' args.push bin args_index = 0 for arg_name in expected_args context.arguments.push if params[arg_name] isnt undefined then params[arg_name] else params[ '__' + args_index++ ] while args_index >= 0 if params['__' + args_index] is undefined then args_index = -1 else context.arguments.push params[ '__' + args_index++ ] if expected_args[0] isnt '{__}' args.push(arg) for arg in context.arguments produced = method.apply self, args if produced instanceof _.Publisher produced.subscribe (answer) -> respond answer else if produced instanceof Events.EventEmitter produced.on 'end', (answer) -> respond answer else respond produced else respond '' # `exchangeSimple` is an interface with intentional entities. # However, if a file can be required at the specified path, a classic exchange will occur exchangeSimple = () -> path = if where is '' and so isnt 'move' then where = 'default' else where result = canExchangeClassic path if result.found where = path try exchangeClassic(result.required, result.extension) catch err then hasSofkey() and has500(err) or respond '' else if region is 'app' and config.defaultExchange? old_params = params params_ = __0:where, __1:what index = 2 ; for k,v of params then params_['__' + index++] = v {extension, where, what, params} = _.clone {}, config.defaultExchange if extension? then where = "node_modules/#{extension}/#{where}" so = 'do' if what? what ?= '' if params? then for k,v of params then params[k] = (if _.type(v) is _.Type.Array then (if v.length is 1 then params_["__#{v[0]}"] else (params_["__#{i}"] for i in v)) else v) params ?= old_params exchangeClassic() else switch so when 'do' produced = '' when 'move' produced = '' when 'eval' produced = '' when 'go' # tranlate url query to Reserve query produced = where respond produced # To log requests... # require('../general/debugate').log '-> in ' + region + ' ' + so + ' ' + what + ' at ' + where + ' as ' + how + ' with ' + (k + ':' + v for k,v of params).join(' ') switch region when 'static' then exchangeStatic() when 'system' then exchangeSystem() else exchangeSimple() #### Key registration # `registerKey` provides a UI to register a system key in browser session cache exports.register_key = (__) -> enter_kup = -> form method:"post", -> text "Enter your Key : " input name:"key", type:"password" entered_kup = -> text 'Key registered' if __.request.method isnt 'POST' new Chocokup.Document 'Key registration', kups:{key:enter_kup}, Chocokup.Kups.Tablet else event = new Events.EventEmitter source = '' __.request.on 'data', (chunk) -> source += chunk __.request.on 'end', () -> fields = require('querystring').parse source __.session.addKey Crypto.createHash('sha256').update(fields.key).digest('hex') event.emit 'end', new Chocokup.Document 'Key registration', kups:{key:entered_kup}, Chocokup.Kups.Tablet event # `forgetKey` provides a UI to clear keys from browser session cache exports.forget_keys = (__) -> forget_kup = -> form method:"post", -> input name:"action", type:"submit", value:"Logoff" forgeted_kup = -> text 'Keys forgotten' if __.request.method isnt 'POST' new Chocokup.Document 'Key unregistration', kups:{key:forget_kup}, Chocokup.Kups.Tablet else __.session.clearKeys() new Chocokup.Document 'Key unregistration', kups:{key:forgeted_kup}, Chocokup.Kups.Tablet #### Create Hash # `create_hash` returns the corresponding sha256 hash from a given key exports.create_hash = (__) -> enter_kup = -> form method:"post", -> text "Create your Key : " input name:"key", type:"password" entered_kup = -> text @params.hash if __.request.method isnt 'POST' new Chocokup.Document 'Key creation', kups:{key:enter_kup}, Chocokup.Kups.Tablet else event = new Events.EventEmitter source = '' __.request.on 'data', (chunk) -> source += chunk __.request.on 'end', () -> fields = require('querystring').parse source key = Crypto.createHash('sha256').update(fields.key).digest('hex') event.emit 'end', new Chocokup.Document 'Key registration', kups:{key:entered_kup}, hash:key, Chocokup.Kups.Tablet event exports.crash = -> while true setTimeout -> throw new Error('We crashed!!!!!') , 100 'done'