Skip to content

Instantly share code, notes, and snippets.

@williamstein
Created January 24, 2014 17:23
Show Gist options
  • Save williamstein/8601924 to your computer and use it in GitHub Desktop.
Save williamstein/8601924 to your computer and use it in GitHub Desktop.
httpProxy = require('http-proxy')
init_http_proxy_server = () =>
_remember_me_check_for_write_access_to_project = (opts) ->
opts = defaults opts,
project_id : required
remember_me : required
cb : required # cb(err, has_access)
account_id = undefined
has_write_access = false
async.series([
(cb) ->
x = opts.remember_me.split('$')
database.key_value_store(name: 'remember_me').get
key : generate_hash(x[0], x[1], x[2], x[3])
cb : (err, signed_in_mesg) =>
account_id = signed_in_mesg?.account_id
if err or not account_id?
cb('unable to get remember_me cookie from db -- cookie invalid'); return
cb()
(cb) ->
user_has_write_access_to_project
project_id : opts.project_id
account_id : account_id
cb : (err, result) =>
if err
cb(err)
else if not result
cb("User does not have write access to project.")
else
has_write_access = true
cb()
], (err) ->
opts.cb(err, has_write_access)
)
_remember_me_cache = {}
remember_me_check_for_write_access_to_project = (opts) ->
opts = defaults opts,
project_id : required
remember_me : required
cb : required # cb(err, has_access)
key = opts.project_id + opts.remember_me
has_write_access = _remember_me_cache[key]
if has_write_access?
opts.cb(false, has_write_access)
return
# get the answer, cache it, return answer
_remember_me_check_for_write_access_to_project
project_id : opts.project_id
remember_me : opts.remember_me
cb : (err, has_write_access) ->
# if cache gets huge for some *weird* reason (should never happen under normal conditions) just reset it to avoid any possibility of DOS-->RAM crash attach
if misc.len(_remember_me_cache) >= 100000
_remember_me_cache = {}
_remember_me_cache[key] = has_write_access
# Set a ttl time bomb on this cache entry. The idea is to keep the cache not too big,
# but also if the user is suddenly granted permission to the project, this should be
# reflected within a few seconds.
f = () ->
delete _remember_me_cache[key]
if has_write_access
setTimeout(f, 1000*60*5) # write access lasts 5 minutes (i.e., if you revoke privs to a user they could still hit the port for 5 minutes)
else
setTimeout(f, 1000*15) # not having write access lasts 15 seconds
opts.cb(err, has_write_access)
_target_cache = {}
target = (remember_me, url, cb) ->
v = url.split('/')
project_id = v[1]
type = v[2] # 'port' or 'raw'
key = remember_me + project_id + type
if type == 'port'
key += v[3]
t = _target_cache[key]
if t?
cb(false, t)
return
tm = misc.walltime()
winston.debug("target: setting up proxy: #{v}")
host = undefined
port = undefined
async.series([
(cb) ->
if not remember_me?
# remember_me = undefined means "allow"; this is used for the websocket upgrade.
cb(); return
remember_me_check_for_write_access_to_project
project_id : project_id
remember_me : remember_me
cb : (err, has_access) ->
if err
cb(err)
else if not has_access
cb("user does not have write access to this project")
else
cb()
(cb) ->
database.get_project_location
project_id : project_id
allow_cache : false
cb : (err, _location) ->
if err
cb(err)
else
if _location?
host = _location.host
cb()
(cb) ->
if host?
cb()
else
storage.open_project_somewhere
project_id : project_id
base_url : program.base_url
cb : (err, _host) ->
host = _host
cb(err)
(cb) ->
# determine the port
if type == 'port'
port = parseInt(v[3])
cb()
else if type == 'raw'
new_local_hub
project_id : project_id
cb : (err, local_hub) ->
if err
cb(err)
else
# TODO Optimization: getting the status is slow (half second?), so
# we cache this for 15 seconds below; caching longer
# could cause trouble due to project restarts, but we'll
# have to look into that for speed (and maybe use the database
# to better track project restarts).
local_hub._get_local_hub_status (err, status) ->
if err
cb(err)
else
port = status['raw.port']
cb()
else
cb("unknown url type -- #{type}")
], (err) ->
winston.debug("target: setup proxy; time=#{misc.walltime(tm)} seconds")
if err
cb(err)
else
t = {host:host, port:port}
_target_cache[key] = t
# Set a ttl time bomb on this cache entry. The idea is to keep the cache not too big,
# but also if the user is suddenly granted permission to the project, or the project server
# is restarted, this should be reflected. Since there are dozens (at least) of hubs,
# and any could cause a project restart at any time, we just timeout this info after
# a few seconds. This helps enormously when there is a burst of requests.
setTimeout((()->delete _target_cache[key]), 1000*30)
cb(false, t)
)
#proxy = httpProxy.createProxyServer(ws:true)
http_proxy_server = http.createServer (req, res) ->
req_url = req.url.slice(program.base_url.length) # strip base_url for purposes of determining project location/permissions
if req_url == "/alive"
res.end('')
return
#buffer = httpProxy.buffer(req) # see http://stackoverflow.com/questions/11672294/invoking-an-asynchronous-method-inside-a-middleware-in-node-http-proxy
cookies = new Cookies(req, res)
remember_me = cookies.get(program.base_url + 'remember_me')
if not remember_me?
res.writeHead(500, {'Content-Type':'text/html'})
res.end("Please login to <a target='_blank' href='https://cloud.sagemath.com'>https://cloud.sagemath.com</a> with cookies enabled, then refresh this page.")
return
target remember_me, req_url, (err, location) ->
if err
winston.debug("proxy denied -- #{err}")
res.writeHead(500, {'Content-Type':'text/html'})
res.end("Access denied. Please login to <a target='_blank' href='https://cloud.sagemath.com'>https://cloud.sagemath.com</a> as a user with access to this project, then refresh this page.")
else
proxy = httpProxy.createProxyServer(ws:false, target:"http://#{location.host}:#{location.port}", timeout:0)
proxy.web(req, res)
http_proxy_server.listen(program.proxy_port, program.host)
_ws_proxy_servers = {}
http_proxy_server.on 'upgrade', (req, socket, head) ->
req_url = req.url.slice(program.base_url.length) # strip base_url for purposes of determining project location/permissions
target undefined, req_url, (err, location) ->
if err
winston.debug("websocket upgrade error -- this shouldn't happen since upgrade would only happen after normal thing *worked*. #{err}")
else
winston.debug("websocket upgrade -- ws://#{location.host}:#{location.port}")
t = "ws://#{location.host}:#{location.port}"
proxy = _ws_proxy_servers[t]
if not proxy?
winston.debug("websocket upgrade: not using cache")
proxy = httpProxy.createProxyServer(ws:true, target:t, timeout:0)
_ws_proxy_servers[t] = proxy
else
winston.debug("websocket upgrade: using cache")
proxy.ws(req, socket, head)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment