├── .gitignore ├── .travis.yml ├── cli.js ├── index.js ├── lib ├── peer-test.js ├── public-test.js ├── tasks-default.js ├── tasks-peer.js └── whoami-test.js ├── package.json ├── peer.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'lts/*' 4 | - '8' 5 | - '10' 6 | - 'node' 7 | cache: 8 | directories: 9 | - node_modules -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var minimist = require('minimist') 4 | var argv = minimist(process.argv.slice(2)) 5 | 6 | require('.')({ 7 | port: argv.port, 8 | peerId: argv._[0] 9 | }) 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec 2 | var crypto = require('crypto') 3 | var os = require('os') 4 | var neatLog = require('neat-log') 5 | var output = require('neat-log/output') 6 | var neatTasks = require('neat-tasks') 7 | var chalk = require('chalk') 8 | var Menu = require('menu-string') 9 | // var debug = require('debug')('dat-doctor') 10 | var defaultTasks = require('./lib/tasks-default') 11 | var peerTasks = require('./lib/tasks-peer') 12 | var peerTest = require('./lib/peer-test') 13 | 14 | var NODE_VER = process.version 15 | var DOCTOR_VER = require('./package.json').version 16 | var DAT_PROCESS = process.title === 'dat' 17 | 18 | module.exports = function (opts) { 19 | if (!opts) opts = {} 20 | opts.peerId = opts.peerId || null 21 | opts.port = typeof opts.port === 'number' ? opts.port : 3282 22 | 23 | var views = [headerOutput, versionsOutput, menuView] 24 | var neat = neatLog(views) 25 | neat.use(getVersions) 26 | 27 | if (opts.peerId) return runP2P() // run p2p tests right away 28 | 29 | var menu = Menu([ 30 | 'Basic Tests (Checks your Dat installation and network setup)', 31 | 'Peer-to-Peer Test (Debug connections between two computers)' 32 | ]) 33 | neat.use(function (state) { 34 | state.opts = opts 35 | state.port = opts.port 36 | }) 37 | neat.use(function (state, bus) { 38 | bus.emit('render') 39 | 40 | neat.input.on('down', function () { 41 | menu.down() 42 | bus.render() 43 | }) 44 | neat.input.on('up', function () { 45 | menu.up() 46 | bus.render() 47 | }) 48 | neat.input.once('enter', function () { 49 | state.selected = menu.selected() 50 | bus.render() 51 | startTests(state.selected) 52 | }) 53 | }) 54 | 55 | function startTests (selected) { 56 | if (selected.index === 0) return runBasic() 57 | else runP2P() 58 | } 59 | 60 | function runBasic () { 61 | var runTasks = neatTasks(defaultTasks(opts)) 62 | views.push(runTasks.view) 63 | neat.use(runTasks.use) 64 | neat.use(function (state, bus) { 65 | bus.once('done', function () { 66 | var testCountMsg = output(` 67 | ${chalk.bold(state.pass)} of ${chalk.bold(state.totalCount)} tests passed 68 | `) 69 | console.log('\n') 70 | if (state.fail === 0) { 71 | console.log(output(` 72 | ${chalk.bold.greenBright('SUCCESS!')} 73 | ${testCountMsg} 74 | Use Peer-to-Peer tests to check direct connections between two computers. 75 | `)) 76 | process.exit(0) 77 | } 78 | console.log(output(` 79 | ${chalk.bold.redBright('FAIL')} 80 | ${testCountMsg} 81 | 82 | Your network may be preventing you from using Dat. 83 | For further troubleshooting, visit https://docs.datproject.org/troubleshooting 84 | `)) 85 | process.exit(1) 86 | }) 87 | }) 88 | } 89 | 90 | function runP2P () { 91 | if (opts.peerId) { 92 | opts.existingTest = true 93 | opts.id = opts.peerId 94 | return startTasks() 95 | } 96 | 97 | opts.existingTest = false 98 | opts.id = crypto.randomBytes(32).toString('hex') 99 | startTasks() 100 | 101 | function startTasks () { 102 | var runTasks = neatTasks(peerTasks(opts)) 103 | views.push(runTasks.view) 104 | neat.use(runTasks.use) 105 | neat.use(function (state, bus) { 106 | // initial tasks done 107 | bus.once('done', function () { 108 | // TODO: Fix, overwriting previous line 109 | views.push(function () { return '\n' }) 110 | bus.render() 111 | 112 | state.id = opts.id 113 | state.existingTest = opts.existingTest 114 | peerTest(state, bus, views) 115 | }) 116 | }) 117 | } 118 | } 119 | 120 | function headerOutput (state) { 121 | return `Welcome to ${chalk.greenBright('Dat')} Doctor!\n` 122 | } 123 | 124 | function menuView (state) { 125 | if (!menu) return '' 126 | if (state.selected && state.selected.index === 0) return `Running ${state.selected.text}\n` 127 | else if (state.selected) { 128 | return output(` 129 | To start a new Peer-to-Peer test, press ENTER. 130 | Otherwise enter test ID. 131 | 132 | > 133 | `) 134 | } 135 | return output(` 136 | Which tests would you like to run? 137 | ${menu.toString()} 138 | `) 139 | } 140 | 141 | function versionsOutput (state) { 142 | if (!state.versions) return '' 143 | var version = state.versions 144 | return output(` 145 | Software Info: 146 | ${os.platform()} ${os.arch()} 147 | Node ${version.node} 148 | Dat Doctor v${version.doctor} 149 | ${datVer()} 150 | `) + '\n' 151 | 152 | function datVer () { 153 | if (!DAT_PROCESS || !version.dat) return '' 154 | return chalk.green(`dat v${version.dat}`) 155 | } 156 | } 157 | 158 | function getVersions (state, bus) { 159 | state.versions = { 160 | dat: null, 161 | doctor: DOCTOR_VER, 162 | node: NODE_VER 163 | } 164 | exec('dat -v', function (err, stdin, stderr) { 165 | if (err && err.code === 127) { 166 | // Dat not installed/executable 167 | state.datInstalled = false 168 | return bus.emit('render') 169 | } 170 | // if (err) return bus.emit('render') 171 | // TODO: right now dat -v exits with error code, need to fix 172 | state.versions.dat = stderr.toString().split('\n')[0].trim() 173 | bus.emit('render') 174 | }) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/peer-test.js: -------------------------------------------------------------------------------- 1 | var swarm = require('discovery-swarm') 2 | var crypto = require('crypto') 3 | var pump = require('pump') 4 | var defaults = require('dat-swarm-defaults')() 5 | var output = require('neat-log/output') 6 | var chalk = require('chalk') 7 | var debug = require('debug')('dat-doctor') 8 | 9 | module.exports = p2pTest 10 | 11 | function p2pTest (state, bus, views) { 12 | views.push(p2pView) 13 | 14 | var tick = 0 15 | state.peers = {} 16 | state.connecting = {} 17 | var sw = swarm({ 18 | dns: { 19 | domain: defaults.dns.domain, 20 | servers: defaults.dns.server 21 | }, 22 | dht: false 23 | }) 24 | 25 | bus.on('connecting', function (peer) { 26 | var id = `${peer.host}:${peer.port}` 27 | if (state.connecting[id]) return 28 | state.connecting[id] = peer 29 | bus.emit('render') 30 | }) 31 | bus.on('connection', function (prefix, info) { 32 | info.host = info.host.split(':').pop() 33 | var id = `${info.host}:${info.port}` 34 | if (state.connecting[id]) state.connecting[id].connected = true 35 | state.peers[prefix] = info 36 | bus.emit('render') 37 | }) 38 | bus.on('echo', function (prefix) { 39 | state.peers[prefix].echo = true 40 | bus.emit('render') 41 | }) 42 | 43 | sw.on('error', function () { 44 | sw.listen(0) 45 | }) 46 | sw.listen(state.port) 47 | sw.on('listening', function () { 48 | bus.emit('render') 49 | sw.join(state.id) 50 | sw.once('connecting', function () { 51 | state.active = true 52 | bus.emit('render') 53 | }) 54 | sw.on('connecting', function (peer) { 55 | bus.emit('connecting', peer) 56 | }) 57 | sw.on('peer', function (peer) { 58 | debug('Discovered %s:%d', peer.host, peer.port) 59 | }) 60 | sw.on('connection', function (connection, info) { 61 | var num = tick++ 62 | var prefix = '0000'.slice(0, -num.toString().length) + num 63 | bus.emit('connection', prefix, info) 64 | 65 | var data = crypto.randomBytes(16).toString('hex') 66 | debug('[%s-%s] Connection established to remote peer', prefix, info.type) 67 | var buf = '' 68 | connection.setEncoding('utf-8') 69 | connection.write(data) 70 | connection.on('data', function (remote) { 71 | buf += remote 72 | if (buf.length === data.length) { 73 | bus.emit('echo', prefix) 74 | debug('[%s-%s] Remote peer echoed expected data back, success!', prefix, info.type) 75 | } 76 | }) 77 | pump(connection, connection, function () { 78 | debug('[%s-%s] Connected closed', prefix, info.type) 79 | bus.emit('render') 80 | }) 81 | }) 82 | }) 83 | 84 | function p2pView (state) { 85 | if (!state.id) return '' 86 | if (!state.existingTest) { 87 | return '\n' + output(` 88 | Running a new Peer-to-Peer test 89 | 90 | To check connectivity with another computer, run the command: 91 | 92 | ${testCmd()} 93 | 94 | ${peers()} 95 | `) 96 | } 97 | 98 | return output(` 99 | Joining existing Peer-to-Peer test: 100 | 101 | ${testCmd()} 102 | 103 | ${peers()} 104 | `) 105 | 106 | function testCmd () { 107 | return process.title === 'dat' ? chalk.magentaBright(`dat doctor ${state.id}`) : chalk.magentaBright(`dat-doctor ${state.id}`) 108 | } 109 | 110 | function peers () { 111 | if (!state.active) return 'Waiting for incoming connections...' 112 | return output(` 113 | ${Object.keys(state.peers).map(peerId => { 114 | var peer = state.peers[peerId] 115 | var address = `${peer.host}:${peer.port}` 116 | var prefix = `${address} (${peer.type.toUpperCase()})` 117 | if (peer.echo) return `${prefix} ${chalk.greenBright.bold('SUCCESS!')}` 118 | return `${prefix} connected, trying to exchange data` 119 | }).join('\n')} 120 | ${connecting()} 121 | `) 122 | 123 | function connecting () { 124 | var peers = Object.keys(state.connecting) 125 | if (!peers.length) return '' 126 | return '\n' + output(` 127 | Trying to Connect: 128 | ${peers.map(peer => { 129 | if (peer.connected) return '' 130 | return ` ${peer}` 131 | }).join('\n')} 132 | `) 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/public-test.js: -------------------------------------------------------------------------------- 1 | var swarm = require('discovery-swarm') 2 | var pump = require('pump') 3 | var defaults = require('dat-swarm-defaults')() 4 | var thunky = require('thunky') 5 | var debug = require('debug')('dat-doctor') 6 | 7 | module.exports = runPublicPeerTest 8 | 9 | function runPublicPeerTest (state, bus, opts, cb) { 10 | var address = opts.address 11 | var port = opts.port || 3282 12 | 13 | var connected = false 14 | var dataEcho = false 15 | 16 | var sw = swarm({ 17 | dns: { 18 | domain: defaults.dns.domain, 19 | servers: defaults.dns.server 20 | }, 21 | whitelist: [address], 22 | dht: false, 23 | hash: false, 24 | utp: opts.utp, 25 | tcp: opts.tcp 26 | }) 27 | 28 | sw.on('error', function () { 29 | if (port === 3282) bus.emit('error', `Default Dat port did not work (${port}), using random port`) 30 | else bus.emit('error', `Specified port did not work (${port}), using random port`) 31 | sw.listen(0) 32 | }) 33 | sw.listen(port) 34 | 35 | sw.on('listening', function () { 36 | state.title = 'Looking for Doctor on the Dat network...' 37 | sw.join('dat-doctor-public-peer', { announce: false }) 38 | sw.on('connecting', function (peer) { 39 | state.title = `Connecting to Dat Doctor, ${peer.host}:${peer.port}` 40 | debug('Trying to connect to doctor, %s:%d', peer.host, peer.port) 41 | }) 42 | sw.on('peer', function (peer) { 43 | state.title = `Discovered Dat Doctor, ${peer.host}:${peer.port}` 44 | debug('Discovered doctor, %s:%d', peer.host, peer.port) 45 | }) 46 | sw.on('connection', function (connection) { 47 | connected = true 48 | state.title = `Connected to Dat Doctor!` 49 | debug('Connection established to doctor') 50 | connection.setEncoding('utf-8') 51 | connection.on('data', function (remote) { 52 | dataEcho = true 53 | state.title = `Successful data transfer with Dat Doctor via ${opts.tcp ? 'TCP' : 'UDP'}` 54 | destroy(cb) 55 | }) 56 | pump(connection, connection, function () { 57 | debug('Connection closed') 58 | destroy(cb) 59 | }) 60 | }) 61 | // debug('Attempting connection to doctor, %s', doctor) 62 | setTimeout(function () { 63 | if (connected) return 64 | bus.emit('error', 'Connection timed out.') 65 | destroy(cb) 66 | }, 10000) 67 | var destroy = thunky(function (done) { 68 | sw.destroy(function () { 69 | if (connected && dataEcho) return done() 70 | state.title = `Public Peer Test via ${opts.tcp ? 'TCP' : 'UDP'} Failed` 71 | if (!connected) { 72 | done('Unable to connect to Dat public server') 73 | } 74 | if (!dataEcho) { 75 | done('Data was not echoed back from public server') 76 | } 77 | done() 78 | }) 79 | }) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /lib/tasks-default.js: -------------------------------------------------------------------------------- 1 | var dns = require('dns') 2 | var chalk = require('chalk') 3 | var debug = require('debug')('dat-doctor') 4 | var runPublicTest = require('./public-test') 5 | var whoamiTest = require('./whoami-test') 6 | 7 | module.exports = function (opts) { 8 | if (!opts) opts = {} 9 | 10 | var DOCTOR_URL = 'doctor1.datprotocol.com' 11 | var doctorAddress = null 12 | var port = opts.port 13 | var skipUTP = false 14 | 15 | var tasks = [ 16 | { 17 | title: 'Who am I?', 18 | task: function (state, bus, done) { 19 | state.port = port 20 | bus.on('error', function (err) { 21 | if (!state.output) state.output = ' ' + chalk.dim(err) 22 | else state.output += '\n ' + chalk.dim(err) 23 | }) 24 | whoamiTest(state, bus, done) 25 | } 26 | }, 27 | { 28 | title: 'Checking Dat Native Module Installation', 29 | task: nativeModuleTask 30 | }, 31 | { 32 | title: 'Pinging the Dat Doctor', 33 | task: dnsLookupTask 34 | }, 35 | { 36 | title: 'Checking Dat Public Connections via TCP', 37 | task: function (state, bus, done) { 38 | publicPeerTask(state, bus, { tcp: true, utp: false }, done) 39 | }, 40 | skip: function (done) { 41 | if (doctorAddress) return done() 42 | done(`Skipping... unable to reach ${DOCTOR_URL}`) 43 | } 44 | }, 45 | { 46 | title: 'Checking Dat Public Connections via UTP', 47 | task: function (state, bus, done) { 48 | publicPeerTask(state, bus, { tcp: false, utp: true }, done) 49 | }, 50 | skip: function (done) { 51 | if (!doctorAddress) { 52 | return done(`Skipping... unable to reach ${DOCTOR_URL}`) 53 | } 54 | if (skipUTP) { 55 | return done('Skipping... UTP not available') 56 | } 57 | return done() 58 | } 59 | } 60 | ] 61 | 62 | return tasks 63 | 64 | function dnsLookupTask (state, bus, done) { 65 | dns.lookup(DOCTOR_URL, function (err, address, _) { 66 | if (err) { 67 | state.title = 'Unable to reach the Dat Doctor Server' 68 | return done(`Please check if you can resolve the url manually, ${chalk.reset.cyanBright(`ping ${DOCTOR_URL}`)}`) 69 | } 70 | state.title = 'Resolved Dat Doctor Server' 71 | doctorAddress = address 72 | done() 73 | }) 74 | } 75 | 76 | function nativeModuleTask (state, bus, done) { 77 | try { 78 | require('utp-native') 79 | state.title = 'Loaded native modules' 80 | } catch (err) { 81 | state.title = 'Error loading native modules' 82 | // TODO: link to FAQ/More Help 83 | skipUTP = true 84 | return done(`Unable to load utp-native.\n This will make it harder to connect peer-to-peer.`) 85 | } 86 | done() 87 | } 88 | 89 | function publicPeerTask (state, bus, opts, done) { 90 | opts = Object.assign({ port: port, address: doctorAddress }, opts) 91 | state.errors = [] 92 | state.messages = [] 93 | 94 | bus.on('error', (err) => { 95 | // TODO: persist these after task is done? 96 | debug('ERROR - ', err) 97 | }) 98 | 99 | runPublicTest(state, bus, opts, function (err) { 100 | if (err) return done(err) 101 | done() 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/tasks-peer.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | // var debug = require('debug')('dat-doctor') 3 | var whoamiTest = require('./whoami-test') 4 | 5 | module.exports = function (opts) { 6 | if (!opts) opts = {} 7 | 8 | var port = opts.port 9 | 10 | var tasks = [ 11 | { 12 | title: 'Who am I?', 13 | task: function (state, bus, done) { 14 | state.port = port 15 | bus.on('error', function (err) { 16 | if (!state.output) state.output = ' ' + chalk.dim(err) 17 | else state.output += '\n ' + chalk.dim(err) 18 | }) 19 | whoamiTest(state, bus, done) 20 | } 21 | }, 22 | { 23 | title: 'Checking Dat Native Module Installation', 24 | task: nativeModuleTask 25 | } 26 | ] 27 | 28 | return tasks 29 | 30 | function nativeModuleTask (state, bus, done) { 31 | try { 32 | require('utp-native') 33 | state.title = 'Loaded native modules' 34 | } catch (err) { 35 | state.title = 'Error loading native modules' 36 | // TODO: link to FAQ/More Help 37 | return done(`Unable to load utp-native.\n This will make it harder to connect peer-to-peer.`) 38 | } 39 | done() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/whoami-test.js: -------------------------------------------------------------------------------- 1 | var dnsDiscovery = require('dns-discovery') 2 | var defaults = require('dat-swarm-defaults')() 3 | // var debug = require('debug')('dat-doctor') 4 | 5 | module.exports = whoamiTest 6 | 7 | function whoamiTest (state, bus, done) { 8 | createClient(state.port) 9 | 10 | function createClient (port) { 11 | var client = dnsDiscovery({ 12 | domain: defaults.dns.domain, 13 | servers: defaults.dns.server 14 | }) 15 | 16 | client.once('error', function (err) { 17 | if (err.code !== 'EADDRINUSE') return done('ERROR: ' + err.message) 18 | if (state.port === 3282) bus.emit('error', `The default Dat port (${state.port}) in use, using random port.`) 19 | else bus.emit('error', `Specified port (${state.port}) in use, using random port`) 20 | bus.emit('error', `This may impact Dat's connectivity if you have a firewall.`) 21 | client.on('close', function () { 22 | createClient([0]) 23 | }) 24 | client.destroy() 25 | }) 26 | 27 | client.listen(port) 28 | client.on('listening', function () { 29 | client.whoami(function (err, me) { 30 | client.destroy() 31 | if (err) return done(' ERROR: Could not detect public ip / port') 32 | if (!me.port) return done(' ERROR: symmetric nat') 33 | state.host = me.host // public IP 34 | state.port = me.port 35 | state.title = `Your address is: ${state.host}:${state.port}` 36 | done() 37 | }) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dat-doctor", 3 | "version": "2.1.3", 4 | "description": "Dat network doctor extension", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && npm run test-deps", 8 | "test-deps": "dependency-check . && dependency-check . --extra --no-dev" 9 | }, 10 | "bin": { 11 | "dat-doctor": "cli.js" 12 | }, 13 | "author": "Dat Project", 14 | "license": "BSD-3-Clause", 15 | "repository": "datproject/dat-doctor", 16 | "dependencies": { 17 | "chalk": "^2.3.2", 18 | "dat-swarm-defaults": "^1.0.1", 19 | "debug": "^4.1.1", 20 | "discovery-swarm": "^5.1.2", 21 | "dns-discovery": "^6.1.0", 22 | "menu-string": "^1.2.0", 23 | "minimist": "^1.2.0", 24 | "neat-log": "^3.1.0", 25 | "neat-tasks": "^1.0.0", 26 | "pump": "^3.0.0", 27 | "thunky": "^1.0.1", 28 | "utp-native": "^2.1.3" 29 | }, 30 | "devDependencies": { 31 | "dependency-check": "^3.3.0", 32 | "standard": "^12.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /peer.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // this is the peer at 'doctor1.publicbits.org' 4 | 5 | var swarm = require('discovery-swarm') 6 | var crypto = require('crypto') 7 | var pump = require('pump') 8 | var defaults = require('dat-swarm-defaults')() 9 | 10 | var sw = swarm({ 11 | dns: { 12 | servers: defaults.dns.server 13 | }, 14 | hash: false, 15 | dht: false 16 | }) 17 | 18 | sw.on('error', function () { 19 | sw.listen(0) 20 | }) 21 | sw.listen(8887) 22 | sw.on('listening', function () { 23 | sw.join('dat-doctor-public-peer') 24 | sw.on('connecting', function (peer) { 25 | console.log('Trying to connect to %s:%d', peer.host, peer.port) 26 | }) 27 | sw.on('peer', function (peer) { 28 | console.log('Discovered %s:%d', peer.host, peer.port) 29 | }) 30 | sw.on('connection', function (connection) { 31 | var data = crypto.randomBytes(16).toString('hex') 32 | console.log('Connection established to remote peer') 33 | connection.setEncoding('utf-8') 34 | connection.write(data) 35 | connection.on('data', function (remote) { 36 | console.log('Got data back from peer %s', remote.toString()) 37 | connection.destroy() 38 | }) 39 | pump(connection, connection, function () { 40 | console.log('Connection closed') 41 | }) 42 | }) 43 | console.log('Waiting for incoming connections... (local port: %d)', sw.address().port) 44 | }) 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![deprecated](http://badges.github.io/stability-badges/dist/deprecated.svg)](https://dat-ecosystem.org/) 2 | 3 | More info on active projects and modules at [dat-ecosystem.org](https://dat-ecosystem.org/) 4 | 5 | --- 6 | 7 | # dat-doctor 8 | 9 | Diagnose network problems for [Dat](http://github.com/datproject/dat) 10 | 11 | [![npm][npm-image]][npm-url] 12 | [![travis][travis-image]][travis-url] 13 | [![standard][standard-image]][standard-url] 14 | 15 | 16 | The doctor comes bundled in the Dat CLI. If you already have that then you can ask the doctor for help! Run: 17 | 18 | ``` 19 | dat doctor 20 | ``` 21 | 22 | ## Usage 23 | 24 | ``` 25 | npm install -g dat-doctor 26 | dat-doctor 27 | ``` 28 | 29 | This will print out another command to run on the computer you are trying to connect to: 30 | 31 | ``` 32 | dat-doctor 84af4d998fb94ec60dcbda1c6c5d7c9158f9a5ef36f63b9d29d9ce3df61eef4b 33 | ``` 34 | 35 | ## License 36 | 37 | BSD-3-Clause 38 | 39 | 40 | [npm-image]: https://img.shields.io/npm/v/dat-doctor.svg?style=flat-square 41 | [npm-url]: https://www.npmjs.com/package/dat-doctor 42 | [travis-image]: https://img.shields.io/travis/datproject/dat-doctor.svg?style=flat-square 43 | [travis-url]: https://travis-ci.org/datproject/dat-doctor 44 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 45 | [standard-url]: http://npm.im/standard 46 | 47 | --------------------------------------------------------------------------------