Files: 5a3cd636ffd4211ee7958de59fc2011a7d6f58eb / confirm.js
7030 bytesRaw
1 | var proc = require('child_process') |
2 | var fs = require('fs') |
3 | var http = require('http') |
4 | var URL = require('url') |
5 | var crypto = require('crypto') |
6 | var os = require('os') |
7 | var qs = require('querystring') |
8 | |
9 | function escapeHTML (html) { |
10 | return String(html) |
11 | .replace(/&/g, '&') |
12 | .replace(/"/g, '"') |
13 | .replace(/</g, '<') |
14 | .replace(/>/g, '>') |
15 | .replace(/\n/g, '
') |
16 | } |
17 | |
18 | function noPublish(content, cb) { |
19 | cb(new Error('missing publish function')) |
20 | } |
21 | |
22 | function noPrivatePublish(content, recps, cb) { |
23 | cb(new Error('missing private publish function')) |
24 | } |
25 | |
26 | function getForm(stream, cb) { |
27 | var bufs = [] |
28 | stream.on('data', function (buf) { |
29 | bufs.push(buf) |
30 | }) |
31 | stream.on('end', function () { |
32 | var data |
33 | try { |
34 | data = qs.parse(Buffer.concat(bufs).toString('utf8')) |
35 | } catch(e) { |
36 | return cb(e) |
37 | } |
38 | cb(null, data) |
39 | }) |
40 | } |
41 | |
42 | function serve(req, res, ctxs) { |
43 | var q = req.uri.query |
44 | var opts = ctxs[q.id] |
45 | if (!opts) { |
46 | res.writeHead(404) |
47 | return res.end('Not Found') |
48 | } |
49 | |
50 | var content = opts.content |
51 | var browser = opts.browser |
52 | var recps = opts.recps |
53 | var publish = opts.publish || noPublish |
54 | var getUrl = opts.getUrl |
55 | var privatePublish = opts.privatePublish || noPrivatePublish |
56 | var redirectBase = opts.redirectBase |
57 | var ctxs = opts.ctxs |
58 | var url = opts.url |
59 | var cb = opts.cb |
60 | |
61 | if (req.method === 'POST') return getForm(req, function (err, data) { |
62 | if (err) return res.end(err.stack || err) |
63 | |
64 | if (!data.token || data.token !== opts.token) { |
65 | res.writeHead(403) |
66 | return res.end('Invalid token') |
67 | } |
68 | |
69 | function publish1(content, recps, cb) { |
70 | if (recps) privatePublish(content, recps, cb) |
71 | else publish(content, cb) |
72 | } |
73 | |
74 | if (data.publish) { |
75 | return publish1(content, recps, function (err, msg) { |
76 | if (err) { |
77 | res.writeHead(500, {'Content-Type': 'text/html'}) |
78 | res.end('<!doctype html><html><head><meta charset=utf-8>' |
79 | + '<title>publish error</title></head><body>' |
80 | + '<h1>' + escapeHTML(err.name || err) + '</h1>' |
81 | + '<pre style="white-space: pre-wrap">' + escapeHTML(err.stack || '') + '</pre>' |
82 | + '</body></html>') |
83 | delete ctxs[q.id] |
84 | return !getUrl && cb(err) |
85 | } |
86 | var json = JSON.stringify(msg, 0, 2) |
87 | if (redirectBase) { |
88 | var url = redirectBase + encodeURIComponent(msg.key) |
89 | res.writeHead(302, {'Location': url}) |
90 | res.end() |
91 | } else { |
92 | res.writeHead(200, {'Content-Type': 'text/html'}) |
93 | res.end('<!doctype html><html><head><meta charset=utf-8>' |
94 | + '<title>published</title></head><body>' |
95 | + '<pre style="white-space: pre-wrap">' + escapeHTML(json) + '</pre>' |
96 | + '</body></html>') |
97 | } |
98 | delete ctxs[q.id] |
99 | return !getUrl && cb(null, msg) |
100 | }) |
101 | } |
102 | if (data.cancel) { |
103 | res.writeHead(200, {'Content-Type': 'text/html'}) |
104 | res.end('<!doctype html><html><head><meta charset=utf-8>' |
105 | + '<title>canceled</title></head><body>' |
106 | + '<p>Cancelled</p>' |
107 | + '</body></html>') |
108 | delete ctxs[q.id] |
109 | return !getUrl && cb(new Error('User cancelled')) |
110 | } |
111 | }) |
112 | |
113 | if (!opts.token) opts.token = crypto.randomBytes(32).toString('base64') |
114 | var contentJson = JSON.stringify(content, null, 2) |
115 | var recpsJson = JSON.stringify(recps, null, 2) |
116 | res.writeHead(200, {'Content-Type': 'text/html'}) |
117 | return res.end('<!doctype html><html><head><meta charset=utf-8>' |
118 | + '<title>publish</title></head><body>' |
119 | + '<form action="" method="post">' |
120 | + (recps ? '<pre style="white-space: pre-wrap">' + recpsJson + '</pre>' : '') |
121 | + '<pre style="white-space: pre-wrap">' + escapeHTML(contentJson) + '</pre>' |
122 | + '<input type="hidden" name="id" value="' + escapeHTML(q.id) + '">' |
123 | + '<input type="hidden" name="token" value="' + escapeHTML(opts.token) + '">' |
124 | + '<input type="submit" name="publish" value="Publish"> ' |
125 | + '<input type="submit" name="cancel" value="Cancel">' |
126 | + '</form></body></html>') |
127 | } |
128 | |
129 | function confirm(opts, cb) { |
130 | var browser = opts.browser |
131 | var recps = opts.recps |
132 | var getUrl = opts.getUrl |
133 | var ctxs = opts.ctxs |
134 | var url = opts.url |
135 | |
136 | if (typeof cb !== 'function') throw new TypeError('bad callback') |
137 | if (!opts.ctxs) throw new TypeError('missing ctxs') |
138 | if (!opts.url) throw new TypeError('missing url') |
139 | |
140 | if (browser && getUrl) { |
141 | return cb(new TypeError('browser option and getUrl option conflict')) |
142 | } |
143 | if (!browser && !getUrl) { |
144 | return cb(new TypeError('missing browser option')) |
145 | } |
146 | if (recps && !Array.isArray(recps)) { |
147 | return cb(new TypeError('recps should be array')) |
148 | } |
149 | |
150 | var id = crypto.randomBytes(16).toString('base64') |
151 | ctxs[id] = opts |
152 | var reqUrl = opts.url + '?id=' + encodeURIComponent(id) |
153 | if (browser) { |
154 | proc.spawn(browser, [reqUrl], {stdio: 'inherit'}) |
155 | opts.cb = cb |
156 | } else cb(null, reqUrl) |
157 | } |
158 | |
159 | module.exports = function (opts) { |
160 | var publish = opts.publish |
161 | var privatePublish = opts.privatePublish |
162 | var config = opts.config || {} |
163 | var browser = config.browser || |
164 | (os.platform() === 'darwin' ? 'open' : 'xdg-open') |
165 | var name = opts.name || 'confirm' |
166 | var port = config.port || 0 |
167 | |
168 | var ctxs = {} |
169 | var url |
170 | if (opts.ws && opts.ws.use) { |
171 | var wsPort = opts.wsConfig && opts.wsConfig.port || 8989 |
172 | var prefix = '/publishguard' |
173 | url = 'http://localhost' + ':' + wsPort + prefix |
174 | opts.ws.use(function (req, res, next) { |
175 | req.uri = URL.parse(req.url, true) |
176 | if (req.uri.pathname !== prefix) return next() |
177 | serve(req, res, ctxs) |
178 | }) |
179 | } else { |
180 | var server = http.createServer(function (req, res) { |
181 | req.uri = URL.parse(req.url, true) |
182 | if (req.uri.pathname !== '/') { |
183 | res.writeHead(404) |
184 | return res.end('Not Found') |
185 | } |
186 | serve(req, res, ctxs) |
187 | }).listen(port, '127.0.0.1', function () { |
188 | if (port === 0) port = this.address().port |
189 | url = 'http://127.0.0.1:' + port + '/' |
190 | console.log('[publishguard] listening on ' + url) |
191 | }) |
192 | } |
193 | |
194 | return { |
195 | publish: function (content, cb) { |
196 | confirm({ |
197 | publish: publish, |
198 | content: content, |
199 | browser: browser, |
200 | ctxs: ctxs, |
201 | url: url, |
202 | }, cb) |
203 | }, |
204 | privatePublish: function (content, recps, cb) { |
205 | confirm({ |
206 | privatePublish: privatePublish, |
207 | recps: recps, |
208 | content: content, |
209 | browser: browser, |
210 | ctxs: ctxs, |
211 | url: url, |
212 | }, cb) |
213 | }, |
214 | publishGetUrl: function (opts, cb) { |
215 | confirm({ |
216 | publish: publish, |
217 | content: opts.content, |
218 | redirectBase: opts.redirectBase, |
219 | getUrl: true, |
220 | ctxs: ctxs, |
221 | url: url, |
222 | }, cb) |
223 | }, |
224 | privatePublishGetUrl: function (opts, cb) { |
225 | confirm({ |
226 | privatePublish: privatePublish, |
227 | recps: opts.recps, |
228 | content: opts.content, |
229 | redirectBase: opts.redirectBase, |
230 | getUrl: true, |
231 | ctxs: ctxs, |
232 | url: url, |
233 | }, cb) |
234 | } |
235 | } |
236 | } |
237 |
Built with git-ssb-web