fs = require('fs')
path_module = require('path')
Cookies = require('cookies')
util = require('util')
ms = require('ms')
async = require('async')
cookieParser = require('cookie-parser')
body_parser = require('body-parser')
express = require('express')
formidable = require('formidable')
http_proxy = require('http-proxy')
http = require('http')
winston = require('winston')
misc = require('smc-util/misc')
{defaults, required} = misc
misc_node = require('smc-util-node/misc_node')
hub_register = require('./hub_register')
auth = require('./auth')
access = require('./access')
hub_proxy = require('./proxy')
hub_projects = require('./projects')
MetricsRecorder = require('./metrics-recorder')
{http_message_api_v1} = require('./api/handler')
{stripe_render_invoice} = require('./stripe/invoice')
SMC_ROOT = process.env.SMC_ROOT
STATIC_PATH = path_module.join(SMC_ROOT, 'static')
exports.init_express_http_server = (opts) ->
opts = defaults opts,
base_url : required
dev : false
database : required
compute_server : required
winston.debug("initializing express http server")
winston.debug("MATHJAX_URL = ", misc_node.MATHJAX_URL)
router = express.Router()
app = express()
app.use(cookieParser())
router.use(body_parser.json())
router.use(body_parser.urlencoded({ extended: true }))
response_time_quantile = MetricsRecorder.new_quantile('http_quantile', 'http server',
percentiles : [0, 0.5, 0.75, 0.9, 0.99, 1]
labels: ['path', 'method', 'code']
)
response_time_histogram = MetricsRecorder.new_histogram('http_histogram', 'http server'
buckets : [0.0001, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.1, 0.5, 1, 5, 10]
labels: ['path', 'method', 'code']
)
router.use (req, res, next) ->
res_finished_q = response_time_quantile.startTimer()
res_finished_h = response_time_histogram.startTimer()
original_end = res.end
res.end = ->
original_end.apply(res, arguments)
{dirname} = require('path')
dir_path = dirname(req.path).split('/')[1]
res_finished_q({path:dir_path, method:req.method, code:res.statusCode})
res_finished_h({path:dir_path, method:req.method, code:res.statusCode})
next()
app.enable('trust proxy')
cacheLongTerm = (res, path) ->
if not opts.dev
timeout = ms('100 days')
res.setHeader('Cache-Control', "public, max-age='#{timeout}'")
res.setHeader('Expires', new Date(Date.now() + timeout).toUTCString());
router.use '/static',
express.static(STATIC_PATH, setHeaders: cacheLongTerm)
router.use '/policies',
express.static(path_module.join(STATIC_PATH, 'policies'), {maxAge: 0})
router.use '/doc',
express.static(path_module.join(STATIC_PATH, 'doc'), {maxAge: 0})
router.get '/', (req, res) ->
has_remember_me = req.cookies[opts.base_url + 'has_remember_me']
if has_remember_me == 'true'
res.redirect(opts.base_url + '/app')
else
res.sendFile(path_module.join(STATIC_PATH, 'index.html'), {maxAge: 0})
router.get '/app', (req, res) ->
res.sendFile(path_module.join(STATIC_PATH, 'app.html'), {maxAge: 0})
router.get '/base_url.js', (req, res) ->
res.send("window.app_base_url='#{opts.base_url}';")
router.get '/alive', (req, res) ->
if not hub_register.database_is_working()
winston.debug("alive: answering *NO*")
res.status(404).end()
else
res.send('alive')
router.get '/metrics', (req, res) ->
res.header("Content-Type", "text/plain")
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate')
metricsRecorder = MetricsRecorder.get()
if metricsRecorder?
res.send(metricsRecorder.metrics())
else
res.send(JSON.stringify(error:'Metrics recorder not initialized.'))
router.get '/concurrent-warn', (req, res) ->
c = opts.database.concurrent()
if not hub_register.database_is_working() or c >= opts.database._concurrent_warn
winston.debug("/concurrent: not healthy, since concurrent >= #{opts.database._concurrent_warn}")
res.status(404).end()
else
res.send("#{c}")
router.get '/concurrent', (req, res) ->
res.send("#{opts.database.concurrent()}")
router.post '/api/v1/*', (req, res) ->
h = req.header('Authorization')
if not h?
res.status(400).send(error:'You must provide authentication via an API key.')
return
[type, user] = misc.split(h)
switch type
when "Bearer"
api_key = user
when "Basic"
api_key = new Buffer.from(user, 'base64').toString().split(':')[0]
else
res.status(400).send(error:"Unknown authorization type '#{type}'")
return
http_message_api_v1
event : req.path.slice(req.path.lastIndexOf('/') + 1)
body : req.body
api_key : api_key
logger : winston
database : opts.database
compute_server : opts.compute_server
ip_address : req.ip
cb : (err, resp) ->
if err
res.status(400).send(error:err)
else
res.send(resp)
stripe_connections = require('./stripe/connect').get_stripe()
if stripe_connections?
router.get '/invoice/*', (req, res) ->
winston.debug("/invoice/* (hub --> client): #{misc.to_json(req.query)}, #{req.path}")
path = req.path.slice(req.path.lastIndexOf('/') + 1)
i = path.lastIndexOf('-')
if i != -1
path = path.slice(i+1)
i = path.lastIndexOf('.')
if i == -1
res.status(404).send("invoice must end in .pdf")
return
invoice_id = path.slice(0,i)
winston.debug("id='#{invoice_id}'")
stripe_render_invoice(stripe_connections, invoice_id, true, res)
else
router.get '/invoice/*', (req, res) ->
res.status(404).send("stripe not configured")
router.get '/blobs/*', (req, res) ->
if not misc.is_valid_uuid_string(req.query.uuid)
res.status(404).send("invalid uuid=#{req.query.uuid}")
return
if not hub_register.database_is_working()
res.status(404).send("can't get blob -- not connected to database")
return
opts.database.get_blob
uuid : req.query.uuid
cb : (err, data) ->
if err
res.status(500).send("internal error: #{err}")
else if not data?
res.status(404).send("blob #{req.query.uuid} not found")
else
filename = req.path.slice(req.path.lastIndexOf('/') + 1)
if req.query.download?
res.attachment(filename)
else
res.type(filename)
res.send(data)
router.get '/cookies', (req, res) ->
if req.query.set
expires = new Date(new Date().getTime() + 1000*24*3600*30*36)
cookies = new Cookies(req, res)
cookies.set(req.query.set, req.query.value, {expires:expires})
res.end()
router.get '/registration', (req, res) ->
if not hub_register.database_is_working()
res.json({error:"not connected to database"})
return
opts.database.get_server_setting
name : 'account_creation_token'
cb : (err, token) ->
if err or not token
res.json({})
else
res.json({token:true})
router.get '/customize', (req, res) ->
if not hub_register.database_is_working()
res.json({error:"not connected to database"})
return
opts.database.get_site_settings
cb : (err, settings) ->
if err or not settings
res.json({})
else
res.json(settings)
router.get ['/projects*', '/help*', '/settings*'], (req, res) ->
url = require('url')
q = url.parse(req.url, true).search
res.redirect(opts.base_url + "/app#" + req.path.slice(1) + q)
router.get '/stats', (req, res) ->
if not hub_register.database_is_working()
res.json({error:"not connected to database"})
return
opts.database.get_stats
update : false
cb : (err, stats) ->
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate')
if err
res.status(500).send("internal error: #{err}")
else
res.header("Content-Type", "application/json")
res.send(JSON.stringify(stats, null, 1))
if opts.base_url
app.use(opts.base_url, router)
else
app.use(router)
if opts.dev
proxy_cache = {}
dev_proxy_port = (req, res) ->
req_url = req.url.slice(opts.base_url.length)
{key, port_number, project_id} = hub_proxy.target_parse_req('', req_url)
proxy = proxy_cache[key]
if proxy?
proxy.web(req, res)
return
winston.debug("proxy port: req_url='#{req_url}', port='#{port_number}'")
get_port = (cb) ->
if port_number == 'jupyter'
hub_proxy.jupyter_server_port
project_id : project_id
compute_server : opts.compute_server
database : opts.database
cb : cb
else
cb(undefined, port_number)
get_port (err, port) ->
winston.debug("get_port: port='#{port}'")
if err
res.status(500).send("internal error: #{err}")
else
target = "http://localhost:#{port}"
proxy = http_proxy.createProxyServer(ws:false, target:target, timeout:0)
proxy_cache[key] = proxy
proxy.on("error", -> delete proxy_cache[key])
setTimeout((-> delete proxy_cache[key]), 10000)
proxy.web(req, res)
port_regexp = '^' + opts.base_url + '\/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\/port\/*'
app.get( port_regexp, dev_proxy_port)
app.post(port_regexp, dev_proxy_port)
dev_proxy_raw = (req, res) ->
req_url = req.url.slice(opts.base_url.length)
{key, project_id} = hub_proxy.target_parse_req('', req_url)
winston.debug("dev_proxy_raw", project_id)
proxy = proxy_cache[key]
if proxy?
proxy.web(req, res)
return
opts.compute_server.project
project_id : project_id
cb : (err, project) ->
if err
res.status(500).send("internal error: #{err}")
else
project.status
cb : (err, status) ->
if err
res.status(500).send("internal error: #{err}")
else if not status['raw.port']
res.status(500).send("no raw server listening")
else
port = status['raw.port']
target = "http://localhost:#{port}"
proxy = http_proxy.createProxyServer(ws:false, target:target, timeout:0)
proxy_cache[key] = proxy
proxy.on("error", -> delete proxy_cache[key])
proxy.web(req, res)
setTimeout((-> delete proxy_cache[key]), 1000*60*60)
raw_regexp = '^' + opts.base_url + '\/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\/raw*'
app.get( raw_regexp, dev_proxy_raw)
app.post(raw_regexp, dev_proxy_raw)
app.on 'upgrade', (req, socket, head) ->
winston.debug("\n\n*** http_server websocket(#{req.url}) ***\n\n")
req_url = req.url.slice(opts.base_url.length)
http_server = http.createServer(app)
return {http_server:http_server, express_router:router}