arcane-middleware
Version:
Middleware for Arcane framework.
692 lines (576 loc) • 17.4 kB
text/coffeescript
#!package export MiddlewareHandler
#!import http-proxy
#!import fs
#!import path
#!import url
#!import http
#!import net
#!import fresh
#!import parseurl
#!import proxy-addr
#!import type-is
#!import accepts
#!import range-parser
#!import vary
#!import content-type
#!import etag
#!import mime
#!import cookie
#!import cookie-signature
#!import utils-merge
class MiddlewareFlow
constructor: (req, res, app) ->
req.__proto__ = MiddlewareHandler.req_serv
res.__proto__ = MiddlewareHandler.res_serv
res.req = req
req.res = res
@res = res
@req = req
@loaded = {
$req: req
$res: res
$app: @
}
for i in app.Store.stack
if i[0]?
# console.log i[0]
@loaded["$#{i[0]}"] = i[1].apply i[2], [req, res, @] #new_p
else
# console.log i[1]
i[1].apply i[2], [req, res, @]
break if res.finished
break if res._header
use: (handler, result) ->
if typeof handler is 'function'
param_list = MiddlewareHandler.parm.get handler
new_p = []
for i in param_list
new_p.push @loaded[i] ? null
ret = null
err = null
try
ret = handler.apply null, new_p
catch err
if typeof err in ['string', 'number']
throw err
else
console.log err.stack ? err
result(err, ret) if typeof result is 'function'
return ret
class MiddlewareHandler
charsetRegExp = /;\s*charset\s*=/
settings: {}
# modules_list: {}
@parm: require('./params').ParameterHandler
@res_serv =
__proto__: http.ServerResponse.prototype
@req_serv =
__proto__: http.IncomingMessage.prototype
# push_params: (handler) ->
# ret = []
# param_list = MiddlewareHandler.parm.get handler
#
# for i in param_list
# name = i.replace ///^\$///g, ''
# ret.push name
#
# if not @Store.flow[name]? and @modules_list?[name]?
# @Store.flow[name] = true
# @Store.flow[name] = @push_params @modules_list[name].__middle
# @Store.stack.push [name, @modules_list[name].__middle, @modules_list[name]]
#
# return ret
flow: MiddlewareFlow
constructor: (@name) ->
@Store =
stack: []
# flow: {}
# settings: {}
# @loaded = {}
# flow: (req, res) ->
# req.__proto__ = MiddlewareHandler.req_serv
# res.__proto__ = MiddlewareHandler.res_serv
#
# res.req = req
# req.res = res
#
# self = this
# self.loaded = {
# $req: req
# $res: res
# $app: @
# }
#
# # console.log req.url, self.Store.stack
#
# # console.log 'ORIGINAL URL', req.url
# # console.log @Store
#
# for i in self.Store.stack
# # new_p = []
# # for j in Store.flow[i[0]]
# # new_p.push self.loaded["$#{j}"] ? null
#
# if i[0]?
# # console.log i[0]
# self.loaded["$#{i[0]}"] = i[1].apply i[2], [req, res, self] #new_p
# else
# # console.log i[1]
# i[1].apply i[2], [req, res, self]
#
# break if res.finished
use: (handler, result) ->
if typeof handler is 'function'
param_list = MiddlewareHandler.parm.get handler
new_p = []
for i in param_list
new_p.push @loaded[i] ? null
ret = null
err = null
try
ret = handler.apply null, new_p
catch err
if typeof err in ['string', 'number']
throw err
else
console.log err.stack ? err
result(err, ret) if typeof result is 'function'
return ret
get: (name) ->
set: (name, value) ->
useMod: (name, handler) ->
handle = handler if handler.constructor.name is 'Function' or name.constructor.name is 'Object'
handle = name if name.constructor.name is 'Function' or name.constructor.name is 'Object'
n = name if name.constructor.name is 'String'
if handle instanceof MiddlewareHandler and typeof handle.middleware is 'function'
# @Store.flow[handle.name] = true
# @Store.flow[handle.name] = @push_params handle.middleware
@Store.stack.push [handle.name, handle.middleware, handle]
else if handle and n
# @Store.flow[n] = true
# @Store.flow[n] = @push_params handle
@Store.stack.push [n, handle, null]
else if handle
@Store.stack.push [null, handle, null]
@get: (name) ->
@set: (name, value) ->
###---##########################################################################################################################
#
#
################################################################################################################################
__etag = (body, encoding) ->
buf = if !Buffer.isBuffer(body) then new Buffer(body, encoding) else body
etag buf, weak: false
__wetag = (body, encoding) ->
buf = if !Buffer.isBuffer(body) then new Buffer(body, encoding) else body
etag buf, weak: true
setCharset = (type, charset) ->
if !type or !charset
return type
# parse type
parsed = contentType.parse(type)
# set charset
parsed.parameters.charset = charset
# format type
contentType.format parsed
acceptParams = (str, index) ->
parts = str.split(RegExp(' *; *'))
ret =
value: parts[0]
quality: 1
params: {}
originalIndex: index
i = 1
while i < parts.length
pms = parts[i].split(RegExp(' *= *'))
if 'q' == pms[0]
ret.quality = parseFloat(pms[1])
else
ret.params[pms[0]] = pms[1]
++i
ret
normalizeType = (type) ->
if ~type.indexOf('/') then acceptParams(type) else
value: mime.lookup(type)
params: {}
normalizeTypes = (types) ->
ret = []
i = 0
while i < types.length
ret.push normalizeType(types[i])
++i
ret
escapeHtml = (html) ->
String(html).replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace />/g, '>'
# http.IncomingMessage.prototype.testing123 = ->
# console.log 'heloloolooloolo'
MiddlewareHandler.res_serv.moveFileUpload = (name, to, cb) ->
if $req.files and $req.files[name]
try
fs.unlinkSync $req.root + to
catch err
throw err
source = fs.createReadStream $req.files[name].path
dest = fs.createWriteStream $req.root + to
source.pipe dest
source.on 'end', () ->
if cb and typeof cb is 'function' then cb null, true
source.on 'error', (err) ->
if cb and typeof cb is 'function' then cb err, null
MiddlewareHandler.res_serv.download = (_mime, filename, file) ->
@set 'Content-disposition', "attachment; filename=#{encodeURIComponent filename}"
@set 'Content-type', _mime
@updateHeaders 200
filestream = fs.createReadStream file
filestream.pipe @
throw 'stream'
MiddlewareHandler.res_serv.sendStatus = (statusCode) ->
body = http.STATUS_CODES[statusCode] or String(statusCode)
@statusCode = statusCode
@type 'txt'
@send body
MiddlewareHandler.res_serv.status = (code) ->
@statusCode = code
return @
MiddlewareHandler.res_serv.get = (field) ->
@getHeader field
MiddlewareHandler.res_serv.set =
MiddlewareHandler.res_serv.header = (field, val) ->
throw 'done' if @finished
if arguments.length == 2
value = if Array.isArray(val) then val.map(String) else String(val)
# add charset to content-type
if field.toLowerCase() == 'content-type' and !charsetRegExp.test(value)
charset = mime.charsets.lookup(value.split(';')[0])
if charset
value += '; charset=' + charset.toLowerCase()
@setHeader field, value
else
for key of field
@set key, field[key]
this
MiddlewareHandler.res_serv.location = (url) ->
loc = url
# "back" is an alias for the referrer
if url == 'back'
loc = @req.get('Referrer') or '/'
# set location
@set 'Location', loc
throw 'location'
this
MiddlewareHandler.res_serv.redirect = (url) ->
address = url
body = undefined
status = 302
# allow status / url
if arguments.length == 2
if typeof arguments[0] == 'number'
status = arguments[0]
address = arguments[1]
else
console.log 'res.redirect(url, status): Use res.redirect(status, url) instead'
status = arguments[1]
# Set location header
@location address
address = @get('Location')
# Support text/{plain,html} by default
@format
text: ->
body = http.STATUS_CODES[status] + '. Redirecting to ' + encodeURI(address)
return
html: ->
u = escapeHtml(address)
body = '<p>' + http.STATUS_CODES[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>'
return
default: ->
body = ''
return
# Respond
# body = statusCodes[status] + '. Redirecting to ' + encodeURI(address)
###################################
@statusCode = status
@set 'Content-Length', Buffer.byteLength(body)
if @req.method == 'HEAD'
@end()
else
@end body
throw 'redirect'
return
MiddlewareHandler.res_serv.send = (body) ->
# console.log (new Error).stack
chunk = body
encoding = undefined
len = undefined
req = @req
type = undefined
# settings
app = @app
# allow status / body
if arguments.length == 2
# res.send(body, status) backwards compat
if typeof arguments[0] != 'number' and typeof arguments[1] == 'number'
# console.log 'res.send(body, status): Use res.status(status).send(body) instead'
@statusCode = arguments[1]
else
# console.log 'res.send(status, body): Use res.status(status).send(body) instead'
@statusCode = arguments[0]
chunk = arguments[1]
# disambiguate res.send(status) and res.send(status, num)
if typeof chunk == 'number' and arguments.length == 1
# res.send(status) will set status message as text string
if !@get('Content-Type')
@type 'txt'
# console.log 'res.send(status): Use res.sendStatus(status) instead'
@statusCode = chunk
chunk = http.STATUS_CODES[chunk]
switch typeof chunk
# string defaulting to html
when 'string'
if !@get('Content-Type')
@type 'html'
when 'boolean', 'number', 'object'
if chunk == null
chunk = ''
else if Buffer.isBuffer(chunk)
if !@get('Content-Type')
@type 'bin'
else
return @json(chunk)
# write strings in utf-8
if typeof chunk == 'string'
encoding = 'utf8'
type = @get('Content-Type')
# reflect this in content-type
if typeof type == 'string'
@set 'Content-Type', setCharset(type, 'utf-8')
# populate Content-Length
if chunk != undefined
if !Buffer.isBuffer(chunk)
# convert chunk to Buffer; saves later double conversions
chunk = new Buffer(chunk, encoding)
encoding = undefined
len = chunk.length
@set 'Content-Length', len
# populate ETag
# etag = undefined
generateETag = len != undefined and __etag # app.get('etag fn')
if typeof generateETag == 'function' and !@get('ETag')
if _etag = generateETag(chunk, encoding)
@set 'ETag', _etag
# freshness
if req.fresh
@statusCode = 304
# strip irrelevant headers
if 204 == @statusCode or 304 == @statusCode
@removeHeader 'Content-Type'
@removeHeader 'Content-Length'
@removeHeader 'Transfer-Encoding'
chunk = ''
if req.method == 'HEAD'
# skip body for HEAD
@end()
else
# respond
@end chunk, encoding
throw 'done'
this
MiddlewareHandler.res_serv.contentType =
MiddlewareHandler.res_serv.type = (type) ->
ct = if type.indexOf('/') == -1 then mime.lookup(type) else type
@set 'Content-Type', ct
MiddlewareHandler.res_serv.format = (obj) ->
req = @req
# next = req.next
fn = obj.default
if fn
delete obj.default
keys = Object.keys(obj)
key = if keys.length > 0 then req.accepts(keys) else false
@vary 'Accept'
if key
@set 'Content-Type', normalizeType(key).value
obj[key] req, this #, next
else if fn
fn()
else
throw new Error 'Not Acceptable'
this
MiddlewareHandler.res_serv.vary = (field) ->
# checks for back-compat
if !field or Array.isArray(field) and !field.length
# deprecate 'res.vary(): Provide a field name'
return this
vary this, field
this
MiddlewareHandler.res_serv.json = (obj) ->
val = obj
# allow status / body
if arguments.length == 2
# res.json(body, status) backwards compat
if typeof arguments[1] == 'number'
# deprecate 'res.json(obj, status): Use res.status(status).json(obj) instead'
@statusCode = arguments[1]
else
# deprecate 'res.json(status, obj): Use res.status(status).json(obj) instead'
@statusCode = arguments[0]
val = arguments[1]
body = JSON.stringify(val, null, 4)
if !@get('Content-Type')
@set 'Content-Type', 'application/json'
@send body
MiddlewareHandler.res_serv.cookie = (name, value, options) ->
opts = utilsMerge({}, options)
secret = @req.secret
signed = opts.signed
if signed and !secret
throw new Error('cookieParser("secret") required for signed cookies')
val = if typeof value == 'object' then 'j:' + JSON.stringify(value) else String(value)
if signed
val = 's:' + cookieSignature.sign(val, secret)
if 'maxAge' of opts
opts.expires = new Date(Date.now() + opts.maxAge)
opts.maxAge /= 1000
if opts.path == null
opts.path = '/'
@append 'Set-Cookie', cookie.serialize(name, String(val), opts)
this
MiddlewareHandler.res_serv.append = (field, val) ->
prev = @get(field)
value = val
if prev
# concat the new and prev vals
value = if Array.isArray(prev) then prev.concat(val) else if Array.isArray(val) then [ prev ].concat(val) else [
prev
val
]
@set field, value
MiddlewareHandler.res_serv.clearCookie = (name, options) ->
opts = utilsMerge({
expires: new Date(1)
path: '/'
}, options)
@cookie name, '', opts
###*
# Helper function for creating a getter on an object.
#
# @param {Object} obj
# @param {String} name
# @param {Function} getter
# @private
###
defineGetter = (obj, name, getter) ->
Object.defineProperty obj, name,
configurable: true
enumerable: true
get: getter
return
MiddlewareHandler.req_serv.is = (types) ->
arr = types
# support flattened arguments
if !Array.isArray(types)
arr = new Array(arguments.length)
i = 0
while i < arr.length
arr[i] = arguments[i]
i++
typeIs this, arr
MiddlewareHandler.req_serv.accepts = ->
accept = accepts(this)
accept.types.apply accept, arguments
MiddlewareHandler.req_serv.acceptsEncodings = ->
accept = accepts(this)
accept.encodings.apply accept, arguments
MiddlewareHandler.req_serv.acceptsLanguages = ->
accept = accepts(this)
accept.languages.apply accept, arguments
MiddlewareHandler.req_serv.get =
MiddlewareHandler.req_serv.header = (name) ->
lc = name.toLowerCase()
switch lc
when 'referer', 'referrer'
return @headers.referrer or @headers.referer
else
return @headers[lc]
return
MiddlewareHandler.req_serv.range = (size) ->
range = @get('Range')
if !range
return
rangeParser size, range
###*
# Compile "proxy trust" value to function.
#
# @param {Boolean|String|Number|Array|Function} val
# @return {Function}
# @api private
###
compileTrust = (val) ->
if typeof val == 'function'
return val
if val == true
# Support plain true/false
return ->
true
if typeof val == 'number'
# Support trusting hop count
return (a, i) ->
i < val
if typeof val == 'string'
# Support comma-separated values
val = val.split(RegExp(' *, *'))
proxyAddr.compile val or []
defineGetter MiddlewareHandler.req_serv, 'protocol', ->
proto = if @connection.encrypted then 'https' else 'http'
# compileTrust(false) default value 'false' posible value ['192.168.1.0', '192.168.1.255']
if !compileTrust(false)(@connection.remoteAddress, 0)
return proto
# Note: X-Forwarded-Proto is normally only ever a
# single value, but this is to be safe.
proto = @get('X-Forwarded-Proto') or proto
proto.split(/\s*,\s*/)[0]
defineGetter MiddlewareHandler.req_serv, 'secure', ->
@protocol is 'https'
defineGetter MiddlewareHandler.req_serv, 'ip', ->
# compileTrust(false) default value 'false' posible value ['192.168.1.0', '192.168.1.255']
proxyAddr this, compileTrust(false)
defineGetter MiddlewareHandler.req_serv, 'ips', ->
# compileTrust(false) default value 'false' posible value ['192.168.1.0', '192.168.1.255']
addrs = proxyAddr.all(this, compileTrust(false))
addrs.slice(1).reverse()
defineGetter MiddlewareHandler.req_serv, 'subdomains', ->
hostname = @hostname
if !hostname
return []
subdomains = if !net.isIP(hostname) then hostname.split('.').reverse() else [ hostname ]
# offset to get the sub domain, 2 default
subdomains.slice 2
defineGetter MiddlewareHandler.req_serv, 'hostname', ->
host = @get('X-Forwarded-Host')
# compileTrust(false) default value 'false' posible value ['192.168.1.0', '192.168.1.255']
if !host or !compileTrust(false)(@connection.remoteAddress, 0)
host = @get('Host')
if !host
return
# IPv6 literal support
offset = if host[0] == '[' then host.indexOf(']') + 1 else 0
index = host.indexOf(':', offset)
if index != -1 then host.substring(0, index) else host
defineGetter MiddlewareHandler.req_serv, 'path', ->
parseurl(this).pathname
defineGetter MiddlewareHandler.req_serv, 'fresh', ->
method = @method
s = @res.statusCode
# GET or HEAD for weak freshness validation only
if 'GET' != method and 'HEAD' != method
return false
# 2xx or 304 as per rfc2616 14.26
if s >= 200 and s < 300 or 304 == s
return fresh(@headers, @res._headers or {})
false
defineGetter MiddlewareHandler.req_serv, 'stale', ->
!@fresh
defineGetter MiddlewareHandler.req_serv, 'xhr', ->
val = @get('X-Requested-With') or ''
val.toLowerCase() == 'xmlhttprequest'
defineGetter MiddlewareHandler.req_serv, 'base_url', ->
@baseUrl ? ''