|
|
|
const querystring = require('querystring');
|
|
|
|
const got = require('got');
|
|
|
|
const cacheManager = require('cache-manager');
|
|
|
|
const expired = require('expired');
|
|
|
|
const deepAssign = require('deep-assign');
|
|
|
|
const pkg = require('../package.json');
|
|
|
|
|
|
|
|
class Onionoo {
|
|
|
|
|
|
|
|
// Constructor returns a new object so instance properties are private
|
|
|
|
constructor(options = {}) {
|
|
|
|
// Set default options
|
|
|
|
this.options = Object.assign({}, {
|
|
|
|
baseUrl: 'https://onionoo.torproject.org',
|
|
|
|
endpoints: [
|
|
|
|
'summary',
|
|
|
|
'details',
|
|
|
|
'bandwidth',
|
|
|
|
'weights',
|
|
|
|
'clients',
|
|
|
|
'uptime'
|
|
|
|
]
|
|
|
|
}, options);
|
|
|
|
if (options.cache !== false) {
|
|
|
|
this.options.cache = cacheManager.caching(Object.assign({}, {
|
|
|
|
store: 'memory',
|
|
|
|
max: 500
|
|
|
|
}, options.cache));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return object containing endpoint methods
|
|
|
|
return this.options.endpoints.reduce((onionoo, endpoint) => {
|
|
|
|
onionoo[endpoint] = options => this.get(endpoint, options);
|
|
|
|
|
|
|
|
return onionoo;
|
|
|
|
}, {});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns a function to make requests to a given endpoint
|
|
|
|
get(endpoint, options) {
|
|
|
|
// Build query string (don't encode ':' for search filters)
|
|
|
|
const qs = querystring.encode(options).replace(/%3A/g, ':');
|
|
|
|
|
|
|
|
// Build url
|
|
|
|
let url = `${this.options.baseUrl}/${endpoint}`;
|
|
|
|
url += qs ? `?${qs}` : '';
|
|
|
|
|
|
|
|
// If caching is disabled, just make the request
|
|
|
|
if (!this.options.cache) {
|
|
|
|
return this.makeRequest(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If caching is enabled, check for url in cache
|
|
|
|
return this.options.cache.get(url)
|
|
|
|
.then(cachedResult => {
|
|
|
|
let options = {};
|
|
|
|
|
|
|
|
// If we have it cached
|
|
|
|
if (cachedResult) {
|
|
|
|
// Return the cached entry if it's still valid
|
|
|
|
if (!expired(cachedResult.headers)) {
|
|
|
|
return cachedResult;
|
|
|
|
|
|
|
|
// If it's stale, add last-modified date to headers
|
|
|
|
} else if (cachedResult.headers['last-modified']) {
|
|
|
|
options.headers = {
|
|
|
|
'if-modified-since': cachedResult.headers['last-modified']
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make a request
|
|
|
|
return this.makeRequest(url, options)
|
|
|
|
.then(response => {
|
|
|
|
// If we get a 304, fill in the body
|
|
|
|
if (response.statusCode === 304) {
|
|
|
|
response.body = cachedResult.body;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we get a 200 or 304, cache it
|
|
|
|
if ([200, 304].includes(response.statusCode)) {
|
|
|
|
this.options.cache.set(url, response);
|
|
|
|
}
|
|
|
|
|
|
|
|
return response;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns a promise for a request
|
|
|
|
makeRequest(url, options = {}) {
|
|
|
|
options = deepAssign({
|
|
|
|
headers: {
|
|
|
|
'user-agent': `onionoo-node-client v${pkg.version} (${pkg.homepage})`
|
|
|
|
}
|
|
|
|
}, options);
|
|
|
|
|
|
|
|
return got(url, options)
|
|
|
|
.catch(err => {
|
|
|
|
// Don't throw 304 responses
|
|
|
|
if (err.statusCode === 304) {
|
|
|
|
return err.response;
|
|
|
|
}
|
|
|
|
throw err;
|
|
|
|
})
|
|
|
|
.then(response => {
|
|
|
|
// Format response
|
|
|
|
response = {
|
|
|
|
statusCode: response.statusCode,
|
|
|
|
statusMessage: response.statusMessage,
|
|
|
|
headers: response.headers,
|
|
|
|
body: response.body && JSON.parse(response.body)
|
|
|
|
};
|
|
|
|
|
|
|
|
return response;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Onionoo;
|