Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ $ npm install --save fast-speedtest-api
$ npm install --global fast-speedtest-api
$ fast-speedtest --help
fast-speedtest - speed test powered by fast.com
usage: fast-speedtest token [-v, --verbose] [-r, --raw] [-n, --no-https] [-t, --timeout timeout] [-c, --count url-count] [-b, --buffer buffer-size] [-u, --unit output-unit]
usage: fast-speedtest [token] [-v, --verbose] [-r, --raw] [-n, --no-https] [-t, --timeout timeout] [-c, --count url-count] [-b, --buffer buffer-size] [-u, --unit output-unit]
```

## Api usage
Expand All @@ -28,7 +28,7 @@ Example:
const FastSpeedtest = require("fast-speedtest-api");

let speedtest = new FastSpeedtest({
token: "your-app-token", // required
token: null, // default: null
verbose: false, // default: false
timeout: 10000, // default: 5000
https: true, // default: true
Expand All @@ -47,7 +47,7 @@ speedtest.getSpeed().then(s => {

## FAQ
### How to get app token ?
Go on [fast.com](https://fast.com/), open your browser devtools, go on `Network` tab and copy the token on the request url that looks like `https://api.fast.com/netflix/speedtest?https=true&token=<the-token>&urlCount=5`
Go on [fast.com](https://fast.com/), open your browser devtools, go on `Network` tab and copy the token on the request url that looks like `https://api.fast.com/netflix/speedtest?https=true&token=<the-token>&urlCount=5`. If no token is defined, then API try to parse token from speedtest website.

## TODO
- Better verbose mode
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"url": "git+https://github.com/branchard/fast-speedtest-api.git"
},
"author": "branchard",
"contributors": [
"honzahommer"
],
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/branchard/fast-speedtest-api/issues"
Expand Down
72 changes: 64 additions & 8 deletions src/Api.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const url = require('url');
const Timer = require('./Timer');
const ApiError = require('./ApiError');

const BASE_URL = 'fast.com';
const DEFAULT_SPEEDTEST_TIMEOUT = 5000; // ms
const DEFAULT_URL_COUNT = 5;
const DEFAULT_BUFFER_SIZE = 8;
Expand All @@ -14,15 +15,15 @@ class Api {
/**
* Create an Api object
*
* @param {object} options {token<string>, [verbose<boolean>, timeout<number>,
* @param {object} options {[token<string>, verbose<boolean>, timeout<number>,
* https<boolean>, urlCount<number>, bufferSize<number>, unit<function>]}
*/
constructor(options) {
constructor(options = {}) {
if (!options) {
throw new Error('You must define options in Api constructor');
}

if (!options.token) {
if (options.token && typeof options.token !== 'string') {
throw new Error('You must define app token');
}

Expand All @@ -34,7 +35,7 @@ class Api {
this.proxy = new HttpsProxyAgent(options.proxy);
}

this.token = options.token;
this.token = options.token || null;
this.verbose = options.verbose || false;
this.timeout = options.timeout || DEFAULT_SPEEDTEST_TIMEOUT;
this.https = options.https == null ? true : Boolean(options.https);
Expand All @@ -43,6 +44,23 @@ class Api {
this.unit = options.unit || Api.UNITS.Bps;
}

/**
* Create URL with correct protocol based on options
*
* @param {String} url
* @return {String} The created url
*/
createUrl(url = BASE_URL) {
if (/^https?:\/\//.test(url)) {
return url;
}

if (!/^\/\//.test(url)) {
url = `//${url}`
}

return `http${this.https ? 's' : ''}:${url}`;
}

/**
* Compute average from array of number
Expand Down Expand Up @@ -71,15 +89,18 @@ class Api {
async get(options) {
return new Promise((resolve, reject) => {
const request = (this.https ? https : http).get(options, (response) => {
if (response.headers['content-type'].includes('json')) {
const ctype = response.headers['content-type'];
if (['json', 'javascript', 'text'].some(r => ctype.includes(r))) {
response.setEncoding('utf8');
let rawData = '';
response.on('data', (chunk) => {
rawData += chunk;
});
response.on('end', () => {
const parsedData = JSON.parse(rawData);
response.data = parsedData;
response.rawData = rawData;
if (ctype.includes('json')) {
response.data = JSON.parse(rawData);
}
resolve({
response,
request,
Expand All @@ -105,10 +126,16 @@ class Api {
* @return {Array<string>} List of videos url
*/
async getTargets() {
if (!this.token) {
this.token = await this.getToken();
if (this.verbose) {
console.log(`API token: ${this.token}`);
}
}
try {
const targets = [];
while (targets.length < this.urlCount) {
const target = `http${this.https ? 's' : ''}://api.fast.com/netflix/speedtest?https=${this.https ? 'true' : 'false'}&token=${this.token}&urlCount=${this.urlCount - targets.length}`;
const target = this.createUrl(`api.${BASE_URL}/netflix/speedtest?https=${this.https ? 'true' : 'false'}&token=${this.token}&urlCount=${this.urlCount - targets.length}`);
const options = url.parse(target);
if (this.proxy) options.agent = this.proxy;
/* eslint-disable no-await-in-loop */
Expand Down Expand Up @@ -198,6 +225,33 @@ class Api {
timer.start();
});
}

async getToken() {
const url = this.createUrl();

let rawData;
let script;
let token;
let m;

({ response: { rawData } } = await this.get(url));
if ((m = /src=[\"'](?<script>[^"']app-.*\.js)[\"']/i.exec(rawData)) !== null) {
({ groups: { script } } = m || { groups: { } });
}
if (!script) {
throw new ApiError({ code: ApiError.CODES.NO_TOKEN });
}

({ response: { rawData } } = await this.get(`${url}/${script}`));
if ((m = /token:\s*[\"'](?<token>[^"']*)[\"']/i.exec(rawData)) !== null) {
({ groups: { token } } = m || { groups: { } });
}
if (!token) {
throw new ApiError({ code: ApiError.CODES.NO_TOKEN });
}

return token;
}
}

Api.UNITS = {
Expand All @@ -213,4 +267,6 @@ Api.UNITS = {
Gbps: rawSpeed => (rawSpeed * 8) / 1000000000,
};

Api.BASE_URL = BASE_URL;

module.exports = Api;
1 change: 1 addition & 0 deletions src/ApiError.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class ApiError extends Error {
ApiError.CODES = {
// CODE: Message
BAD_TOKEN: 'Unknown app token',
NO_TOKEN: 'Unable to parse token',
UNREACHABLE_HTTPS_API: 'Fast api is unreachable with https, try with http',
UNREACHABLE_HTTP_API: 'Fast api is unreachable, check your network connection',
PROXY_NOT_AUTHENTICATED: 'Failed to authenticate with provided proxy credentials',
Expand Down
13 changes: 9 additions & 4 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ if (args.includes('-h') || args.includes('--help')) {
process.exit(0);
}

const token = args[0];
const restArgs = args.slice(1);
let token;
let restArgs = args;

if (args[0] && !args[0].startsWith('-')) {
token = args[0];
restArgs = args.slice(1);
}

/* eslint-disable require-jsdoc */
function getArgParam(argName, fullArgName) {
Expand All @@ -30,8 +35,8 @@ function getArgParam(argName, fullArgName) {

/* eslint-enable require-jsdoc */

if (!token || typeof token !== 'string') {
throw new Error('You must define an app token');
if (token && typeof token !== 'string') {
throw new Error('App token must be string');
}

const verbose = restArgs.includes('-v') || restArgs.includes('--verbose');
Expand Down
29 changes: 29 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const assert = require('assert');
const Timer = require('../src/Timer');
const Api = require('../src/Api');

describe('Timer', () => {
describe('#constructor', () => {
Expand Down Expand Up @@ -32,3 +33,31 @@ describe('Timer', () => {
});
});
});

describe('Api', () => {
describe('#constructor', () => {
it('should return new Api instance', () => {
const api = new Api();
assert.ok(api instanceof Api);
});
});

describe('#createUrl', () => {
it('should create url with https', () => {
const url = (new Api()).createUrl();
assert.equal(`https://${Api.BASE_URL}`, url);
});

it('should create url without https', () => {
const url = (new Api({ https: false })).createUrl();
assert.equal(`http://${Api.BASE_URL}`, url);
});
});

describe('#getToken', () => {
it('should get token from speedtest website', () => {
const token = (new Api()).getToken();
assert.ok(token);
});
});
});