Skip to content
This repository has been archived by the owner on Feb 7, 2020. It is now read-only.

Commit

Permalink
Merge 138adf5 into 20b509e
Browse files Browse the repository at this point in the history
  • Loading branch information
bucko13 committed May 3, 2019
2 parents 20b509e + 138adf5 commit 22c3dec
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 31 deletions.
47 changes: 47 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright 2019 Tierion
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Config from 'bcfg'

let config = null

function getConfig(options = {}) {
// if (config) {
// config.inject(options)
// return config
// }

// create a new config module for chainpoint
// this will set the prefix to `~/.chainpoint`
// and also parse env vars that are prefixed with `CHAINPOINT_`
config = new Config('chainpoint')
config.inject(options)
config.load({
// Parse URL hash
hash: true,
// Parse querystring
query: true,
// Parse environment
env: true,
// Parse args
argv: true
})

// Will parse [PREFIX]/chainpoint.conf (throws on FS error).
// PREFIX defaults to `~/.chainpoint`
// can change the prefix by passing in a `prefix` option
config.open('chainpoint.conf')
return config
}

export default getConfig
4 changes: 3 additions & 1 deletion lib/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import { isEmpty, forEach, map, every, reject, keys, flatten, mapKeys, camelCase
import { isValidNodeURI } from './utils/network'
import { isValidProofHandle } from './utils/proofs'
import { isSecureOrigin, isValidUUID, fetchEndpoints, testArrayArg } from './utils/helpers'
import getConfig from './config'
import { NODE_PROXY_URI } from './constants'

let config = getConfig()
/**
* Retrieve a collection of proofs for one or more hash IDs from the appropriate Node(s)
* The output of `submitProofs()` can be passed directly as the `proofHandles` arg to
Expand Down Expand Up @@ -93,7 +95,7 @@ async function getProofs(proofHandles) {
)
let getOptions = {
method: 'GET',
uri: (isSecureOrigin() ? NODE_PROXY_URI : node) + '/proofs',
uri: (isSecureOrigin() ? config.str('node-proxy-uri', NODE_PROXY_URI) : node) + '/proofs',
body: {},
headers,
timeout: 10000
Expand Down
4 changes: 3 additions & 1 deletion lib/submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import { isHex, isSecureOrigin, fetchEndpoints, validateHashesArg, validateUrisA
import { isValidNodeURI, getNodes } from './utils/network'
import { mapSubmitHashesRespToProofHandles } from './utils/proofs'
import { NODE_PROXY_URI } from './constants'
import getConfig from './config'

let config = getConfig()
/**
* Submit hash(es) to one or more Nodes, returning an Array of proof handle objects, one for each submitted hash and Node combination.
* @param {Array<String>} hashes - An Array of String Hashes in Hexadecimal form.
Expand Down Expand Up @@ -52,7 +54,7 @@ export async function submitHashes(hashes, uris) {
// Setup an options Object for each Node we'll submit hashes to.
// Each Node will then be sent the full Array of hashes.
let nodesWithPostOpts = map(nodes, node => {
let uri = isSecureOrigin() ? NODE_PROXY_URI : node
let uri = isSecureOrigin() ? config.str('node-proxy-uri', NODE_PROXY_URI) : node
let headers = Object.assign(
{
'Content-Type': 'application/json',
Expand Down
83 changes: 59 additions & 24 deletions lib/utils/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@
import { resolveTxt } from 'dns'
import { parse } from 'url'
import { promisify } from 'util'
import { isInteger, isFunction, isEmpty, slice, map, shuffle, filter, isString } from 'lodash'
import { isInteger, isFunction, isEmpty, slice, map, shuffle, filter, first, isString } from 'lodash'
import { isURL, isIP } from 'validator'
import fetch from 'node-fetch'
const { AbortController, abortableFetch } = require('abortcontroller-polyfill/dist/cjs-ponyfill')
const { fetch } = abortableFetch(require('node-fetch'))

import getConfig from '../config'
import { DNS_CORE_DISCOVERY_ADDR } from '../constants'
import { testArrayArg } from './helpers'

let config = getConfig()

/**
* Check if valid Core URI
Expand Down Expand Up @@ -84,7 +89,8 @@ export async function getCores(num) {

if (resolveTxt && isFunction(resolveTxt)) {
let resolveTxtAsync = promisify(resolveTxt)
let records = await resolveTxtAsync(DNS_CORE_DISCOVERY_ADDR)
let coreDiscovery = config.str('core-discovery-addr', DNS_CORE_DISCOVERY_ADDR)
let records = await resolveTxtAsync(coreDiscovery)

if (isEmpty(records)) throw new Error('no core addresses available')

Expand All @@ -103,7 +109,7 @@ export async function getCores(num) {
} else {
// `dns` module is not available in the browser
// fallback to simple random selection of Cores
let cores = ['https://a.chainpoint.org', 'https://b.chainpoint.org', 'https://c.chainpoint.org']
let cores = config.array('cores', ['http://35.245.211.97'])
return slice(shuffle(cores), 0, num)
}
}
Expand All @@ -121,10 +127,10 @@ export async function getNodes(num) {

if (!isInteger(num) || num < 1) throw new Error('num arg must be an Integer >= 1')

// TODO: Replace hard coded value with configurable env vars
// let coreURI = await getCores(1)
// let getNodeURI = first(coreURI) + '/nodes/random'
let getNodeURI = 'http://35.245.211.97/nodes/random'
// get cores uri from configs, if none, then check with getCores
let coreURI = config.array('cores')
if (!coreURI) coreURI = await getCores(1)
let getNodeURI = first(coreURI) + '/nodes/random'
let response = await fetch(getNodeURI)
response = await response.json()

Expand All @@ -136,28 +142,57 @@ export async function getNodes(num) {
return isValidNodeURI(n)
})

let testedNodes = []
let failedNodes = []

// since not all nodes returned from a core are guaranteed to work
// we need to test each one until we have the number originally requested
for (let node of filteredNodes) {
try {
await fetch(node, { timeout: 150, method: 'GET' })
testedNodes.push(node)
// if we have enough nodes, we don't need to test anymore
if (testedNodes.length === num) break
} catch (e) {
// store failed nodes so that they can be logged
failedNodes.push(node)
}
}
// we need to test each one
let testedNodes = await testNodeEndpoints(filteredNodes, failedNodes)

// remove any that have failed and slice to requested number
let slicedNodes = testedNodes.filter(node => node).slice(0, num)

if (failedNodes.length) console.error('Could not connect to the following nodes:\n', failedNodes.join(',\n'))
if (failedNodes.length)
console.error(
`Could not connect to the following nodes provided by core ${first(coreURI)}:\n`,
failedNodes.join(',\n')
)

// We should never return an empty array of nodes
if (!testedNodes.length)
if (!slicedNodes.length)
throw new Error('There seems to be an issue retrieving a list of available nodes. Please try again.')

return testedNodes
return slicedNodes
}

/**
* Test an array of node uris to see if they are responding to requests.
* Adds cross-platform support for a timeout to make the check faster. The browser's fetch
* does not support a timeout paramater so need to add with AbortController
*
* @param {String[]} nodes - array of node URIs
* @param {Array} failures - Need an external array to be passed to track failures
* @returns {Promise} - returns a Promise.all that resolves to an array of urls. Any that fail return as undefined
* and should be filtered out of the final result
*/
export function testNodeEndpoints(nodes, failures = [], timeout = 150) {
testArrayArg(nodes)
return Promise.all(
nodes.map(async node => {
try {
isValidNodeURI(node)
let controller, signal, timeoutId
if (AbortController) {
controller = new AbortController()
signal = controller.signal
timeoutId = setTimeout(() => controller.abort(), timeout)
}
await fetch(node, { timeout, method: 'GET', signal })

clearTimeout(timeoutId)
return node
} catch (e) {
failures.push(node)
}
})
)
}
5 changes: 4 additions & 1 deletion lib/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { fetchEndpoints, isSecureOrigin } from './utils/helpers'
// it was called
import * as evaluate from './evaluate'
import { NODE_PROXY_URI } from './constants'
import getConfig from './config'

let config = getConfig()

/**
* Verify a collection of proofs using an optionally provided Node URI
Expand Down Expand Up @@ -73,7 +76,7 @@ export default async function verifyProofs(proofs, uri) {
: {}
)

let uri = isSecureOrigin() ? NODE_PROXY_URI + url.parse(anchorURI).path : anchorURI
let uri = isSecureOrigin() ? config.str('node-proxy-uri', NODE_PROXY_URI) + url.parse(anchorURI).path : anchorURI

return {
method: 'GET',
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
"coverage": "nyc report --reporter=text-lcov | coveralls",
"eslint-check": "eslint --print-config . | eslint-config-prettier-check",
"lint": "eslint lib/**/*.js *.js",
"test": "npm run test:lint && nyc mocha --reporter spec -r esm 'tests/!(e2e)*-test.js'",
"test": "CHAINPOINT_CORES=http://35.245.211.97 npm run test:lint && nyc mocha --reporter spec -r esm 'tests/!(e2e)*-test.js'",
"test:lint": "npm run lint",
"test:unit": "nyc mocha --reporter spec -r esm 'tests/!(e2e)*-test.js'",
"test:watch": "nyc mocha --reporter spec -r esm --watch 'tests/!(e2e)*-test.js'",
"test:unit": "CHAINPOINT_CORES=http://35.245.211.97 nyc mocha --reporter spec -r esm 'tests/!(e2e)*-test.js'",
"test:watch": "CHAINPOINT_CORES=http://35.245.211.97 nyc mocha --reporter spec -r esm --watch 'tests/!(e2e)*-test.js'",
"test:e2e": "npm run webpack && mocha --reporter spec -r esm tests/e2e-test.js",
"webpack": "webpack",
"prepublishOnly": "webpack"
Expand Down Expand Up @@ -68,6 +68,8 @@
"dependencies": {
"@babel/polyfill": "^7.4.3",
"@ungap/url-search-params": "^0.1.2",
"abortcontroller-polyfill": "^1.3.0",
"bcfg": "^0.1.6",
"chainpoint-parse": "^3.2.0",
"lodash": "^4.17.11",
"node-fetch": "^2.3.0",
Expand Down
61 changes: 61 additions & 0 deletions tests/config-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright 2019 Tierion
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import getConfig from '../lib/config'
import { expect } from 'chai'
import Config from 'bcfg'
import fs from 'bfile'

describe('config', () => {
let foo, prefix, config
beforeEach(async () => {
foo = 'bar'
prefix = '/tmp/chainpoint_tests'
fs.mkdirSync(prefix)
})

afterEach(async () => {
await fs.remove(prefix)
config = null
})

it('should return a bcfg object', () => {
config = getConfig()
expect(config).to.be.an.instanceof(Config)
})

it('should load env vars', () => {
process.env.CHAINPOINT_FOO = foo
config = getConfig()
expect(config.str('foo')).to.equal(foo)
delete process.env.CHAINPOINT_FOO
})

it('should load options that are passed to it', () => {
config = getConfig({ foo })
expect(config.str('foo')).to.equal(foo)
})

it('should load argv', () => {
process.argv.push(`--foo=${foo}`)
config = getConfig()
expect(config.str('foo')).to.equal(foo)
// remove variable from argv
process.argv.pop()
})

it('should load configs from a config file', async () => {
fs.writeFileSync(prefix + '/chainpoint.conf', 'foo: bar')
config = getConfig({ prefix })
expect(config.str('foo')).to.equal(foo)
})
})
45 changes: 44 additions & 1 deletion tests/network-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { expect } from 'chai'
import nock from 'nock'

import { network } from '../lib/utils'
const { expect } = require('chai')
import nodes from './data/nodes'

describe('network utilities', () => {
describe('isValidNodeURI', () => {
Expand Down Expand Up @@ -52,4 +55,44 @@ describe('network utilities', () => {
nodes.forEach(node => expect(network.isValidNodeURI(node), `Invalid node URI returned: ${node}`).to.be.true)
})
})

describe('testNodeEndpoints', () => {
afterEach(() => {
nock.cleanAll()
})

it('should skip invalid node URIs and only return valid Nodes that respond to requests', async () => {
nodes.forEach(node => {
nock(node)
.get('/')
.delay(100)
.reply(200)
})
let badNodes = ['fail.com', 'http://0.0.0.3']
let failed = []
let tested = await network.testNodeEndpoints([...nodes, ...badNodes], failed)
// clear failed endpoints from result
tested = tested.filter(node => node)

// should not have more than the known working nodes in result
expect(failed).to.eql(badNodes)
expect(tested).to.have.lengthOf(nodes.length)
})

it("should reject endpoints that don't respond after specified timeout", async () => {
let timeoutDelay = 100
nodes.forEach(node => {
nock(node)
.get('/')
.delay(timeoutDelay)
.reply(200)
})

let failed = []
let tested = await network.testNodeEndpoints(nodes, failed, timeoutDelay - 50)
tested = tested.filter(node => node)
expect(tested).to.have.lengthOf(0)
expect(failed).to.have.lengthOf(nodes.length)
})
})
})
Loading

0 comments on commit 22c3dec

Please sign in to comment.