aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/app.js54
-rw-r--r--lib/render.js13
-rw-r--r--lib/serve.js97
3 files changed, 128 insertions, 36 deletions
diff --git a/lib/app.js b/lib/app.js
index 1705d1c..5cc4e62 100644
--- a/lib/app.js
+++ b/lib/app.js
@@ -14,6 +14,10 @@ var Git = require('./git')
var cat = require('pull-cat')
var proc = require('child_process')
var toPull = require('stream-to-pull-stream')
+var BoxStream = require('pull-box-stream')
+var crypto = require('crypto')
+
+var zeros = new Buffer(24); zeros.fill(0)
module.exports = App
@@ -198,8 +202,54 @@ App.prototype.wantSizeBlob = function (id, cb) {
})
}
-App.prototype.addBlob = function (cb) {
- return this.sbot.blobs.add(cb)
+App.prototype.addBlobRaw = function (cb) {
+ var done = multicb({pluck: 1, spread: true})
+ var sink = pull(
+ u.pullLength(done()),
+ this.sbot.blobs.add(done())
+ )
+ done(function (err, size, hash) {
+ if (err) return cb(err)
+ cb(null, {link: hash, size: size})
+ })
+ return sink
+}
+
+App.prototype.addBlob = function (isPrivate, cb) {
+ if (!isPrivate) return this.addBlobRaw(cb)
+ else return this.addBlobPrivate(cb)
+}
+
+App.prototype.addBlobPrivate = function (cb) {
+ var bufs = []
+ var self = this
+ // use the hash of the cleartext as the key to encrypt the blob
+ var hash = crypto.createHash('sha256')
+ return pull.drain(function (buf) {
+ bufs.push(buf)
+ hash.update(buf)
+ }, function (err) {
+ if (err) return cb(err)
+ var secret = hash.digest()
+ pull(
+ pull.values(bufs),
+ BoxStream.createBoxStream(secret, zeros),
+ self.addBlobRaw(function (err, link) {
+ if (err) return cb(err)
+ link.key = secret.toString('base64')
+ cb(null, link)
+ })
+ )
+ })
+}
+
+App.prototype.getBlob = function (id, key) {
+ if (!key) return this.sbot.blobs.get(id)
+ if (typeof key === 'string') key = new Buffer(key, 'base64')
+ return pull(
+ this.sbot.blobs.get(id),
+ BoxStream.createUnboxStream(key, zeros)
+ )
}
App.prototype.pushBlob = function (id, cb) {
diff --git a/lib/render.js b/lib/render.js
index d7fe04f..cc08259 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -136,7 +136,7 @@ Render.prototype.markdown = function (text, mentions) {
else if (link.host === 'http://localhost:7777')
mentionsObj[link.href] = link.link
if (link.link)
- mentionsByLink[link.link] = link
+ mentionsByLink[link.link + (link.key ? '#' + link.key : '')] = link
})
var out = marked(String(text), this.markedOpts)
delete this._mentions
@@ -147,6 +147,7 @@ Render.prototype.markdown = function (text, mentions) {
Render.prototype.imageUrl = function (ref) {
var m = /^blobstore:(.*)/.exec(ref)
if (m) ref = m[1]
+ ref = ref.replace(/#/, '%23')
return this.opts.img_base + 'image/' + ref
}
@@ -196,8 +197,14 @@ Render.prototype.toUrl = function (href) {
if (!u.isRef(href)) return false
return this.opts.base + href
case '&':
- if (!u.isRef(href)) return false
- return this.opts.blob_base + href
+ var parts = href.split('#')
+ var hash = parts.shift()
+ var key = parts.shift()
+ var fragment = parts.join('#')
+ if (!u.isRef(hash)) return false
+ return this.opts.blob_base + hash
+ + (key ? encodeURIComponent('#' + key) : '')
+ + (fragment ? '#' + fragment : '')
case '#': return this.opts.base + 'channel/' +
encodeURIComponent(href.substr(1))
case '/': return this.opts.base + href.substr(1)
diff --git a/lib/serve.js b/lib/serve.js
index 30689eb..839725a 100644
--- a/lib/serve.js
+++ b/lib/serve.js
@@ -27,7 +27,7 @@ module.exports = Serve
var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs')
var hlCssDir = path.join(require.resolve('highlight.js'), '../../styles')
-var urlIdRegex = /^(?:\/+(([%&@]|%25)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))(?:\.([^?]*))?|(\/.*?))(?:\?(.*))?$/
+var urlIdRegex = /^(?:\/+(([%&@]|%25)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))([^?]*)?|(\/.*?))(?:\?(.*))?$/
function ctype(name) {
switch (name && /[^.\/]*$/.exec(name)[0] || 'html') {
@@ -83,20 +83,18 @@ Serve.prototype.go = function () {
else data[name] = [data[name], value]
}
busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
- var done = multicb({pluck: 1, spread: true})
var cb = filesCb()
pull(
toPull(file),
- u.pullLength(done()),
- self.app.addBlob(done())
+ self.app.addBlob(!!data.private, function (err, link) {
+ if (err) return cb(err)
+ if (link.size === 0 && !filename) return cb()
+ link.name = filename
+ link.type = mimetype
+ addField(fieldname, link)
+ cb()
+ })
)
- done(function (err, size, id) {
- if (err) return cb(err)
- if (size === 0 && !filename) return cb()
- addField(fieldname,
- {link: id, name: filename, type: mimetype, size: size})
- cb()
- })
})
busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
addField(fieldname, val)
@@ -236,7 +234,7 @@ Serve.prototype.handle = function () {
case '%25': m[2] = '%'; m[1] = decodeURIComponent(m[1])
case '%': return this.id(m[1], m[3])
case '@': return this.userFeed(m[1], m[3])
- case '&': return this.blob(m[1])
+ case '&': return this.blob(m[1], m[3])
default: return this.path(m[4])
}
}
@@ -970,7 +968,7 @@ function threadHeads(msgs, rootId) {
}
-Serve.prototype.id = function (id, ext) {
+Serve.prototype.id = function (id, path) {
var self = this
if (self.query.raw != null) return self.rawId(id)
@@ -1006,16 +1004,14 @@ Serve.prototype.id = function (id, ext) {
channel: channel,
}),
self.wrapPage(id),
- self.respondSink(200, {
- 'Content-Type': ctype(ext),
- })
+ self.respondSink(200)
)
})
)
})
}
-Serve.prototype.userFeed = function (id, ext) {
+Serve.prototype.userFeed = function (id, path) {
var self = this
var q = self.query
var opts = {
@@ -1035,9 +1031,7 @@ Serve.prototype.userFeed = function (id, ext) {
self.wrapMessages(),
self.wrapUserFeed(isScrolled, id),
self.wrapPage(about.name || id),
- self.respondSink(200, {
- 'Content-Type': ctype(ext)
- })
+ self.respondSink(200)
)
})
}
@@ -1070,17 +1064,33 @@ Serve.prototype.highlight = function (dirs) {
this.file(path.join(hlCssDir, dirs))
}
-Serve.prototype.blob = function (id) {
+Serve.prototype.blob = function (id, path) {
var self = this
- var blobs = self.app.sbot.blobs
- if (self.req.headers['if-none-match'] === id) return self.respond(304)
+ var etag = id + (path || '')
+ if (self.req.headers['if-none-match'] === etag) return self.respond(304)
+ var key
+ if (path) {
+ path = decodeURIComponent(path)
+ if (path[0] === '#') {
+ try {
+ key = new Buffer(path.substr(1), 'base64')
+ } catch(err) {
+ return self.respond(400, err.message)
+ }
+ if (key.length !== 32) {
+ return self.respond(400, 'Bad blob key')
+ }
+ } else {
+ return self.respond(400, 'Bad blob request')
+ }
+ }
self.app.wantSizeBlob(id, function (err, size) {
if (err) {
if (/^invalid/.test(err.message)) return self.respond(400, err.message)
else return self.respond(500, err.message || err)
}
pull(
- blobs.get(id),
+ self.app.getBlob(id, key),
pull.map(Buffer),
ident(gotType),
self.respondSink()
@@ -1088,22 +1098,41 @@ Serve.prototype.blob = function (id) {
function gotType(type) {
type = type && mime.lookup(type)
if (type) self.res.setHeader('Content-Type', type)
- if (typeof size === 'number') self.res.setHeader('Content-Length', size)
+ // don't serve size for encrypted blob, because it refers to the size of
+ // the ciphertext
+ if (typeof size === 'number' && !key)
+ self.res.setHeader('Content-Length', size)
if (self.query.name) self.res.setHeader('Content-Disposition',
'inline; filename='+encodeDispositionFilename(self.query.name))
self.res.setHeader('Cache-Control', 'public, max-age=315360000')
- self.res.setHeader('etag', id)
+ self.res.setHeader('etag', etag)
self.res.writeHead(200)
}
})
}
Serve.prototype.image = function (path) {
- var id = path.substr(1)
- var etag = 'image-' + id
var self = this
- var blobs = self.app.sbot.blobs
+ var id, key
+ var m = urlIdRegex.exec(path)
+ if (m && m[2] === '&') id = m[1], path = m[3]
+ var etag = 'image-' + id + (path || '')
if (self.req.headers['if-none-match'] === etag) return self.respond(304)
+ if (path) {
+ path = decodeURIComponent(path)
+ if (path[0] === '#') {
+ try {
+ key = new Buffer(path.substr(1), 'base64')
+ } catch(err) {
+ return self.respond(400, err.message)
+ }
+ if (key.length !== 32) {
+ return self.respond(400, 'Bad blob key')
+ }
+ } else {
+ return self.respond(400, 'Bad blob request')
+ }
+ }
self.app.wantSizeBlob(id, function (err, size) {
if (err) {
if (/^invalid/.test(err.message)) return self.respond(400, err.message)
@@ -1115,7 +1144,7 @@ Serve.prototype.image = function (path) {
var heresTheType = done().bind(self, null)
pull(
- blobs.get(id),
+ self.app.getBlob(id, key),
pull.map(Buffer),
ident(heresTheType),
pull.collect(onFullBuffer)
@@ -2076,6 +2105,7 @@ Serve.prototype.wrapThread = function (opts) {
branches: opts.branches,
postBranches: opts.postBranches,
recps: recps,
+ private: opts.recps != null,
}, function (err, composer) {
if (err) return cb(err)
cb(null, [
@@ -2215,6 +2245,7 @@ Serve.prototype.composer = function (opts, cb) {
blobs[data.upload.link] = {
type: data.upload.type,
size: data.upload.size,
+ key: data.upload.key,
}
}
if (data.blob_type && blobs[data.blob_link]) {
@@ -2243,11 +2274,13 @@ Serve.prototype.composer = function (opts, cb) {
}
if (data.upload) {
+ var href = data.upload.link
+ + (data.upload.key ? '#' + data.upload.key : '')
// TODO: be able to change the content-type
var isImage = /^image\//.test(data.upload.type)
data.text = (data.text ? data.text + '\n' : '')
+ (isImage ? '!' : '')
- + '[' + data.upload.name + '](' + data.upload.link + ')'
+ + '[' + data.upload.name + '](' + href + ')'
}
// get bare feed names
@@ -2342,6 +2375,8 @@ Serve.prototype.composer = function (opts, cb) {
h('table.ssb-msgs',
h('tr.msg-row',
h('td.msg-left', {colspan: 2},
+ opts.private ?
+ h('input', {type: 'hidden', name: 'private', value: '1'}) : '',
h('input', {type: 'file', name: 'upload'})
),
h('td.msg-right',