secure-scuttlebot classic
at main 6.8 kB view raw
1var fs = require('fs') 2var http = require('http') 3var path = require('path') 4 5var DEFAULT_PORT = 8888 6var DEFAULT_HOST = 'localhost' 7var MIME_MAP = { 8 '.html': 'text/html; charset=utf-8', 9 '.css': 'text/css; charset=utf-8', 10 '.js': 'application/javascript; charset=utf-8', 11 '.json': 'application/json; charset=utf-8', 12 '.jpg': 'image/jpeg', 13 '.jpeg': 'image/jpeg', 14 '.png': 'image/png', 15 '.svg': 'image/svg+xml' 16} 17 18function getContentType (filePath) { 19 var ext = path.extname(filePath).toLowerCase() 20 return MIME_MAP[ext] || 'application/octet-stream' 21} 22 23function resolvePath (reqPath) { 24 var pathname = (reqPath || '/').split('?')[0] 25 if (!pathname || pathname === '/') pathname = '/index.html' 26 var relative = pathname.replace(/^\/+/, '') 27 if (!relative) relative = 'index.html' 28 if (relative.indexOf('..') !== -1) return null 29 return relative 30} 31 32exports.name = 'decent-ui' 33exports.version = '1.0.0' 34exports.manifest = {} 35 36exports.init = function (sbot, config) { 37 var decentDir = path.join(__dirname, '..', 'decent', 'build') 38 var cfg = config && config.decent ? config.decent : {} 39 console.log('decent-ui config:', JSON.stringify(cfg)) 40 var port = typeof cfg.port === 'number' ? cfg.port : DEFAULT_PORT 41 var host = DEFAULT_HOST 42 var wsCfg = config && config.ws ? config.ws : {} 43 var wsPort = typeof wsCfg.port === 'number' ? wsCfg.port : 8989 44 var wsHost = typeof cfg.wsHost === 'string' ? cfg.wsHost : null 45 var wsRemote = typeof cfg.wsRemote === 'string' ? cfg.wsRemote : null 46 if (!wsHost && typeof wsCfg.host === 'string') 47 wsHost = wsCfg.host 48 49 function splitHostPort (rawHost) { 50 if (!rawHost || typeof rawHost !== 'string') return null 51 if (rawHost[0] === '[') { 52 var end = rawHost.indexOf(']') 53 if (end === -1) return {host: rawHost, port: null} 54 var rest = rawHost.slice(end + 1) 55 if (rest[0] === ':' && /^\d+$/.test(rest.slice(1))) 56 return {host: rawHost.slice(0, end + 1), port: Number(rest.slice(1))} 57 return {host: rawHost, port: null} 58 } 59 if (/:\\d+$/.test(rawHost)) { 60 var lastColon = rawHost.lastIndexOf(':') 61 return {host: rawHost.slice(0, lastColon), port: Number(rawHost.slice(lastColon + 1))} 62 } 63 return {host: rawHost, port: null} 64 } 65 66 function respondNotFound (res) { 67 res.statusCode = 404 68 res.setHeader('Content-Type', 'text/plain; charset=utf-8') 69 res.end('Not found') 70 } 71 72 function respondInvalid (res) { 73 res.statusCode = 400 74 res.setHeader('Content-Type', 'text/plain; charset=utf-8') 75 res.end('Invalid request') 76 } 77 78 function getBaseHost (hostHeader) { 79 if (!hostHeader || typeof hostHeader !== 'string') return null 80 if (hostHeader[0] === '[') { 81 var end = hostHeader.indexOf(']') 82 if (end === -1) return hostHeader 83 return hostHeader.slice(0, end + 1) 84 } 85 var colon = hostHeader.indexOf(':') 86 if (colon === -1) return hostHeader 87 return hostHeader.slice(0, colon) 88 } 89 90 function getRemoteForRequest (req) { 91 if (!sbot || !sbot.id || typeof sbot.id !== 'string') return null 92 if (!req || !req.headers) return null 93 94 var hostHeader = req.headers['x-forwarded-host'] || req.headers.host 95 var baseHost = getBaseHost(hostHeader) 96 if (!baseHost) return null 97 98 var proto = 'http' 99 if (req.connection && req.connection.encrypted) 100 proto = 'https' 101 else if (typeof req.headers['x-forwarded-proto'] === 'string') 102 proto = req.headers['x-forwarded-proto'].split(',')[0].trim() 103 104 var wsProto = proto === 'https' ? 'wss' : 'ws' 105 106 var i = sbot.id.indexOf('.') 107 var key = i === -1 ? sbot.id.substring(1) : sbot.id.substring(1, i) 108 109 if (wsRemote) { 110 return wsRemote + '~shs:' + key 111 } 112 113 var wsTarget = wsHost || baseHost 114 var parsedHost = splitHostPort(wsTarget) 115 var hostName = parsedHost ? parsedHost.host : wsTarget 116 var hostPort = parsedHost && parsedHost.port ? parsedHost.port : wsPort 117 118 return wsProto + '://' + hostName + ':' + hostPort + '~shs:' + key 119 } 120 121 function serveStatic (req, res) { 122 if (req.method !== 'GET' && req.method !== 'HEAD') { 123 respondInvalid(res) 124 return 125 } 126 127 var relPath = resolvePath(req.url) 128 if (!relPath) { 129 respondInvalid(res) 130 return 131 } 132 133 var filePath = path.join(decentDir, relPath) 134 function serveFile (resolvedPath) { 135 if (relPath === 'index.html' && req.method === 'GET') { 136 return fs.readFile(resolvedPath, 'utf8', function (readErr, html) { 137 if (readErr) { 138 respondNotFound(res) 139 return 140 } 141 142 var headInsert = '' 143 if (html.indexOf('rel="stylesheet" href="/style.css"') === -1 && 144 html.indexOf('rel="stylesheet" href="style.css"') === -1) { 145 headInsert += '<link rel="preload" as="style" href="/style.css">' + 146 '<link rel="stylesheet" href="/style.css">' 147 } 148 149 var remote = getRemoteForRequest(req) 150 if (remote) { 151 headInsert += '<script>window.PATCHBAY_REMOTE = ' + 152 JSON.stringify(remote) + 153 ';</script>' 154 } 155 156 if (headInsert) 157 html = html.replace('</head>', headInsert + '</head>') 158 159 res.writeHead(200, {'Content-Type': getContentType(resolvedPath)}) 160 res.end(html) 161 }) 162 } 163 164 res.writeHead(200, {'Content-Type': getContentType(resolvedPath)}) 165 166 if (req.method === 'HEAD') { 167 res.end() 168 return 169 } 170 171 fs.createReadStream(resolvedPath).pipe(res) 172 } 173 174 fs.stat(filePath, function (err, stat) { 175 if (err || !stat || !stat.isFile()) { 176 if (relPath === 'style.css') { 177 var fallbackPath = path.join(__dirname, '..', 'decent', 'style.css') 178 return fs.stat(fallbackPath, function (fallbackErr, fallbackStat) { 179 if (fallbackErr || !fallbackStat || !fallbackStat.isFile()) { 180 respondNotFound(res) 181 return 182 } 183 serveFile(fallbackPath) 184 }) 185 } 186 respondNotFound(res) 187 return 188 } 189 190 serveFile(filePath) 191 }) 192 } 193 194 var server = http.createServer(serveStatic) 195 var startUrl = null 196 197 server.on('error', function (err) { 198 console.error('decent-ui server error:', err.message || err) 199 }) 200 201 server.listen(port, host, function () { 202 var addr = server.address() 203 var listeningPort = addr && addr.port ? addr.port : port 204 startUrl = 'http://' + host + ':' + listeningPort + '/' 205 console.log('Decent launched at ' + startUrl) 206 }) 207 208 var closed = false 209 function closeServer (cb) { 210 if (closed) return cb && cb() 211 closed = true 212 server.close(function () { 213 cb && cb() 214 }) 215 } 216 217 process.once('exit', closeServer) 218 219 return { 220 decent: { 221 port: port, 222 host: host 223 } 224 } 225}