├── .coveralls.yml ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example.js ├── index.js ├── lib ├── is-node.js ├── logger.js └── url.js ├── package.json └── tests ├── integration.test.js ├── random-access-http.test.js └── valid-url.test.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: Ol7Ap8LS2ER32hLdx9evIMFgCSChJNueO 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": ["standard"] } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Libraries don't benefit from lock files 40 | package-lock.json 41 | yarn.lock 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 6 5 | - 8 6 | - 9 7 | script: 8 | - npm run test-travis 9 | after_script: 10 | - npm run report-coverage 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Bret Comnes and contributors 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # random-access-http 2 | 3 | Continuous reading from a http(s) url using random offsets and lengths 4 | 5 | ``` 6 | npm install random-access-http 7 | ``` 8 | 9 | [![Build Status](https://travis-ci.org/random-access-storage/random-access-http.svg?branch=master)](https://travis-ci.org/random-access-storage/random-access-http) [![Coverage Status](https://coveralls.io/repos/github/random-access-storage/random-access-http/badge.svg?branch=master)](https://coveralls.io/github/random-access-storage/random-access-http?branch=master) 10 | 11 | ## Why? 12 | 13 | Peers in a distributed system tend to come and go over a short period of time in many common p2p scenarios, especially when you are giving away a file without incentivizing the swarm to seed the file for a long time. There are also an abundance of free cloud hosts that let you host large files over http. 14 | 15 | This module provides you random access to a file hosted over http so that it can be used by a client in a distributed system (such as hypercore or hyperdrive) to acquire parts of the file for itself and the other peers in the swarm. 16 | 17 | ## Usage 18 | 19 | ```js 20 | var randomAccessHTTP = require('random-access-http') 21 | 22 | var file = randomAccessHTTP('http://example.com/somefile.mp4') 23 | 24 | // Read 10 bytes at an offset of 5 25 | file.read(5, 10, function(err, buffer) { 26 | console.log(buffer) 27 | file.close(function() { 28 | console.log('http keepalive agents and sockets destroyed') 29 | }) 30 | }) 31 | ``` 32 | 33 | file will use a keepalive agent to reduce the number http requests needed for the session. When you are done you should call `file.close()` to destroy the agent. 34 | 35 | ## API 36 | 37 | #### `var file = randomAccessHTTP(url, [options])` 38 | 39 | Create a new 'file' that reads from the provided `url`. The `url` can be either `http`, `https` or a relative path if url is set in options. 40 | 41 | Options include: 42 | ```js 43 | { 44 | url: string // Optionsal. The base url if first argument is relative 45 | verbose: boolean, // Optional. Default: false. 46 | timeout: number, // Optional. Default: 60000 47 | maxRedirects: number, // Optional. Default: 10 48 | maxContentLength: number, // Optional. Default: 50MB 49 | strict: true, // When false, will accept non-ranged response (it will slice the response to the requested offset/length) 50 | } 51 | ``` 52 | 53 | #### `file.write(offset, buffer, [callback])` 54 | 55 | **Not implemented!** Please let us know if you have opinions on how to implement this. 56 | This will silently fail with no data being writen. 57 | 58 | #### `file.read(offset, length, callback)` 59 | 60 | Read a buffer at a specific offset. Callback is called with the buffer read. 61 | By default, this will fail if the server returns byte ranges different than what is requested. 62 | If you want to support uncooperative static file servers (that doesn't use ranges), pass the `strict` with a falsy value. 63 | 64 | #### `file.close([callback])` 65 | 66 | Close outstanding http keepalive agent sockets. 67 | 68 | #### `file.on('open')` 69 | 70 | Emitted when the url has been checked to support range requests and the keep-alive agent has been created. 71 | 72 | #### `file.on('close')` 73 | 74 | Emitted after the keepalive agent and its associated sockets have been destroyed. 75 | 76 | ## See Also 77 | 78 | - [bittorrent.org/beps/bep_0019 WebSeed - HTTP/FTP Seeding (GetRight style)](http://www.bittorrent.org/beps/bep_0019.html) 79 | - [bittorrent.org/beps/bep_0017 HTTP Seeding (Hoffman-style)](http://www.bittorrent.org/beps/bep_0017.html) 80 | - [RFC2616 14.35.1 Byte Ranges](http://tools.ietf.org/html/rfc2616#section-14.35) 81 | - [random-access-file](https://github.com/mafintosh/random-access-file) 82 | - [random-access-memory](https://github.com/mafintosh/random-access-memory) 83 | - [hypercore](https://github.com/mafintosh/hypercore) 84 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var raHttp = require('./') 2 | 3 | var file = raHttp('/readme.md', { url: 'https://raw.githubusercontent.com/e-e-e/http-random-access/master/' }) 4 | 5 | file.read(2, 10, (err, data) => { 6 | if (err) { 7 | console.log('Something went wrong!') 8 | console.log(err) 9 | return 10 | } 11 | console.log(data.toString()) 12 | }) 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var axios = require('axios') 2 | var randomAccess = require('random-access-storage') 3 | var logger = require('./lib/logger') 4 | var isNode = require('./lib/is-node') 5 | var { validUrl, prependUrlProtocol } = require('./lib/url') 6 | 7 | var defaultOptions = { 8 | responseType: 'arraybuffer', 9 | timeout: 60000, 10 | maxRedirects: 10, // follow up to 10 HTTP 3xx redirects 11 | maxContentLength: 50 * 1000 * 1000 // cap at 50MB, 12 | } 13 | 14 | var randomAccessHttp = function (filename, options) { 15 | if (!options) options = {} 16 | 17 | var url = prependUrlProtocol(options.url) 18 | 19 | if (!url) filename = prependUrlProtocol(filename) 20 | if (!filename || (!validUrl(filename) && !validUrl(url))) { 21 | throw new Error('Expect first argument to be a valid URL or a relative path, with url set in options') 22 | } 23 | 24 | // If this is falsy, raHttp will read non-ranged data 25 | var strict = true 26 | var axiosConfig = Object.assign({}, defaultOptions) 27 | if (isNode) { 28 | var http = require('http') 29 | var https = require('https') 30 | // keepAlive pools and reuses TCP connections, so it's faster 31 | axiosConfig.httpAgent = new http.Agent({ keepAlive: true }) 32 | axiosConfig.httpsAgent = new https.Agent({ keepAlive: true }) 33 | } 34 | if (options) { 35 | if (url) axiosConfig.baseURL = url 36 | if (options.timeout) axiosConfig.timeout = options.timeout 37 | if (options.maxRedirects) axiosConfig.maxRedirects = options.maxRedirects 38 | if (options.maxContentLength) axiosConfig.maxContentLength = options.maxContentLength 39 | if (options.strict !== undefined) strict = options.strict 40 | } 41 | var _axios = axios.create(axiosConfig) 42 | var file = filename 43 | var verbose = !!(options && options.verbose) 44 | 45 | return randomAccess({ 46 | open: function (req) { 47 | if (verbose) logger.log('Testing to see if server accepts range requests', url, file) 48 | // should cache this 49 | _axios.head(file) 50 | .then((response) => { 51 | if (verbose) logger.log('Received headers from server') 52 | var accepts = response.headers['accept-ranges'] 53 | if (accepts && accepts.toLowerCase().indexOf('bytes') !== -1) { 54 | if (response.headers['content-length']) this.length = response.headers['content-length'] 55 | return req.callback(null) 56 | } 57 | 58 | if (strict) return req.callback(new Error('Accept-Ranges does not include "bytes"')) 59 | 60 | logger.warn('Accept-Ranges does not include "bytes" or may not be supported.') 61 | return req.callback(null) 62 | }) 63 | .catch((err) => { 64 | if (verbose) logger.log('Error opening', file, '-', err) 65 | req.callback(err) 66 | }) 67 | }, 68 | read: function (req) { 69 | var range = `${req.offset}-${req.offset + req.size - 1}` 70 | var headers = { 71 | range: `bytes=${range}` 72 | } 73 | if (verbose) logger.log('Trying to read', file, headers.Range) 74 | _axios.get(file, { headers: headers }) 75 | .then((response) => { 76 | if (!response.headers['content-range']) { 77 | if (strict) throw new Error('No content range. Use the "strict" option to bypass.') 78 | if (req.offset + req.size > response.data.length) throw new Error('Could not satisfy length') 79 | logger.warn('Reading data without content-range.') 80 | return req.callback(null, Buffer.from(response.data.slice(req.offset, req.offset + req.size))) 81 | } 82 | 83 | if (response.status !== 206) throw new Error('Bad response: ' + response.status) 84 | var expectedRange = `bytes ${range}/${this.length}` 85 | if (response.headers['content-range'] !== expectedRange) throw new Error('Server returned unexpected range: ' + response.headers['content-range']) 86 | if (req.offset + req.size > this.length) throw new Error('Could not satisfy length') 87 | if (verbose) logger.log('read', JSON.stringify(response.headers, null, 2)) 88 | req.callback(null, Buffer.from(response.data)) 89 | }) 90 | .catch((err) => { 91 | if (verbose) { 92 | logger.log('error', file, headers.Range) 93 | logger.log(err, err.stack) 94 | } 95 | req.callback(err) 96 | }) 97 | }, 98 | write: function (req) { 99 | // This is a dummy write function - does not write, but fails silently 100 | if (verbose) logger.log('trying to write', file, req.offset, req.data) 101 | req.callback() 102 | }, 103 | del: function (req) { 104 | // This is a dummy del function - does not del, but fails silently 105 | if (verbose) logger.log('trying to del', file, req.offset, req.size) 106 | req.callback() 107 | }, 108 | close: function (req) { 109 | if (_axios.defaults.httpAgent) { 110 | _axios.defaults.httpAgent.destroy() 111 | } 112 | if (_axios.defaults.httpsAgent) { 113 | _axios.defaults.httpsAgent.destroy() 114 | } 115 | req.callback() 116 | } 117 | }) 118 | } 119 | 120 | module.exports = randomAccessHttp 121 | -------------------------------------------------------------------------------- /lib/is-node.js: -------------------------------------------------------------------------------- 1 | module.exports = !!(process && process.release && process.release.name === 'node') 2 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | // This is simply for testing 2 | module.exports = { 3 | log: console.log, 4 | warn: console.warn 5 | } 6 | -------------------------------------------------------------------------------- /lib/url.js: -------------------------------------------------------------------------------- 1 | var url = require('url') 2 | 3 | module.exports.validUrl = function (str) { 4 | if (typeof str !== 'string') return false 5 | var parsed = url.parse(str) /* eslint-disable-line node/no-deprecated-api */ 6 | return ~['http:', 'https:'].indexOf(parsed.protocol) 7 | } 8 | 9 | module.exports.prependUrlProtocol = function (str) { 10 | if (typeof str !== 'string') return false 11 | var parsed = url.parse(str) /* eslint-disable-line node/no-deprecated-api */ 12 | 13 | if (parsed.protocol === null) { 14 | parsed.protocol = 'http:' 15 | var parts = parsed.href.split('/') 16 | parsed.slashes = true 17 | parsed.hostname = parsed.host = parts[0] 18 | parsed.pathname = parsed.path = parts.slice(1).join('/') 19 | } 20 | 21 | return url.format(parsed) 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "random-access-http", 3 | "version": "2.0.2", 4 | "description": "A random access interface for files served over http", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && tape tests/**.test.js", 8 | "test-travis": "standard && nyc tape tests/**.test.js | tap-spec", 9 | "tdd": "tape-watch tests/**.test.js", 10 | "report-coverage": "nyc report --reporter=text-lcov | coveralls" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/random-access-storage/random-access-http.git" 15 | }, 16 | "keywords": [ 17 | "http", 18 | "https", 19 | "random-access", 20 | "random-access-storage" 21 | ], 22 | "author": "Bret Comnes", 23 | "contributors": "Benjamin Forster", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/random-access-storage/random-access-http/issues" 27 | }, 28 | "homepage": "https://github.com/random-access-storage/random-access-http#readme", 29 | "dependencies": { 30 | "axios": "^0.19.0", 31 | "random-access-storage": "^1.4.0", 32 | "url": "^0.11.0" 33 | }, 34 | "devDependencies": { 35 | "coveralls": "^3.0.9", 36 | "nyc": "^14.1.1", 37 | "proxyquire": "^2.1.3", 38 | "sinon": "^7.5.0", 39 | "st": "^2.0.0", 40 | "standard": "^14.3.1", 41 | "stoppable": "^1.1.0", 42 | "tap-spec": "^5.0.0", 43 | "tape": "^4.11.0", 44 | "tape-watch": "^2.3.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/integration.test.js: -------------------------------------------------------------------------------- 1 | var rahttp = require('../') 2 | var tape = require('tape') 3 | 4 | var testUrl = 'https://ia800309.us.archive.org/2/items/Popeye_Nearlyweds/Popeye_Nearlyweds_512kb.mp4' 5 | 6 | tape('open and close', function (t) { 7 | t.plan(4) 8 | var popeye = rahttp(testUrl) 9 | popeye.on('close', function () { 10 | t.pass('close event fired') 11 | }) 12 | popeye.on('open', function () { 13 | t.pass('open event fired') 14 | }) 15 | popeye.open(function (err) { 16 | t.error(err, 'url opened without error') 17 | popeye.close(function (err) { 18 | t.error(err, 'url closed without error') 19 | }) 20 | }) 21 | }) 22 | 23 | tape('read 10 bytes', function (t) { 24 | t.plan(3) 25 | var popeye = rahttp(testUrl) 26 | var length = 10 27 | popeye.read(0, 10, function (err, buf) { 28 | t.error(err, 'url read without error') 29 | t.equal(buf.length, length) 30 | popeye.close(function (err) { 31 | t.error(err, 'url closed without error') 32 | }) 33 | }) 34 | }) 35 | 36 | tape('read 100 bytes at an offset of 2000', function (t) { 37 | t.plan(3) 38 | var popeye = rahttp(testUrl) 39 | var length = 100 40 | popeye.read(2000, length, function (err, buf) { 41 | t.error(err, 'url read without error') 42 | t.equal(buf.length, length) 43 | popeye.close(function (err) { 44 | t.error(err, 'url closed without error') 45 | }) 46 | }) 47 | }) 48 | 49 | tape('read from https wikipedia', function (t) { 50 | t.plan(3) 51 | var popeye = rahttp('https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png') 52 | var length = 10 53 | popeye.read(0, 10, function (err, buf) { 54 | t.error(err, 'url read without error') 55 | t.equal(buf.length, length) 56 | popeye.close(function (err) { 57 | t.error(err, 'url closed without error') 58 | }) 59 | }) 60 | }) 61 | 62 | tape('read from http archive.org', function (t) { 63 | t.plan(3) 64 | var popeye = rahttp('http://ia801009.us.archive.org/5/items/mbid-e77048de-139b-3fd3-977b-d86993e0e1b2/mbid-e77048de-139b-3fd3-977b-d86993e0e1b2-12826202809.jpg') 65 | var length = 10 66 | popeye.read(30, 10, function (err, buf) { 67 | t.error(err, 'url read without error') 68 | t.equal(buf.length, length) 69 | popeye.close(function (err) { 70 | t.error(err, 'url closed without error') 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /tests/random-access-http.test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var proxyquire = require('proxyquire').noPreserveCache() 3 | var sinon = require('sinon') 4 | var stoppable = require('stoppable') 5 | var http = require('http') 6 | var st = require('st') 7 | var path = require('path') 8 | var port = 3000 9 | 10 | var server 11 | 12 | var content = 'reinscribed like a chain letter through the generations, and despite all the errors of reproduction — indeed, perhaps because of such errors — it has recruited its copyists and interpreters into the ranks of brotherhood' 13 | var standardHandler = (req, res) => { 14 | res.setHeader('Content-Type', 'plain/text') 15 | res.setHeader('Accept-Ranges', 'Bytes') 16 | res.setHeader('Content-Length', content.length) 17 | 18 | if (req.method === 'HEAD') { 19 | return res.end() 20 | } 21 | var range = req.headers.range 22 | if (range) { 23 | var match = range.match(/bytes=(\d+)-(\d+)/) 24 | if (match) { 25 | var a = parseInt(match[1], 10) 26 | var b = parseInt(match[2], 10) 27 | var str = content.slice(a, b + 1) 28 | res.setHeader('Content-Range', `bytes ${a}-${b}/${content.length}`) 29 | res.setHeader('Content-Length', str.length) 30 | res.statusCode = 206 31 | res.end(str) 32 | return 33 | } 34 | } 35 | res.end(content) 36 | } 37 | 38 | function startServer (fn, cb) { 39 | if (server && server.listening) { 40 | return server.stop(() => startServer(fn, cb)) 41 | } 42 | server = stoppable(http.createServer(fn)) 43 | server.listen(port, cb) 44 | } 45 | 46 | function stopServer (t) { 47 | if (!server.listening) return t.end() 48 | server.stop(() => { 49 | t.end() 50 | }) 51 | } 52 | 53 | test('it uses node http/s agent setting with keepAlive when run in node', (t) => { 54 | var httpStub = sinon.stub() 55 | var httpsStub = sinon.stub() 56 | var raHttp = proxyquire('../index.js', { 57 | http: { 58 | Agent: httpStub 59 | }, 60 | https: { 61 | Agent: httpsStub 62 | } 63 | }) 64 | raHttp('normal', { url: 'http://localhost:3000' }) 65 | t.ok(httpStub.calledWithNew()) 66 | t.ok(httpStub.calledWith({ keepAlive: true })) 67 | t.ok(httpsStub.calledWithNew()) 68 | t.ok(httpsStub.calledWith({ keepAlive: true })) 69 | t.end() 70 | }) 71 | 72 | test('it does not use node http/s when in browser', (t) => { 73 | var httpStub = sinon.stub() 74 | var httpsStub = sinon.stub() 75 | var raHttp = proxyquire('../index.js', { 76 | './lib/is-node': false, 77 | http: { 78 | Agent: httpStub 79 | }, 80 | https: { 81 | Agent: httpsStub 82 | } 83 | }) 84 | raHttp('not/using/node', { url: 'http://localhost:3000' }) 85 | t.ok(httpStub.notCalled) 86 | t.ok(httpsStub.notCalled) 87 | t.end() 88 | }) 89 | 90 | test('raHttp.open() callback returns error if server does not support range requests', (t) => { 91 | var raHttp = require('../index.js') 92 | var withoutRangeSupportHandler = (req, res) => { 93 | res.end() 94 | } 95 | startServer(withoutRangeSupportHandler, (err) => { 96 | t.error(err, 'server started without error') 97 | var ra = raHttp('without-range-support', { url: 'http://localhost:3000' }) 98 | ra.read(0, 10, (err, res) => { 99 | t.ok(err.message.search(/Accept-Ranges does not include/) !== -1, 'error contains "Accept-Ranges does not include"') 100 | stopServer(t) 101 | }) 102 | }) 103 | }) 104 | 105 | test('raHttp.open() callback returns error if call to axios.head() fails', (t) => { 106 | var raHttp = require('../index.js') 107 | var notFoundHandler = (req, res) => { 108 | res.statusCode = 404 109 | res.end() 110 | } 111 | startServer(notFoundHandler, (err) => { 112 | t.error(err, 'server started without error') 113 | var ra = raHttp('not-found', { url: 'http://localhost:3000' }) 114 | ra.read(0, 10, (err, res) => { 115 | t.ok(err.message.search(/Request failed with status code 404/) !== -1, 'error contains "Request failed with status code 404"') 116 | stopServer(t) 117 | }) 118 | }) 119 | }) 120 | 121 | test('raHttp.read() returns a buffer of length requested', (t) => { 122 | var raHttp = require('../index.js') 123 | startServer(standardHandler, (err) => { 124 | t.error(err) 125 | var ra = raHttp('test', { url: 'http://localhost:3000' }) 126 | ra.read(10, 20, (err, data) => { 127 | t.error(err) 128 | t.ok(data instanceof Buffer) 129 | t.same(data.length, 20) 130 | stopServer(t) 131 | }) 132 | }) 133 | }) 134 | 135 | test('raHttp.write does not throw error', (t) => { 136 | var raHttp = require('../index.js') 137 | startServer(standardHandler, (err) => { 138 | t.error(err) 139 | var ra = raHttp('test-write', { url: 'http://localhost:3000' }) 140 | t.doesNotThrow(ra.write.bind(ra, 10, 'some-data', function () { 141 | stopServer(t) 142 | })) 143 | }) 144 | }) 145 | 146 | test('raHttp.write logs with options.verbose === true', (t) => { 147 | var stub = sinon.stub() 148 | var proxyRaHttp = proxyquire('../index', { 149 | './lib/logger': { 150 | log: stub 151 | } 152 | }) 153 | startServer(standardHandler, (err) => { 154 | t.error(err) 155 | var ra = proxyRaHttp('test-write', { url: 'http://localhost:3000', verbose: true }) 156 | ra.write(10, 'some-data', (err, res) => { 157 | t.error(err) 158 | t.ok(stub.calledWith('trying to write', 'test-write', 10, 'some-data')) 159 | stopServer(t) 160 | }) 161 | }) 162 | }) 163 | 164 | test('raHttp.del does not throw error', (t) => { 165 | var raHttp = require('../index.js') 166 | startServer(standardHandler, (err) => { 167 | t.error(err) 168 | var ra = raHttp('test-del', { url: 'http://localhost:3000' }) 169 | t.doesNotThrow(ra.del.bind(ra, 10, 100, function () { 170 | stopServer(t) 171 | })) 172 | }) 173 | }) 174 | 175 | test('raHttp.del logs with options.verbose === true', (t) => { 176 | var stub = sinon.stub() 177 | var proxyRaHttp = proxyquire('../index', { 178 | './lib/logger': { 179 | log: stub 180 | } 181 | }) 182 | startServer(standardHandler, (err) => { 183 | t.error(err) 184 | var ra = proxyRaHttp('test-del', { url: 'http://localhost:3000', verbose: true }) 185 | ra.del(10, 100, (err, res) => { 186 | t.error(err) 187 | t.ok(stub.calledWith('trying to del', 'test-del', 10, 100)) 188 | stopServer(t) 189 | }) 190 | }) 191 | }) 192 | 193 | test('raHttp.read() on server that does not support ranges', (t) => { 194 | var raHttp = require('../index.js') 195 | var mount = st({ 196 | path: path.join(__dirname, '..') 197 | }) 198 | 199 | var server = stoppable(http.createServer(function (req, res) { 200 | mount(req, res) 201 | })) 202 | 203 | server.listen(port, function () { 204 | var ra = raHttp('LICENSE', { url: 'http://localhost:3000', strict: false }) 205 | ra.read(10, 20, (err, data) => { 206 | t.error(err) 207 | t.ok(data instanceof Buffer) 208 | t.same(data.length, 20) 209 | server.stop(function () { 210 | t.end() 211 | }) 212 | }) 213 | }) 214 | }) 215 | -------------------------------------------------------------------------------- /tests/valid-url.test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var { validUrl, prependUrlProtocol } = require('../lib/url') 3 | 4 | test('prependUrlProtocol assumes http without path', (t) => { 5 | t.same(prependUrlProtocol('example.com'), 'http://example.com') 6 | t.end() 7 | }) 8 | 9 | test('prependUrlProtocol assumes http with path', (t) => { 10 | t.same(prependUrlProtocol('example.com/foo/bar.html'), 'http://example.com/foo/bar.html') 11 | t.end() 12 | }) 13 | 14 | test('prependUrlProtocol doesn\'t change protocol if given', (t) => { 15 | t.same(prependUrlProtocol('https://example.com/foo/bar.html'), 'https://example.com/foo/bar.html') 16 | t.same(prependUrlProtocol('ftp://example.com/foo/bar.html'), 'ftp://example.com/foo/bar.html') 17 | t.end() 18 | }) 19 | 20 | test('validUrl returns false if url is not a string', (t) => { 21 | t.notOk(validUrl()) 22 | t.notOk(validUrl(null)) 23 | t.notOk(validUrl({})) 24 | t.notOk(validUrl(['foo'])) 25 | t.end() 26 | }) 27 | 28 | test('validUrl returns false for rubbish strings', (t) => { 29 | t.notOk(validUrl('f234324 ff43 f43f4 f43 ')) 30 | t.notOk(validUrl('company tax cuts will increase wages...')) 31 | t.end() 32 | }) 33 | 34 | test('validUrl returns false if url is not http/s', (t) => { 35 | t.notOk(validUrl('ftp://ok.com')) 36 | t.notOk(validUrl('ssh://this:not@ok.net')) 37 | t.notOk(validUrl('mailto:not@ok.net')) 38 | t.end() 39 | }) 40 | 41 | test('validUrl returns true if url is http/s', (t) => { 42 | t.ok(validUrl('http://theanarchistlibrary.org/')) 43 | t.ok(validUrl('http://127.0.0.1:4000')) 44 | t.ok(validUrl('https://en.wikipedia.org/wiki/S._R._Ranganathan')) 45 | t.end() 46 | }) 47 | --------------------------------------------------------------------------------