├── .travis.yml ├── .zuul.yml ├── lib ├── crypto.js └── to-stream.js ├── test ├── crypto.js └── basic.js ├── example.js ├── LICENSE ├── package.json ├── README.md └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: tape 2 | browsers: 3 | - name: chrome 4 | version: latest 5 | - name: firefox 6 | version: latest 7 | - name: safari 8 | version: latest 9 | - name: ie 10 | version: latest 11 | - name: microsoftedge 12 | version: latest 13 | - name: android 14 | version: latest 15 | -------------------------------------------------------------------------------- /lib/crypto.js: -------------------------------------------------------------------------------- 1 | var aes = require('browserify-aes') 2 | var randombytes = require('randombytes') 3 | 4 | /** 5 | * The IV value is re-used across calls to cipher(), since it's used with a new key 6 | * each time. 7 | */ 8 | var IV = Buffer.alloc(16) 9 | 10 | exports.createCipher = function () { 11 | var key = randombytes(16) 12 | var cipher = aes.createCipheriv('aes-128-ctr', key, IV) 13 | cipher.key = key 14 | return cipher 15 | } 16 | 17 | exports.createDecipher = function (key) { 18 | return aes.createDecipheriv('aes-128-ctr', key, IV) 19 | } 20 | -------------------------------------------------------------------------------- /test/crypto.js: -------------------------------------------------------------------------------- 1 | // peerdb.io is down 2 | 3 | // var concat = require('simple-concat') 4 | // var crypto = require('../lib/crypto') 5 | // var randombytes = require('randombytes') 6 | // var test = require('tape') 7 | // var stream = require('readable-stream') 8 | 9 | // test('encrypt, decrypt', function (t) { 10 | // t.plan(2) 11 | 12 | // var cipher = crypto.createCipher() 13 | // var decipher = crypto.createDecipher(cipher.key) 14 | 15 | // var readable = stream.PassThrough() 16 | 17 | // var data = randombytes(10000) 18 | // readable.end(data) 19 | 20 | // var s = readable.pipe(cipher).pipe(decipher) 21 | 22 | // concat(s, function (err, buf) { 23 | // t.error(err) 24 | // t.deepEqual(data, buf) 25 | // }) 26 | // }) 27 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | // peerdb.io is down 2 | 3 | // var hat = require('hat') 4 | // var peerdb = require('../') 5 | // var test = require('tape') 6 | 7 | // test('put, get (buffer)', function (t) { 8 | // t.plan(6) 9 | // var value = Buffer('some data') 10 | // peerdb.put(value, function (err, key) { 11 | // t.error(err) 12 | // t.equal(typeof key, 'string') 13 | 14 | // peerdb.get(key, function (err, value2) { 15 | // t.error(err) 16 | // t.ok(Buffer.isBuffer(value)) 17 | // t.deepEqual(value, value2) 18 | // peerdb.close(t.error) 19 | // }) 20 | // }) 21 | // }) 22 | 23 | // test('get timeout', function (t) { 24 | // t.plan(3) 25 | // var key = hat(160) 26 | // peerdb.get(key, function (err, value) { 27 | // t.ok(err instanceof Error) 28 | // t.ok(err.message.indexOf('not found') !== -1) 29 | // peerdb.close(t.error) 30 | // }) 31 | // }) 32 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var peerdb = require('./') 2 | 3 | // Make it easier to play with this in the console 4 | if (typeof window !== 'undefined') { 5 | window.peerdb = peerdb 6 | window.Buffer = Buffer 7 | } 8 | 9 | // var value = Buffer('some data') 10 | 11 | // peerdb.put(value, function (err, key) { 12 | // if (err) throw err 13 | // console.log('put value as key: ' + key) 14 | // peerdb.get(key, function (err, value) { 15 | // if (err) throw err 16 | // console.log('got value: ' + value) 17 | // }) 18 | // }) 19 | 20 | var drop = require('drag-drop') 21 | 22 | drop('body', function (files) { 23 | console.log('drop!') 24 | peerdb.put(files[0], function (err, key) { 25 | if (err) throw err 26 | console.log('put value as key: ' + key) 27 | }) 28 | }) 29 | 30 | document.querySelector('html').style.width = '100%' 31 | document.querySelector('html').style.height = '100%' 32 | document.querySelector('html').style.margin = '0' 33 | 34 | document.querySelector('body').style.width = '100%' 35 | document.querySelector('body').style.height = '100%' 36 | document.querySelector('body').style.margin = '0' 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) WebTorrent, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/to-stream.js: -------------------------------------------------------------------------------- 1 | /* global Blob */ 2 | 3 | module.exports = toStream 4 | 5 | var FileReadStream = require('filestream/read') 6 | var fs = require('fs') 7 | var stream = require('readable-stream') 8 | 9 | function toStream (obj) { 10 | if (typeof Blob !== 'undefined' && obj instanceof Blob) { 11 | return fromBlob(obj) 12 | } 13 | if (Buffer.isBuffer(obj)) { 14 | return fromBuffer(obj) 15 | } 16 | if (typeof obj === 'string') { 17 | return fromFilePath(obj) 18 | } 19 | } 20 | 21 | /** 22 | * Convert a `File` to a lazy readable stream. 23 | * @param {File|Blob} file 24 | * @return {function} 25 | */ 26 | function fromBlob (file) { 27 | return new FileReadStream(file) 28 | } 29 | 30 | /** 31 | * Convert a `Buffer` to a lazy readable stream. 32 | * @param {Buffer} buffer 33 | * @return {function} 34 | */ 35 | function fromBuffer (buffer) { 36 | var s = new stream.PassThrough() 37 | s.end(buffer) 38 | return s 39 | } 40 | 41 | /** 42 | * Convert a file path to a lazy readable stream. 43 | * @param {string} path 44 | * @return {function} 45 | */ 46 | function fromFilePath (path) { 47 | return fs.createReadStream(path) 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peerdb", 3 | "description": "TODO", 4 | "version": "0.0.2", 5 | "author": { 6 | "name": "WebTorrent, LLC", 7 | "email": "feross@webtorrent.io", 8 | "url": "https://webtorrent.io" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/feross/peerdb/issues" 12 | }, 13 | "dependencies": { 14 | "browserify-aes": "^1.0.6", 15 | "filestream": "^4.1.3", 16 | "magnet-uri": "^5.1.3", 17 | "once": "^1.3.3", 18 | "pump": "^1.0.1", 19 | "randombytes": "^2.0.3", 20 | "readable-stream": "^2.1.4", 21 | "run-parallel": "^1.1.6", 22 | "simple-concat": "^1.0.0", 23 | "simple-get": "^2.2.1", 24 | "webtorrent": ">=0.107.6" 25 | }, 26 | "devDependencies": { 27 | "browserify": "^14.0.0", 28 | "hat": "0.0.3", 29 | "standard": "*", 30 | "tape": "^4.5.1", 31 | "uglify-js": "^2.6.2" 32 | }, 33 | "homepage": "https://github.com/feross/peerdb", 34 | "keywords": [ 35 | "db", 36 | "database", 37 | "serverless", 38 | "p2p", 39 | "peer to peer", 40 | "peer-to-peer", 41 | "peer assisted delivery", 42 | "cdn", 43 | "web seed", 44 | "webtorrent" 45 | ], 46 | "license": "MIT", 47 | "main": "index.js", 48 | "repository": { 49 | "type": "git", 50 | "url": "git://github.com/feross/peerdb.git" 51 | }, 52 | "scripts": { 53 | "size": "browserify . | uglifyjs -c -m | gzip | wc -c", 54 | "test": "standard && tape test/*.js" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # peerdb [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] 2 | 3 | [travis-image]: https://img.shields.io/travis/feross/peerdb/master.svg 4 | [travis-url]: https://travis-ci.org/feross/peerdb 5 | [npm-image]: https://img.shields.io/npm/v/peerdb.svg 6 | [npm-url]: https://npmjs.org/package/peerdb 7 | [downloads-image]: https://img.shields.io/npm/dm/peerdb.svg 8 | [downloads-url]: https://npmjs.org/package/peerdb 9 | 10 | ### TODO -- WORK IN PROGRESS 11 | 12 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/peerdb.svg)](https://saucelabs.com/u/peerdb) 13 | 14 | ## features 15 | 16 | - Encrypts files (by default) 17 | - Modes: central-only, P2P-only (free), hybrid (default) 18 | - Backed by MaxCDN 19 | - 100% open source client and server 20 | - Useful for "serverless websites" (i.e. no backend) 21 | 22 | ## why? 23 | 24 | - Simple API (compare to Amazon S3, CloudFront, requires server-side?) 25 | - Cheap 26 | - Using a trustless server to add availability to a P2P app 27 | - 100% of profits will go to the development of WebTorrent and WebTorrent Desktop 28 | 29 | ## install 30 | 31 | ``` 32 | npm install peerdb 33 | ``` 34 | 35 | ## usage 36 | 37 | It's super easy to store data: 38 | 39 | ```js 40 | var db = require('peerdb') 41 | 42 | db.put(Buffer('some data'), function (err, key) { 43 | // `key` is a unique identifier based on the data (content-addressed) 44 | db.get(key, function (err, value) { 45 | console.log(value) // 'some data' 46 | }) 47 | }) 48 | ``` 49 | 50 | To ensure that data remains accessible when no peers are online, store it 51 | on a centralized content delivery network (CDN): 52 | 53 | ```js 54 | var db = require('peerdb') 55 | 56 | db.setup({ 57 | apiKey: '...' 58 | }) 59 | 60 | db.put(Buffer('some data'), function (err, key) { 61 | // `key` is a unique identifier based on the data (content-addressed) 62 | db.get(key, function (err, value) { 63 | console.log(value) // 'some data' 64 | 65 | // Data can be deleted from the central server and the local database 66 | db.del(key, function (err) { 67 | // Data is deleted! 68 | }) 69 | }) 70 | }) 71 | ``` 72 | 73 | ## api 74 | 75 | TODO 76 | 77 | ## license 78 | 79 | MIT. Copyright (c) [WebTorrent, LLC](https://webtorrent.io). 80 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = PeerDB 2 | 3 | var concat = require('simple-concat') 4 | var crypto = require('./lib/crypto') 5 | var get = require('simple-get') 6 | var magnet = require('magnet-uri') 7 | var once = require('once') 8 | var parallel = require('run-parallel') 9 | var pump = require('pump') 10 | var stream = require('readable-stream') 11 | var toStream = require('./lib/to-stream') 12 | var WebTorrent = require('webtorrent') 13 | 14 | var ANNOUNCE = 'wss://tracker.btorrent.xyz' 15 | var TIMEOUT = 20000 16 | var UPLOAD_URL = 'http://peerdb.io/uploads/' 17 | 18 | function PeerDB () { 19 | this.destroyed = false 20 | this._client = new WebTorrent({ 21 | dht: false 22 | }) 23 | } 24 | 25 | PeerDB._getDefaultInstance = function () { 26 | if (!PeerDB._defaultInstance) { 27 | PeerDB._defaultInstance = new PeerDB() 28 | } 29 | return PeerDB._defaultInstance 30 | } 31 | 32 | PeerDB.put = function (value, cb) { 33 | var db = PeerDB._getDefaultInstance() 34 | db.put(value, cb) 35 | } 36 | 37 | PeerDB.get = function (key, cb) { 38 | var db = PeerDB._getDefaultInstance() 39 | db.get(key, cb) 40 | } 41 | 42 | PeerDB.del = function (key, cb) { 43 | var db = PeerDB._getDefaultInstance() 44 | db.del(key, cb) 45 | } 46 | 47 | PeerDB.close = function (cb) { 48 | var db = PeerDB._getDefaultInstance() 49 | PeerDB._defaultInstance = null 50 | db.close(cb) 51 | } 52 | 53 | PeerDB.prototype.put = function (value, cb) { 54 | this._checkOpen() 55 | if (!cb) cb = noop 56 | cb = once(cb) 57 | 58 | var opts = { 59 | announce: ANNOUNCE, 60 | name: 'PeerDB User Data' 61 | } 62 | 63 | var valueStream = toStream(value) 64 | var cipher = crypto.createCipher() 65 | 66 | var cipherStream = pump(valueStream, cipher, onError) 67 | 68 | var torrent = this._client.seed(cipherStream, opts) 69 | torrent.once('error', onError) 70 | torrent.once('seed', onSeed) 71 | 72 | function onSeed () { 73 | torrent.files[0].getBuffer(function (err, buf) { 74 | if (err) return cb(err) 75 | 76 | parallel([ 77 | // Upload torrent data 78 | function (cb) { 79 | get.concat({ 80 | method: 'POST', 81 | body: buf, 82 | url: UPLOAD_URL + '?key=' + torrent.infoHash 83 | }, function (err, res) { 84 | if (err) return cb(err) 85 | if (res.statusCode !== 200) { 86 | return cb(new Error('PeerDB: Upload failed with http response: ' + res.statusCode)) 87 | } 88 | cb(null) 89 | }) 90 | }, 91 | // Upload torrent metadata (.torrent file) 92 | function (cb) { 93 | get.concat({ 94 | method: 'POST', 95 | body: torrent.torrentFile, 96 | url: UPLOAD_URL + '?key=' + torrent.infoHash + '&torrent=true' 97 | }, function (err, res) { 98 | if (err) return cb(err) 99 | if (res.statusCode !== 200) { 100 | return cb(new Error('PeerDB: Upload failed with http response: ' + res.statusCode)) 101 | } 102 | cb(null) 103 | }) 104 | } 105 | ], function (err) { 106 | cb(err, torrent.infoHash + ':' + cipher.key.toString('hex')) 107 | }) 108 | }) 109 | } 110 | 111 | function onError (err) { 112 | if (err) cb(err) 113 | } 114 | } 115 | 116 | PeerDB.prototype.get = function (key, cb) { 117 | this._checkOpen() 118 | if (!cb) cb = noop 119 | cb = once(cb) 120 | 121 | var parts = key.split(':') 122 | key = parts[0] 123 | var cipherKey = parts[1] 124 | 125 | if (cipherKey) { 126 | cipherKey = Buffer.from(cipherKey, 'hex') 127 | } 128 | 129 | var torrent = this._client.get(key) 130 | if (!torrent) { 131 | var opts = { 132 | infoHash: key, 133 | announce: ANNOUNCE, 134 | urlList: UPLOAD_URL + key, 135 | xs: UPLOAD_URL + key + '.torrent' 136 | } 137 | 138 | torrent = this._client.add(magnet.encode(opts), opts) 139 | torrent.once('error', function (err) { 140 | cb(err) 141 | }) 142 | } 143 | 144 | if (torrent.ready) { 145 | onReady(torrent) 146 | } else { 147 | torrent.once('ready', function () { 148 | onReady(torrent) 149 | }) 150 | } 151 | 152 | var timeout = setTimeout(function () { 153 | cb(new Error('PeerDB: key "' + key + '" not found')) 154 | }, TIMEOUT) 155 | 156 | function onReady (torrent) { 157 | torrent.files[0].getBuffer(function (err, cipherText) { 158 | clearTimeout(timeout) 159 | if (err) return cb(err) 160 | 161 | var readable = stream.PassThrough() 162 | readable.end(cipherText) 163 | var decipher = crypto.createDecipher(cipherKey) 164 | 165 | var decipherStream = pump(readable, decipher, onError) 166 | 167 | concat(decipherStream, cb) 168 | }) 169 | } 170 | 171 | function onError (err) { 172 | if (err) cb(err) 173 | } 174 | } 175 | 176 | PeerDB.prototype.del = function (key, cb) { 177 | this._checkOpen() 178 | if (!cb) cb = noop 179 | cb = once(cb) 180 | 181 | this._client.remove(key) 182 | 183 | // TODO: delete from central server 184 | process.nextTick(function () { 185 | cb(null) 186 | }) 187 | } 188 | 189 | PeerDB.prototype.close = function (cb) { 190 | this._checkOpen() 191 | if (!cb) cb = noop 192 | cb = once(cb) 193 | 194 | this._client.destroy(function (err) { 195 | cb(err) 196 | }) 197 | 198 | this.destroyed = true 199 | this._client = null 200 | } 201 | 202 | PeerDB.prototype._checkOpen = function () { 203 | if (this.destroyed) { 204 | throw new Error('PeerDB: database is closed') 205 | } 206 | } 207 | 208 | function noop () {} 209 | --------------------------------------------------------------------------------