secure-scuttlebot classic
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}