From c3e576ddba3dd59e1741458a89eaf53941bf4cac Mon Sep 17 00:00:00 2001 From: dosse91 Date: Sun, 30 Jun 2019 07:03:06 +0200 Subject: [PATCH] Major project restructuring and unification with mpot branch --- README.md | 38 +- backend/empty.php | 12 + garbage.php => backend/garbage.php | 6 +- getIP.php => backend/getIP.php | 7 + .../getIP_ipInfo_apikey.php | 0 doc.md | 790 ++++++++++++------ empty.php | 7 - example-customSettings.html | 200 ----- example-multipleServers-full.html | 393 +++++++++ ...tml => example-multipleServers-pretty.html | 129 +-- ...ic.html => example-singleServer-basic.html | 10 +- ...rt.html => example-singleServer-chart.html | 36 +- ...> example-singleServer-customSettings.html | 95 +-- ...ing.html => example-singleServer-full.html | 341 ++++---- ...s.html => example-singleServer-gauges.html | 293 ++++--- ...y.html => example-singleServer-pretty.html | 94 +-- ...l => example-singleServer-progressBar.html | 102 ++- {telemetry => results}/idObfuscation.php | 0 results/index.php | 4 +- {telemetry => results}/stats.php | 6 +- {telemetry => results}/telemetry.php | 4 + {telemetry => results}/telemetry_mysql.sql | 0 .../telemetry_postgresql.sql | 0 {telemetry => results}/telemetry_settings.php | 4 +- speedtest.js | 342 ++++++++ speedtest_worker.js | 315 +++---- speedtest_worker.min.js | 1 - 27 files changed, 2054 insertions(+), 1175 deletions(-) create mode 100644 backend/empty.php rename garbage.php => backend/garbage.php (85%) rename getIP.php => backend/getIP.php (95%) rename getIP_ipInfo_apikey.php => backend/getIP_ipInfo_apikey.php (100%) delete mode 100644 empty.php delete mode 100644 example-customSettings.html create mode 100644 example-multipleServers-full.html rename example-telemetryEnabled.html => example-multipleServers-pretty.html (57%) rename example-basic.html => example-singleServer-basic.html (61%) rename example-chart.html => example-singleServer-chart.html (90%) rename example-customSettings2.html => example-singleServer-customSettings.html (71%) rename example-telemetry-resultSharing.html => example-singleServer-full.html (51%) rename example-gauges.html => example-singleServer-gauges.html (59%) rename example-pretty.html => example-singleServer-pretty.html (77%) rename example-progressBar.html => example-singleServer-progressBar.html (75%) rename {telemetry => results}/idObfuscation.php (100%) rename {telemetry => results}/stats.php (97%) rename {telemetry => results}/telemetry.php (93%) rename {telemetry => results}/telemetry_mysql.sql (100%) rename {telemetry => results}/telemetry_postgresql.sql (100%) rename {telemetry => results}/telemetry_settings.php (92%) create mode 100644 speedtest.js delete mode 100644 speedtest_worker.min.js diff --git a/README.md b/README.md index 9b9eab6..d3c330f 100644 --- a/README.md +++ b/README.md @@ -10,50 +10,40 @@ This is a very lightweight Speedtest implemented in Javascript, using XMLHttpReq [Take a Speedtest](http://speedtest.fdossena.com) ## Compatibility -Only modern browsers are supported (IE11, latest Edge, latest Chrome, latest Firefox, latest Safari) +All modern browsers are supported: IE11, latest Edge, latest Chrome, latest Firefox, latest Safari. +Works with mobile versions too. ## Features * Download * Upload * Ping * Jitter -* IP Address +* IP Address, ISP, distance from server (optional) * Telemetry (optional) * Results sharing (optional) +* Multiple Points of Test (optional) -![Screenshot](https://speedtest.fdossena.com/screenshot.png) +![Screenshot](https://speedtest.fdossena.com/mpot_v5.gif) -## Requirements - - A reasonably fast web server with PHP (see doc.md for details and use without PHP) - - Your server must accept large POST requests (up to 20 Megabytes), otherwise the upload test will fail - - It's also better if your server does not use compression, but it's not mandatory +## Server requirements +* A reasonably fast web server with Apache 2 (nginx, IIS also supported) +* PHP 5.4 (other backends also available) +* MySQL database to store test results (optional, PostgreSQL and SQLite also supported) +* A fast! internet connection -## Quick installation videos -* [Debian 9.0 with Apache](https://fdossena.com/?p=speedtest/quickstart_deb.frag) -* [Windows Server 2016 with IIS](https://fdossena.com/?p=speedtest/quickstart_win.frag) -* [Ubuntu (External)](https://freedif.org/how-to-install-selfhosted-speedtest) - -Also, here's an [example config on Ubuntu 16 LTS](https://github.com/adolfintel/speedtest/issues/50) - -## How to use in your site -* See the examples -* [Read the wiki](https://github.com/adolfintel/speedtest/wiki) -* Read doc.md - -## Multiple test servers -Please see the ```mpot``` branch +## Installation videos +* [Quick start installation guide for Ubuntu Server 19.04](https://fdossena.com/?p=speedtest/quickstart_v5_ubuntu.frag) ## Docker -Please see the ```docker``` branch +Please see the `docker` branch ## Node.js backend -A Node.js implementation is available in the ```node``` branch, maintained by [dunklesToast](https://github.com/dunklesToast). +A Node.js implementation is available in the `node` branch, maintained by [dunklesToast](https://github.com/dunklesToast). ## Donate [![Donate with Liberapay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/fdossena/donate) [Donate with PayPal](https://www.paypal.me/sineisochronic) -Send ETH at this address: ```0x8A5273d4e2618c4cff2C62d8EB731701FceEd8E3``` ## License Copyright (C) 2016-2019 Federico Dossena diff --git a/backend/empty.php b/backend/empty.php new file mode 100644 index 0000000..e35f663 --- /dev/null +++ b/backend/empty.php @@ -0,0 +1,12 @@ + diff --git a/garbage.php b/backend/garbage.php similarity index 85% rename from garbage.php rename to backend/garbage.php index 351c496..e2fcc78 100644 --- a/garbage.php +++ b/backend/garbage.php @@ -5,13 +5,17 @@ @ini_set('output_handler', ''); // Headers header('HTTP/1.1 200 OK'); +if(isset($_GET["cors"])){ + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Methods: GET, POST'); +} // Download follows... header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=random.dat'); header('Content-Transfer-Encoding: binary'); // Never cache me -header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); +header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, s-maxage=0'); header('Cache-Control: post-check=0, pre-check=0', false); header('Pragma: no-cache'); // Generate data diff --git a/getIP.php b/backend/getIP.php similarity index 95% rename from getIP.php rename to backend/getIP.php index 4f21f31..7d7c400 100644 --- a/getIP.php +++ b/backend/getIP.php @@ -7,6 +7,13 @@ error_reporting(0); $ip = ""; header('Content-Type: application/json; charset=utf-8'); +if(isset($_GET["cors"])){ + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Methods: GET, POST'); +} +header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, s-maxage=0'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); if (!empty($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; } elseif (!empty($_SERVER['X-Real-IP'])) { diff --git a/getIP_ipInfo_apikey.php b/backend/getIP_ipInfo_apikey.php similarity index 100% rename from getIP_ipInfo_apikey.php rename to backend/getIP_ipInfo_apikey.php diff --git a/doc.md b/doc.md index 8cd83be..d3448a9 100644 --- a/doc.md +++ b/doc.md @@ -1,127 +1,255 @@ # HTML5 Speedtest > by Federico Dossena -> Version 4.7.2 +> Version 5.0 > [https://github.com/adolfintel/speedtest/](https://github.com/adolfintel/speedtest/) +The documentation is being rewritten. It will be done in a few days. ## Introduction -In this document, we will introduce an XHR based HTML5 Speedtest and see how to use it. -This test measures download speed, upload speed, ping and jitter. +HTML5 Speedtest is a Free and Open Source speedtest that you can host on your server(s), and users can run in their browser. -First of all, the requirements to run this test: +__Features:__ +* Download test +* Upload test +* Ping and Jitter test +* IP address, ISP and distance detection +* Telemetry (optional) +* Results sharing (optional) +* Multiple Points of Test (optional) -* The browser must support XHR Level 2 and Web Workers and Javascript must be enabled. - * Internet Explorer 11 - * Microsoft Edge 12+ - * Mozilla Firefox 12+ - * Google Chrome / Chromium 31+ - * Apple Safari 7.1+ - * Opera 18+ -* Client side, the test can use up to 500 megabytes of RAM -* Server side, you'll need a fast connection (at least 100 Mbps recommended), and the web server must accept large POST requests (up to 20 megabytes). - The recommended setup is: GNU/Linux, Apache2, PHP, MySQL database (if you want to use telemetry). +__Browser support:__ +The test supports any browser that supports XHR Level 2 and Web Workers. JavaScript must be enabled. -If this looks good, let's proceed and see how to use the test. +The following browsers are officially supported: +* Internet Explorer 11 +* Microsoft Edge (last 2 versions) +* Mozilla Firefox (latest ESR and last 2 versions) +* Google Chrome and other Chromium-based browsers (last 2 versions) +* Apple Safari (last 2 versions) +* Opera (last 2 versions) -## Quick installation videos -* [Debian 9.0 with Apache](https://fdossena.com/?p=speedtest/quickstart_deb.frag) -* [Windows Server 2016 with IIS](https://fdossena.com/?p=speedtest/quickstart_win.frag) +Client side, the test can use up to 500MB of RAM on very fast connections. + +## Quick start guides +These guides cover a simple single server installation of the Speedtest. + +* [Quick start installation guide for Ubuntu Server 19.04](https://fdossena.com/?p=speedtest/quickstart_v5_ubuntu.frag) + +More guides will be added later ## Installation -To install the test on your server, upload the following files: -* `speedtest_worker.min.js` -* `garbage.php` -* `getIP.php` -* `empty.php` -* one of the examples +### Single server, PHP +Server side, you'll need: +* Apache 2 (nginx and IIS also supported). A fast internet connection is required (possibly gigabit), and the web server must accept large POST requests (up to 20MB) +* PHP 5.4 or newer, a 64-bit version is strongly recommended +* OpenSSL and its PHP module (this is usually installed automatically by most distros) +* If you want to store test results (telemetry), one of the following: + - MySQL/MariaDB and the mysqli PHP module + - PostgreSQL and its PHP PDO module + - SQLite 3 and its PHP PDO module +* If you want to enable results sharing: + - FreeType 2 and its PHP module (this is usually installed automatically by most distros) -Later we'll see how to use the test without PHP, and how to configure the telemetry and result sharing if you want to use that. +Let's install the speedtest. -__Important:__ keep all the files together; all paths are relative to the js file +Put all files on your web server via FTP or by copying them directly. You can install it in the root, or in a subdirectory. -__Important:__ If you expect to serve more than ~500 tests per day, you will need to sign up to [ipinfo.io](https://ipinfo.io) and edit `getIP_ipInfo_apikey.php` to set your access token. IpInfo.io has kindly offered free access to their APIs for users of this project; if you're interested, contact me at [info@fdossena.com](mailto:info@fdossena.com) and provide a description of what you intend to do with the project, and you'll get the API key. This is only required if you intend to use ISP and distance detection. +__Important:__ The speedtest needs write permissions in the installation folder! -__Important:__ Make sure PHP is allowed to write to the directory where you're installing the speedtest because getIP.php needs to create a cache file to improve performance. +#### ipinfo.io +The speedtest uses [ipinfo.io](https://ipinfo.io) to detect ISP and distance from server. This is completely optional and can be disabled if you want (see Speedtest settings), but it is enabled by default, and if you expect more than ~500 tests per day, you will need to sign up to [ipinfo.io](https://ipinfo.io) and edit `backend/getIP_ipInfo_apikey.php` to set your access token. -## Basic usage -You can start using this speedtest on your site without any special knowledge. -Start by copying one of the included examples. Here's a description for each of them: -* `example-basic.html`: This example shows the most basic configuration possible. Everything runs with the default settings, in a very simple page where the output is shown -* `example-pretty.html`: This is a more sophisticated example, with a nicer layout and a start/stop button. __This is the best starting point for most users__ -* `example-progressBar.html`: A modified version of `example-pretty.html` with a progress indicator -* `example-customSettings.html`: A modified version of `example-pretty.html` showing how the test can be started with custom parameters -* `example-customSettings2.html`: A modified version of `example-pretty.html` showing how to make a custom test with only download and upload -* `example-gauges.html`: The most sophisticated example, with the same functions as `example-pretty.html` but also gauges and progress indicators for each test. This is the nicest example included, and also a good starting point, but drawing the gauges may slow down the test on slow devices like a Raspberry Pi -* `example-chart.html`: The old example5.html, showing how to use the test with the Chart.js library +IpInfo.io has kindly offered free access to their APIs for users of this project; if you're interested, contact me at [info@fdossena.com](mailto:info@fdossena.com) and provide a description of what you intend to do with the project, and you'll get the API key. -These 2 examples require some additional server configuration, discussed in the Telemetry section: -* `example-telemetry.html`: A modified version of `example-pretty.html` with basic telemetry turned on. See the section on Telemetry for details -* `example-telemetry-resultsSharing.html`: A modified version of `example-telemetry.html` with results sharing. This is the most complete and most complex example, showing off all of the speedtest features +#### Telemetry and results sharing +The test supports storing test results and can generate shareable images that users can embed in forum signatures and such. -### Customizing your example -The included examples are good starting places if you want to have a simple speedtest on your site. -Once you've tested everything and you're sure that everything works, edit it and add some custom stuff like your logo or new colors. -If you want to change the test parameters, for instance to make the download test shorter, you can do so in every example: -Look for the line that contains `postMessage('start ` -This is where custom parameters can be passed to the test as a JSON string. You can write the string manually or use ``JSON.stringify`` to do that for you. -Here's an example: -```js -w.postMessage('start {"time_dl":"10"}'); +To use this function, you will need a database. The test supports MySQL, PostgreSQL and SQLite as backends. + +##### Creating the database +This step is only required for MySQL and PostgreSQL. If you want to use SQLite, skip to the next step. + +Log into your database using phpMyAdmin or a similar software and create a new database. Inside the `results` folder you will find `telemetry_mysql.sql` and `telemetry_postgresql.sql`, which are templates for MySQL and PostgreSQL respectively. Import the one you need, and you will see a `speedtest_users` table in the database. You can delete the templates afterwards. + +##### Configuring telemetry +Open `results/telemetry_settings.php` in a text editor. Set `$db_type` to either `mysql`,`postgresql` or `sqlite`. + +If you chose to use SQLite, you might want to change `$Sqlite_db_file` to another path where you want the database to be stored. Just make sure that the file cannot be downloaded by users. Sqlite doesn't require any additional configuration, you can skip the rest of this section. + +If you chose to use MySQL, you must set your database credentials: +```php +$MySql_username="USERNAME"; //your database username +$MySql_password="PASSWORD"; //your database password +$MySql_hostname="DB_HOSTNAME"; //database address, usually localhost +$MySql_databasename="DB_NAME"; //the name of the database where you loaded telemetry_mysql.sql ``` -This starts the test with default settings, but sets the download test to last only 10 seconds. -Here's a cleaner version using ``JSON.stringify``: + +If you chose to use PostgreSQL, you must set your database credentials: +```php +$PostgreSql_username="USERNAME"; //your database username +$PostgreSql_password="PASSWORD"; //your database password +$PostgreSql_hostname="DB_HOSTNAME"; //database address, usually localhost +$PostgreSql_databasename="DB_NAME"; //the name of the database where you loaded telemetry_postgresql.sql +``` + +##### Results sharing +This feature generates an image that can be share by the user containing the download, upload, ping, jitter and ISP (if enabled). + +By default, the telemetry generates a progressive ID for each test. Even if no sensitive information is leaked, you might not want users to be able to guess other test IDs. To avoid this, you can turn on ID obfuscation, which turns IDs into a reversible hash, much like YouTube video IDs. + +To enable ID obfuscation, edit `results/telemetry_settings.php` and set `$enable_id_obfuscation` to `true`. From now on, all test IDs will be obfuscated using a unique salt. The IDs in the database are still progressive, but users will only know their obfuscated versions and won't be able to easily guess other IDs. + +__Important:__ ID obfuscation currently only works on 64-bit PHP! + +##### Seeing the results +A basic front-end for visualizing and searching tests by ID is available in `results/stats.php`. + +A login is required to access the interface. __Important__: change the default password in `results/telemetry_settings.php`. + +#### The end +Now that the test is installed, rename one of the examples to `index.html` and delete the other examples. +The best starting point for most people is `example-singleServer-pretty.html`. If you want to use telemetry and results sharing, use `example-singleServer-full.html` instead. + +If you don're not using telemetry and results sharing, you can delete the `results` folder too. + +Details about the examples and how to make custom UIs will be discussed later. + +### Multiple servers, PHP +The speedtest can automatically choose between multiple test points and use the one with the lowest ping in a list. + +Note that this is an advanced use case and it is recommended that you already know how to use the speedtest with a single server. + +We must distinguish 2 types of servers: +* __Frontend server__: hosts the UI, the JS files, and optionally telemetry and results sharing stuff. You only need 1 of these, and this is the server that your clients will first connect to. +* __Test backends__: the servers used to actually perform the test. There can be 1+ of these, and they only host the backend files. + +#### Frontend server +This is the server that your users will first connect to. It hosts the UI, the JS files, and optionally telemetry and results sharing stuff. + +Requirements: +* Apache 2 (nginx and IIS also supported). A fast connection is not mandatory, but is still recommended +* PHP 5.4 or newer +* If you want to store test results (telemetry), one of the following: + - MySQL/MariaDB and the mysqli PHP module + - PostgreSQL and its PHP PDO module + - SQLite 3 and its PHP PDO module +* If you want to enable results sharing: + - FreeType 2 and its PHP module (this is usually installed automatically by most distros) + +To install the speedtest frontend, copy the following files to your web server: +* `speedtest.js` +* `speedtest_worker.js` +* Optionally, the `results` folder +* One of the `multipleServers` examples (the best starting points are `example-multipleServers-pretty.html` if you don't want to use telemetry and results sharing, `example-multipleServers-full.html` if you want to use them). Rename the example you choose to `index.html` + +__Important:__ The speedtest needs write permissions in the installation folder! + +##### Server list +Edit `index.html`, you will see a list of servers: ```js -var params={ - time_dl:10 +var SPEEDTEST_SERVERS=[ + { + name:"Speedtest Demo Server 1", //user friendly name for the server + server:"//mpotdemo.fdossena.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically + dlURL:"garbage.php", //path to download test on this server (garbage.php or replacement) + ulURL:"empty.php", //path to upload test on this server (empty.php or replacement) + pingURL:"empty.php", //path to ping/jitter test on this server (empty.php or replacement) + getIpURL:"getIP.php" //path to getIP on this server (getIP.php or replacement) + }, + { + name:"Speedtest Demo Server 2", + server:"//mpotdemo2.fdossena.com/", + dlURL:"garbage.php", + ulURL:"empty.php", + pingURL:"empty.php", + getIpURL:"getIP.php" + } + //add other servers here, comma separated +]; +``` + +Replace the demo servers with your test points. Each server in the list is an object containing: +* `name`: user friendly name for this test point +* `server`: URL to the server. If your server only supports HTTP or HTTPS, put http:// or https:// at the beginning, respectively; if it supports both, put // at the beginning and it will be replaced automatically +* `dlURL`: path to the download test on this server (garbage.php or replacement) +* `ulURL`: path to the upload test on this server (empty.php or replacement) +* `pingURL`: path to the ping test on this server (empty.php or replacement) +* `getIpURL`: path to getIP on this server (getIP.php or replacement) + +None of these parameters can be omitted. + +__Important__: You can't mix HTTP with HTTPS; if the frontend uses HTTP, you won't be able to connect to HTTPS backends, and viceversa. + +__Important__: For HTTPS, all your servers must have valid certificates or the browser will refuse to connect + +__Important__: Don't use my demo servers, they're slow! + +##### Telemetry and results sharing +Telemetry is stored on the frontend server. The setup procedure is the same as the single server version. + +#### Test backends +These are the servers that will actually be used to perform the test. + +Requirements: +* Apache 2 (nginx and IIS also supported). A fast internet connection is required (possibly gigabit), and the web server must accept large POST requests (up to 20MB) +* PHP 5.4 or newer +* OpenSSL and its PHP module (this is usually installed automatically by most distros) + +To install a backend, simply copy all the files in the `backend` folder to your backend server. + +__Important:__ The speedtest needs write permissions in the installation folder! + +#### ipinfo.io +The speedtest uses [ipinfo.io](https://ipinfo.io) to detect ISP and distance from server. This is completely optional and can be disabled if you want (see Speedtest settings), but it is enabled by default, and if you expect more than ~500 tests per day, you will need to sign up to [ipinfo.io](https://ipinfo.io) and edit `getIP_ipInfo_apikey.php` to set your access token. + +IpInfo.io has kindly offered free access to their APIs for users of this project; if you're interested, contact me at [info@fdossena.com](mailto:info@fdossena.com) and provide a description of what you intend to do with the project, and you'll get the API key. + +## Making a custom front-end +This section explains how to use speedtest.js in your webpages. + +The best way to learn is by looking at the provided examples. + +__Single server:__ +* `example-singleServer-basic.html`: The most basic configuration possible. Runs the test with the default settings when the page is loaded and displays the results with no fancy graphics. +* `example-singleServer-pretty.html`: A more sophisticated example with a nicer layout and a start/stop button. __This is the best starting point for most users__ +* `example-singleServer-progressBar.html`: Same as `example-singleServer-pretty.html` but adds a progress indicator +* `example-singleServer-customSettings.html`: Same as `example-singleServer-pretty.html` but configures the test so that it only performs download and upload tests, and with a fixed length instead of automatic +* `example-singleServer-gauges.html`: The most sophisticated example, with the same functionality as `example-singleServer-pretty.html` but adds gauges. This is also a good starting point, but the gauges may slow down underpowered devices +* `example-singleServer-chart.html`: Shows how to use the test with the Chart.js library +* `example-singleServer-full.html`: The most complete example. Based on `example-singleServer-gauges.html`, also enables telemetry and results sharing + +__Multiple servers:__ +* `example-multipleServers-pretty.html`: Same as `example-singleServer-pretty.html` but with multiple test points. Server selection is fully automatic +* `example-multipleServers-full.html`: Same as `example-singleServer-full.html` but with multiple test points. Server selection is automatic but the server can be changed afterwards by the user + +### Initialization +To use the speedtest in your page, first you need to load it: +```xml + +``` + +After loading, you can initialize the test: +```js +var s=new Speedtest(); +``` + +### Event handlers +Now, you can set up event handlers to update your UI: +```js +s.onupdate=function(data){ + //update your UI here } -w.postMessage('start '+JSON.stringify(params)) -``` -Notice that there is a space after the word `start`, don't forget that! - -For a list of all test settings, look below, under Test parameters and Advanced test parameters. __Do not change anything if you don't know what you're doing.__ - -## Advanced usage -If you don't want to start from one of the examples, here's how to use the worker. Examples are still good for reference, so keep them handy. - -To run the test, you need to do 3 things: - -* Create the worker -* Write some code that handles the data coming from the worker -* Start the test - -### Creating the worker -```js -var w = new Worker("speedtest_worker.min.js") -``` - -__Important:__ use the minified version, it's smaller! - -### Response handler -First, we set up a timer that fetches the status of the worker continuously: -```js -var timer = setInterval(function () { - w.postMessage('status') -}, 100) -``` - -Then we write a response handler that receives the status and updates the page. Later -we'll see the details of the format of the response. - -```js -w.onmessage = function (event) { - var data = JSON.parse(event.data); - if (data.testState >= 4) { - clearInterval(timer) // test is finished or aborted - } - // .. update your page here .. +s.onend=function(aborted){ + //end of the test + if(aborted){ + //something to do if the test was aborted instead of ending normally + } } ``` -#### Response format -The response from the worker is a JSON string containing these entries: - +The `onupdate` event handler will be called periodically by the test with data coming from the speedtest worker thread. The `data` argument is an object containing the following: * __testState__: an integer between -1 and 5 * `-1` = Test not started yet * `0` = Test starting @@ -152,31 +280,23 @@ The response from the worker is a JSON string containing these entries: * __dlProgress__: the progress of the download test as a number between 0 and 1 * __ulProgress__: the progress of the upload test as a number between 0 and 1 * __pingProgress__: the progress of the ping+jitter test as a number between 0 and 1 -* __testId__: when telemetry is active, this is the ID of the test in the database. This string is null until the test is finished (testState 4), or if telemetry encounters an error. This ID is used for results sharing +* __testId__: when telemetry is active, this is the ID of the test in the database. This is null until the test is finished, or if telemetry encounters an error. This ID is used for results sharing -### Starting the test -To start the test with the default settings, which is usually the best choice, send the start command to the worker: +The `onend` event handler will be called at the end of the test (`onupdate` will be called first), with a boolean telling you if the test was aborted (either manually or because of an error) or if it ended normally. +### Test parameters +Before starting the test, you can change some of the settings from their default values. You might want to do this to better adapt the speedtest to a specific scenario, such as a satellite connection. To change a setting, use ```js -w.postMessage('start') +s.setParameter("parameter_name",value); ``` -If you want, you can change these settings and pass them to the worker as JSON when you start it, like this: - +For instance, to enable telemetry we can use: ```js -w.postMessage('start {"param1": "value1", "param2": "value2", ...}') -``` -or this: -```js -var params{ - param1:value1, - param2:value2, - ... -} -w.postMessage('start '+JSON.stringify(params)) +s.setParameter("telemetry_level","basic"); ``` +And now the test results will be stored and we will get our test ID at the end of the test (along with the other data) -#### Test parameters +__Main parameters:__ * __time_dl_max__: Maximum duration of the download test in seconds. If auto duration is disabled, this is used as the duration of the test. * Default: `15` * Recommended: `>=5` @@ -204,9 +324,12 @@ w.postMessage('start '+JSON.stringify(params)) * Default: `telemetry/telemetry.php` * __Important:__ path is relative to js file * __Note:__ you can ignore this parameter if you're not using the telemetry - -#### Advanced test parameters -* __test_order__: the order in which tests will be performed. Each character represents an operation: +* __telemetry_level__: The type of telemetry to use. See the telemetry section for more info about this + * Default: `none` + * `basic`: send results only + * `full`: send results and timing information, even for aborted tests + * `debug`: same as full but also sends debug information. Not recommended. +* __test_order__: the order in which tests will be performed. You can use this to change the order of the test, or to only enable specific tests. Each character represents an operation: * `I`: get IP * `D`: download test * `U`: upload test @@ -222,6 +345,9 @@ w.postMessage('start '+JSON.stringify(params)) * `mi`: estimate distance in miles * not set: do not measure distance * Default: `km` + +__Advanced parameters:__ (Seriously, don't change these unless you know what you're doing) +* __telemetry_extra__: Extra data that you want to be passed to the telemetry. This is a string field, if you want to pass an object, make sure you use ``JSON.stringify``. This string will be added to the database entry for this test. * __enable_quirks__: enables browser-specific optimizations. These optimizations override some of the default settings. They do not override settings that are explicitly set. * Default: `true` * __garbagePhp_chunkSize__: size of chunks sent by garbage.php in megabytes @@ -270,135 +396,326 @@ w.postMessage('start '+JSON.stringify(params)) * `1514 / 1460`: TCP+IPv4+ETH, ignoring HTTP overhead * `1514 / 1440`: TCP+IPv6+ETH, ignoring HTTP overhead * `1`: ignore overheads. This measures the speed at which you actually download and upload files rather than the raw connection speed -* __telemetry_level__: The type of telemetry to use. See the telemetry section for more info about this - * Default: `none` - * `basic`: send results only - * `full`: send results and timing information, even for aborted tests - * `debug`: same as full but also sends debug information. Not recommended. -* __telemetry_extra__: Extra data that you want to be passed to the telemetry. This is a string field, if you want to pass an object, make sure you use ``JSON.stringify``. This string will be added to the database entry for this test. - -### Aborting the test prematurely -The test can be aborted at any time by sending an abort command to the worker: +### Multiple Points of Test +If you want to use more than one test server, this is the time to add all your test points and select the best one. Skip this part if you don't want to use this feature. + +The best way to do this is to declare an array with all your servers, and give it to the speedtest: ```js -w.postMessage('abort') +var SPEEDTEST_SERVERS=[ + server1, + server2, + ... +]; +s.addTestPoints(SPEEDTEST_SERVERS); ``` -This will terminate all network activity and stop the worker. +Each server in the list is an object containing: +* `name`: user friendly name for this test point +* `server`: URL to the server. If your server only supports HTTP or HTTPS, put `http://` or `https://` at the beginning, respectively; if it supports both, put `//` at the beginning and it will be replaced automatically +* `dlURL`: path to the download test on this server (garbage.php or replacement) +* `ulURL`: path to the upload test on this server (empty.php or replacement) +* `pingURL`: path to the ping test on this server (empty.php or replacement) +* `getIpURL`: path to getIP on this server (getIP.php or replacement) -__Important:__ do not simply kill the worker while it's running, as it may leave pending XHR requests! +None of these parameters can be omitted. -### Important notice on backwards compatibility -__Do NOT link the js file from github or fdossena.com directly into your html file. __ +Example: +```js +{ + name:"Milano, IT", + server:"http://backend1.myspeedtest.net/", + dlURL:"garbage.php", + ulURL:"empty.php", + pingURL:"empty.php", + getIpURL:"getIP.php" +} +``` -A lot of web developers think that referring to the latest version of a library in their project is a good thing. It is not. -Things may change and I don't want to break your project, so do yourself a favor, and keep all files on your server. -You have been warned. +Now, we can run the server selector: +```js +s.selectServer(function(server){ + //do something +}) +``` +The `selectServer` function is asynchronous in order to avoid freeing the UI, and it will run a callback function when it is done choosing the server with the lowest ping. +The `server` argument is the selected server, and you can display it in the UI if you want. __You cannot start the test until the selection is done!__ -## Using the test without PHP -If your server does not support PHP, or you're using something newer like Node.js, you can still use this test by replacing `garbage.php`, `empty.php` and `getIP.php` with equivalents. +You can also set the test point manually (for instance, from a combobox in the UI): +```js +s.setSelectedServer(server) +``` +where `server` is the server that you want to use. -### Replacements +### Running the test +Finally, we can run the test: +```js +s.start(); +``` +During the test, your `onupdate` event handler will be called periodically with data that you can use to update your UI. Your `onend` handler will be called at the end of the test. + +You can abort the test at any time: +```js +s.abort(); +``` + +When the test is finished, you can run it again if you want, or you can just destroy `s`. + +## Implementation details +The purpose of this section is to help developers who want to make changes to the inner workings of the speedtest. +It will be divided into 4 sections: `speedtest.js`, `speedtest_worker.js`, the `backend` files and the `resuls` files. + +### `speedtest.js` +This is the main interface between your webpage and the speedtest. +It hides the speedtest web worker to the page, and provides many convenient functions to control the test. + +You can think of this as a finite state machine. These are the states (use getState() to see them): +* __0__: here you can change the speedtest settings (such as test duration) with the `setParameter("parameter",value)` function. From here you can either start the test using `start()` (goes to state 3) or you can add multiple test points using `addTestPoint(server)` or `addTestPoints(serverList)` (goes to state 1). Additionally, this is the perfect moment to set up callbacks for the `onupdate(data)` and `onend(aborted)` events. +* __1__: here you can add test points. You only need to do this if you want to use multiple test points. + A server is defined as an object like this: + ``` + { + name: "User friendly name", + server:"http://yourBackend.com/", <---- URL to your server. You can specify http:// or https://. If your server supports both, just write // without the protocol + dlURL:"garbage.php" <----- path to garbage.php or its replacement on the server + ulURL:"empty.php" <----- path to empty.php or its replacement on the server + pingURL:"empty.php" <----- path to empty.php or its replacement on the server. This is used to ping the server by this selector + getIpURL:"getIP.php" <----- path to getIP.php or its replacement on the server + } + ``` + While in state 1, you can only add test points, you cannot change the test settings. When you're done, use selectServer(callback) to select the test point with the lowest ping. This is asynchronous, when it's done, it will call your callback function and move to state 2. Calling setSelectedServer(server) will manually select a server and move to state 2. +* __2__: test point selected, ready to start the test. Use `start()` to begin, this will move to state 3 +* __3__: test running. Here, your `onupdate` event calback will be called periodically, with data coming from the worker about speed and progress. A data object will be passed to your `onupdate` function, with the following items: + - `dlStatus`: download speed in mbps + - `ulStatus`: upload speed in mbps + - `pingStatus`: ping in ms + - `jitterStatus`: jitter in ms + - `dlProgress`: progress of the download test as a float 0-1 + - `ulProgress`: progress of the upload test as a float 0-1 + - `pingProgress`: progress of the ping/jitter test as a float 0-1 + - `testState`: state of the test (-1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=aborted) + - `clientIp`: IP address of the client performing the test (and optionally ISP and distance) + At the end of the test, the `onend` function will be called, with a boolean specifying whether the test was aborted or if it ended normally. + The test can be aborted at any time with `abort()`. + At the end of the test, it will move to state 4 +* __4__: test finished. You can run it again by calling `start()` if you want. + +#### List of functions in the Speedtest class + +##### getState() +Returns the state of the test: 0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done + +##### setParameter(parameter,value) +Change one of the test settings from their defaults. +- parameter: string with the name of the parameter that you want to set +- value: new value for the parameter + +Invalid values or nonexistant parameters will be ignored by the speedtest worker. + +##### addTestPoint(server) +Add a test point (multiple points of test) +- server: the server to be added as an object. Must contain the following elements: + ``` + { + name: "User friendly name", + server:"http://yourBackend.com/", URL to your server. You can specify http:// or https://. If your server supports both, just write // without the protocol + dlURL:"garbage.php" path to garbage.php or its replacement on the server + ulURL:"empty.php" path to empty.php or its replacement on the server + pingURL:"empty.php" path to empty.php or its replacement on the server. This is used to ping the server by this selector + getIpURL:"getIP.php" path to getIP.php or its replacement on the server + } + ``` + +Note that this will add `mpot`:`true` to the parameters sent to the speedtest worker. + +##### addTestPoints(list) +Same as addTestPoint, but you can pass an array of servers + +##### getSelectedServer() +Returns the selected server (multiple points of test) + +##### setSelectedServer() +Manually selects one of the test points (multiple points of test) + +##### selectServer(result) +Automatically selects a server from the list of added test points. The server with the lowest ping will be chosen. (multiple points of test) + +The selector checks multiple servers in parallel (default: 6 streams) to speed things up if the list of servers is long. + +The process is asynchronous and the passed `result` callback function will be called when it's done, then the test can be started. + +##### start() +Starts the test. + +Note (multiple points of test): the selected server will be added to the `telemetry_extra` string. If this string was already set, then `telemetry_extra` will be a JSON string containing both the server and the original string + +During the test, the `onupdate(data)` callback function will be called periodically with data from the worker. +At the end of the test, the `onend(aborted)` function will be called with a boolean telling you if the test was aborted or if it ended normally. + +##### abort() +Aborts the test while it's running. + +### `speedtest_worker.js` +This is where the actual speedtest code is. It receives the settings from the main thread, runs the test, and reports back the results. + +The worker accepts 3 commands: +* `start`: starts the test. Optionally, test settings can be passed as a JSON string after the word start and a space +* `status`: returns the current status as a JSON string. The status string contents are the ones described in the Event handlers section in the section about making a custom front-end. +* `abort`: aborts the test + +#### Parameters +In addition to the parameters listed in the Test settings section in the section about making a custom front-end, there is one additional setting: +* `mpot`: set this to true to run the test with multiple points of test. This will add `cors=true` to all requests (all responses will contain CORS headers) and enable some extra quirks. + Default: `false` + +#### Download test +The download test is performed by transferring large blobs of garbage data using XHR from the server to the client. + +The test uses multiple streams. If a stream finishes the download, it is restarted. The amount of downloaded data for each stream is tracked using the XHR Level 2 `onprogress` event. + +The test streams are not perfectly synchronized because we don't want them to finish all at the same time if they do. + +Every 200ms, a timer updates the `dlStatus` string with the current speed and calculates a "bonus" time by which to shorten the test depending on how high the speed is, (when `time_auto` is set to `true`). + +See the code for more implementation details. + +#### Upload test +This works similarly to the download test, but in reverse. A large blob of garbage data is generated and it is sent to the server repeatedly using multiple streams. + +To keep track of the amount of transferred data, the XHR Level 2 `upload.onprogress` event is used. + +This test has a couple of complications: +* Some browsers don't have a working `upload.onprogress` event. For this, we use a small blobs instead of a large one and we keep track of progress using the `onload` event. This is referred to as IE11 Workaround (but the same bug was also found in some versions of Edge and Safari) +* When `mpot` is set to `true`, an empty request must first be sent in order to load the CORS headers before the test can start + +See the code for more implementation details. + +#### Ping + Jitter test +The Ping/Jitter test __is NOT an ICMP ping__. This is a common misconsception. You cannot use ICMP over HTTP, and certainly not in a browser. + +This test works by creating a persistent HTTP connection to the server, and then repeatedly downloading an empty file, and measuring how long it takes between the request and the response. + +Timing can be measured as a simple timestamp difference or with the Performance API if available. + +Jitter is the variance in ping times. + +See the code for more implementation details. + +### `backend` files +#### `garbage.php` +Uses OpenSSL to generate a stream of incompressible garbage data for the download test. + +If accepts a `ckSize` GET parameter, which specifies how much garbage data to generate in megabytes (4-1024). + +#### `empty.php` +An empty file used for the upload and ping test. It only sends headers to create the connection. + +#### `getIP.php` +Returns client IP, ISP and distance from the server. + +GET parameters: +* `isp`: if set, fetches ISP info from ipinfo.io +* `distance`: if set, calculates distance from server. You can specify `km` or `mi` for the format. + +If `isp` is set, the output is a JSON string containing: +* `processedString`: string that can be displayed to the user +* `rawIspInfo`: info about the client as a JSON string, straight from ipinfo.io + +If `isp` is not set, the output is just a string containing the client's IP address. + +Note: if your server is behind some proxy, firewall, VPN, etc., the client's IP address may not be detected properly. If this happens, you must analyze the traffic coming from the client to find the name of the HTTP header that contains the original IP address. `getIP.php` contains some of these headers but not all of them. + +#### CORS headers +All these files will send the following CORS headers if the GET parameter `cors=true` is passed to them: +``` +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST +Access-Control-Allow-Headers: Content-Encoding, Content-Type +``` + +### `results` files +#### `telemetry.php` +This file stores telemetry information into the database. + +Data is passed as POST parameters: +* `ispinfo`: ISP info (if enabled, empty strng otherwise) +* `extra`: the `telemetry_extra` string passed to the worker (if set, empty string otherwise) +* `dl`: download speed +* `ul`: upload speed +* `ping`: ping time +* `jitter`: jitter value +* `log`: telemetry log (if `telemetry_level` is set to `full` or higher, empty string otherwise) + +#### `index.php` +Generates a shareable results image for a given test ID. + +GET parameters: +* `id`: ID of the test you want to display + +The looks of this image can be customized by editing the variables in this file. + +#### `idObfuscation.php` +Contains the implementation of ID obfuscation and deobfuscation. + +See the code for the implementation details, it's basically a bunch of bitwise operations. + +#### `stats.php` +Simple UI to display and search test results. Not required to run the test. + +## Alternative backends +If for some reason you can't or don't want to use PHP, the speedtest can run with other backends, or even no backend (with limited functionality). + +You will need replacements for `backend/garbage.php` and `backend/empty.php` and optionally `backend/getIP.php`, and the test needs to know where to find them: +```js +//Speedtest initialization +var s=new Speedtest(); +... +//Custom backend +s.setParameter("url_dl","URL to your garbage.php replacement"); +s.setParameter("url_ul","URL to your empty.php replacement"); +s.setParameter("url_ping","URL to your empty.php replacement"); +s.setParameter("url_getIp","URL to your getIP.php replacement"); +``` #### Replacement for `garbage.php` A replacement for `garbage.php` must generate incompressible garbage data. -A large file (10-100 Mbytes) is a possible replacement. You can get [one here](http://downloads.fdossena.com/geth.php?r=speedtest-bigfile). +A large file (10-100 Mbytes) is a possible replacement. You can get one [here](http://downloads.fdossena.com/geth.php?r=speedtest-bigfile). -If you're using Node.js or some other server, your replacement should accept the `ckSize` parameter (via GET) which tells it how many megabytes of garbage to generate. -It is important here to turn off compression, and generate incompressible data. A symlink to `/dev/urandom` is also ok. +If you want to make your own backend, see the section on the implementation details of `garbage.php`. + #### Replacement for `empty.php` -Your replacement must simply respond with a HTTP code 200 and send nothing else. You may want to send additional headers to disable caching. The test assumes that Connection:keep-alive is sent by the server. +Your replacement must simply respond with a HTTP code 200 and send nothing else. You may want to send additional headers to disable caching. The test assumes that `Connection:keep-alive` is sent by the server. + An empty file can be used for this. +If you want to make your own backend, see the section on the implementation details of `empty.php`. + #### Replacement for `getIP.php` Your replacement can simply respond with the client's IP as plaintext or do something more fancy. -If you want, you can also accept the `isp=true` parameter and also include the ISP info. In this case, return a JSON string containing a string called `processedString` with the text that you want to be displayed in the IP address field, and an object called `rawIspInfo` containing whatever you want (will be included in telemetry if enabled). -#### JS -You need to start the test with your replacements like this: +If you want to make your own backend, see the section on the implementation details of `getIP.php`. +### No backend +The speedtest can run, albeit with limited functionality, using only a web server as backend, with no PHP or other server-side scripting. + +You will be able to run the download and upload test, but no IP, ISP and distance detection, no telemetry and results sharing, and only a single point of test. + +To do this, you will need: +* A replacement for `garbage.php`: a large incompressible file, like [this](http://downloads.fdossena.com/geth.php?r=speedtest-bigfile). We'll call this `backend/garbage.dat` +* A replacement for `empty.php`: an empty file will do. We'll call this `backend/empty.dat` + +Now you need to configure the test to use them. Look for `s=new Speedtest()` and right below it, put the following: ```js -w.postMessage('start {"url_dl": "newGarbageURL", "url_ul": "newEmptyURL", "url_ping": "newEmptyURL", "url_getIp": "newIpURL"}') -``` -## Telemetry -Telemetry currently requires PHP and either MySQL, PostgreSQL or SQLite. -To set up the telemetry, we need to do 4 things: -* copy the `telemetry` folder -* edit `telemetry_settings.php` to add your database settings -* create the database -* enable telemetry - -### Creating the database -This step is only for MySQL and PostgreSQL. -Log into your database using phpMyAdmin or a similar software and import the appropriate sql file into an empty database. For MySQL databases use `telemetry_mysql.sql` and for PostgreSQL databases use `telemetry_postgesql.sql`. They're inside the `telemetry` folder. You can delete the files afterwards. -If you see a table called `speedtest_users`, empty, you did it right. - -### Configuring `telemetry.php` -Open `telemetry_settings.php` with notepad or a similar text editor. -Set your preferred database, ``$db_type="mysql";``, ``$db_type="sqlite";`` or ``$db_type="postgresql";`` - -If you choose to use Sqlite3, you must set the path to your database file: -```php -$Sqlite_db_file = "../../telemetry.sql"; -``` -Sqlite doesn't require any additional configuration. - -If you choose to use MySQL, you must also add your database credentials: -```php -$MySql_username="USERNAME"; //your database username -$MySql_password="PASSWORD"; //your database password -$MySql_hostname="DB_HOSTNAME"; //database address, usually localhost -$MySql_databasename="DB_NAME"; //the name of the database where you loaded telemetry_mysql.sql +s.setParameter("url_dl","backend/garbage.dat"); +s.setParameter("url_ul","backend/empty.dat"); +s.setParameter("url_ping","backend/empty.dat"); +s.setParameter("test_order","P_D_U"); ``` -If you choose to use PostgreSQL, you must also add your database credentials: -```php -$PostgreSql_username="USERNAME"; //your database username -$PostgreSql_password="PASSWORD"; //your database password -$PostgreSql_hostname="DB_HOSTNAME"; //database address, usually localhost -$PostgreSql_databasename="DB_NAME"; //the name of the database where you loaded telemetry_postgresql.sql -``` - -### Enabling telemetry -Edit your test page; where you start the worker, you need to specify the `telemetry_level`. -There are 3 levels: -* `none`: telemetry is disabled (default) -* `basic`: telemetry collects IP, ISP info, User Agent, Preferred language, Test results -* `full`: in addition to the basic telemetry, timing information is also collected so you know how long each part of the test took -* `debug`: same as full, but also collects a debug log (10-150 Kb each, not recommended unless you're developing the speedtest) - -Example: -```js -w.postMessage('start {"telemetry_level":"basic"}') -``` - -You can use example-telemetryEnabled.html and example-telemetry-resultSharing.html as starting points. - -### Results sharing -This feature generates an image that can be share by the user containing the download, upload, ping, jitter and ISP (if enabled). - -To use this feature, copy the `results` folder. You can customize the style of the generated image by editing the settings in `results/index.php`. - -This feature requires Telemetry to be enabled, and FreeType2 must be installed in PHP (if not already be installed by your distro). - -__Important:__ This feature relies on PHP functions `imagefttext` and `imageftbbox` that are well known for being problematic. The most common problem is that they can't find the font files and therefore nothing is drawn. This problem is metioned [here](http://php.net/manual/en/function.imagefttext.php) and was experienced by a lot of users. - -#### Obfuscated Test IDs -By default, the telemetry generates a progressive ID for each test. Even if no sensitive information is leaked, you might not want users to be able to guess other test IDs. To avoid this, you can turn on ID obfuscation, which turns IDs into a reversible hash, much like YouTube video IDs. - -To enable this feature, edit `telemetry_settings.php` and set `enable_id_obfuscation` to true. - -From now on, all test IDs will be obfuscated using a unique salt. The IDs in the database are still progressive, but users will only know their obfuscated versions and won't be able to easily guess other IDs. - -__Important:__ Make sure PHP is allowed to write to the `telemetry` folder. The salt will be stored in a file called `idObfuscation_salt.php`. This file is like a private key, don't lose it or you won't be able to deobfuscate IDs anymore! - -### Seeing the results -A basic front-end for visualizing and searching tests by ID is available in `telemetry/stats.php`. - -A login is required to access the interface. __Important__: change the default password in `telemetry_settings.php`. +This will point to our static files and set the test to only do ping/jitter, download and uplod tests. ## Troubleshooting These are the most common issues reported by users, and how to fix them. If you still need help, contact me at [info@fdossena.com](mailto:info@fdossena.com). @@ -412,17 +729,17 @@ If a small download starts, open it in a text editor. Does it say it's missing o Check your server's maximum POST size, make sure it's at least 20Mbytes, possibly more #### Download and/or upload results are slightly too optimistic -The test was fine tuned to run over a typical IPv4 internet connection. If you're using it under different conditions, see the ``overheadCompensationFactor`` parameter. +The test was fine tuned to run over a typical IPv4 internet connection. If you're using it under different conditions, see the `overheadCompensationFactor` parameter. #### All tests are wrong, give extremely high results, browser lags/crashes, ... You're running the test on localhost, therefore it is trying to measure the speed of your loopback interface. The test is meant to be run over an Internet connection, from a different machine. #### Ping test shows double the actual ping -Make sure your server is sending the ```Connection:keep-alive``` header +Make sure your server is sending the `Connection:keep-alive` header #### The server is behind a load balancer, proxy, etc. and I get the wrong IP address -Edit getIP.php and replace lines 5-13 with what is more appropriate in your scenario. -Example: ```$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];``` +Edit getIP.php and replace lines 14-23 with what is more appropriate in your scenario. +Example: `$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];` #### The results sharing just generates a blank image If the image doesn't display and the browser displays a broken image icon, FreeType2 is not installed or configured properly. @@ -457,34 +774,28 @@ This is a configuration issue. Make a file called web.config in wwwroot and adap ``` #### ID obfuscation doesn't work (incorrect output, blank results image) -ID obfuscation only works on 64 bit PHP (requires PHP_INT_SIZE to be 8). +ID obfuscation only works on 64-bit PHP (requires PHP_INT_SIZE to be 8). Note that older versions of PHP 5 on Windows use PHP_INT_SIZE of 4, even if they're 64 bit. If you're in this situation, update your PHP install. +Also, make sure that the web server has write permission on the `results` folder. + ## Known bugs and limitations ### General * The ping/jitter test is measured by seeing how long it takes for an empty XHR to complete. It is not an acutal ICMP ping. Different browsers may also show different results, especially on very fast connections on slow devices. -### IE-Specific +### IE specific * The upload test is not precise on very fast connections with high latency (will probably be fixed by Edge 17) * On IE11, a same origin policy error is erroneously triggered under unknown conditions. Seems to be related to running the test from unusual URLs like a top level domain (for instance http://abc/speedtest). These are bugs in IE11's implementation of the same origin policy, not in the speedtest itself. * On IE11, under unknown circumstances, on some systems the test can only be run once, after which speedtest_worker.js will not be loaded by IE until the browser is restarted. This is a rare bug in IE11. -### Firefox-Specific +### Firefox specific * On some Linux systems with hardware acceleration turned off, the page rendering makes the browser lag, reducing the accuracy of the ping/jitter test, and potentially even the download and upload tests on very fast connections. -## Making changes +## Contributing Since this is an open source project, you can modify it. -To make changes to the speedtest itself, edit `speedtest_worker.js` - -To create the minified version, use [UglifyJS](https://github.com/mishoo/UglifyJS2) like this: - -``` -uglifyjs -c -o speedtest_worker.min.js -- speedtest_worker.js -``` - -Pull requests are very appreciated. If you don't use github (or git), simply contact me at [info@fdossena.com](mailto:info@fdossena.com). - -__Important:__ please add your name to modified versions to distinguish them from the main project. +If you made some changes that you think should make it into the main project, send a Pull Request on GitHub, or contact me at [info@fdossena.com](mailto:info@fdossena.com). +We don't require you to use a specific coding convention, write the code however you want and we'll change the formatting if necessary. +Donations are also appreciated: you can donate with [PayPal](https://www.paypal.me/sineisochronic) or [Liberapay](https://liberapay.com/fdossena/donate). ## License This software is under the GNU LGPL license, Version 3 or newer. @@ -493,3 +804,4 @@ To put it short: you are free to use, study, modify, and redistribute this softw You can also use it in proprietary software but all changes to this software must remain under the same GNU LGPL license. Contact me at [info@fdossena.com](mailto:info@fdossena.com) for other licensing models. + diff --git a/empty.php b/empty.php deleted file mode 100644 index eb1fc85..0000000 --- a/empty.php +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/example-customSettings.html b/example-customSettings.html deleted file mode 100644 index 129f9f7..0000000 --- a/example-customSettings.html +++ /dev/null @@ -1,200 +0,0 @@ - - - - - -HTML5 Speedtest - - - - -

HTML5 Speedtest - Custom settings example

-
-
-
-
-
Download
-
-
Mbps
-
-
-
Upload
-
-
Mbps
-
-
-
-
-
Ping
-
-
ms
-
-
-
Jitter
-
-
ms
-
-
-
- IP Address: -
-
-
Custom parameters:
-Source code - - - \ No newline at end of file diff --git a/example-multipleServers-full.html b/example-multipleServers-full.html new file mode 100644 index 0000000..c72a932 --- /dev/null +++ b/example-multipleServers-full.html @@ -0,0 +1,393 @@ + + + + + + + + +HTML5 Speedtest + + +

HTML5 Speedtest

+
+

Selecting a server...

+
+ + + diff --git a/example-telemetryEnabled.html b/example-multipleServers-pretty.html similarity index 57% rename from example-telemetryEnabled.html rename to example-multipleServers-pretty.html index 53eb53b..43abeda 100644 --- a/example-telemetryEnabled.html +++ b/example-multipleServers-pretty.html @@ -4,6 +4,78 @@ HTML5 Speedtest + + + - -

HTML5 Speedtest - Telemetry example

+

HTML5 Speedtest

+
Selecting server...
@@ -187,8 +214,10 @@ function initUI(){ IP Address:
-
Basic telemetry is active; results will be saved in your database, without the full log. If the results don't appear, check the settings in telemetry_settings.php
Source code - + - \ No newline at end of file + diff --git a/example-basic.html b/example-singleServer-basic.html similarity index 61% rename from example-basic.html rename to example-singleServer-basic.html index b700c0f..1600214 100644 --- a/example-basic.html +++ b/example-singleServer-basic.html @@ -4,7 +4,7 @@ HTML5 Speedtest - +

HTML5 Speedtest

@@ -21,16 +21,14 @@

Source code diff --git a/example-chart.html b/example-singleServer-chart.html similarity index 90% rename from example-chart.html rename to example-singleServer-chart.html index d7b0ce6..b26beec 100644 --- a/example-chart.html +++ b/example-singleServer-chart.html @@ -35,9 +35,10 @@ margin: 0 auto; } - + + diff --git a/example-customSettings2.html b/example-singleServer-customSettings.html similarity index 71% rename from example-customSettings2.html rename to example-singleServer-customSettings.html index cdcc538..6bee3e0 100644 --- a/example-customSettings2.html +++ b/example-singleServer-customSettings.html @@ -4,6 +4,48 @@ HTML5 Speedtest + + + - -

HTML5 Speedtest - Custom settings example

+

HTML5 Speedtest

@@ -167,6 +166,8 @@ function initUI(){
Source code - + - \ No newline at end of file + diff --git a/example-telemetry-resultSharing.html b/example-singleServer-full.html similarity index 51% rename from example-telemetry-resultSharing.html rename to example-singleServer-full.html index eb89c7b..e3c4467 100644 --- a/example-telemetry-resultSharing.html +++ b/example-singleServer-full.html @@ -1,9 +1,130 @@ - -HTML5 Speedtest + + + - +HTML5 Speedtest -

HTML5 Speedtest - Telemetry and sharing example

-
-
-
-
-
Download
- -
-
Mbps
+

HTML5 Speedtest

+
+
+
+
+
+
Download
+ +
+
Mbps
+
+
+
Upload
+ +
+
Mbps
+
-
-
Upload
- -
-
Mbps
+
+
+
Ping
+ +
+
ms
+
+
+
Jitter
+ +
+
ms
+
+
+
+ IP Address: +
+
-
-
-
Ping
- -
-
ms
-
-
-
Jitter
- -
-
ms
-
-
-
- IP Address: -
- + Source code
-
Basic telemetry is active; results will be saved in your database, without the full log. If the results don't appear, or the sharing panel doesn't appear at the end of the test, check the settings in telemetry_settings.php
-Source code - + - \ No newline at end of file + diff --git a/example-gauges.html b/example-singleServer-gauges.html similarity index 59% rename from example-gauges.html rename to example-singleServer-gauges.html index 088a38f..91cd665 100644 --- a/example-gauges.html +++ b/example-singleServer-gauges.html @@ -1,9 +1,115 @@ - -HTML5 Speedtest + + + - +HTML5 Speedtest

HTML5 Speedtest

-
-
-
-
-
Download
- -
-
Mbps
+
+
+
+
+
+
Download
+ +
+
Mbps
+
+
+
Upload
+ +
+
Mbps
+
-
-
Upload
- -
-
Mbps
+
+
+
Ping
+ +
+
ms
+
+
+
Jitter
+ +
+
ms
+
+
+
+ IP Address:
-
-
-
Ping
- -
-
ms
-
-
-
Jitter
- -
-
ms
-
-
-
- IP Address: -
+ Source code
-Source code - + - \ No newline at end of file + diff --git a/example-pretty.html b/example-singleServer-pretty.html similarity index 77% rename from example-pretty.html rename to example-singleServer-pretty.html index f715f43..7b108aa 100644 --- a/example-pretty.html +++ b/example-singleServer-pretty.html @@ -4,6 +4,48 @@ HTML5 Speedtest + + + -

HTML5 Speedtest

@@ -188,6 +184,8 @@ function initUI(){
Source code - + - \ No newline at end of file + diff --git a/example-progressBar.html b/example-singleServer-progressBar.html similarity index 75% rename from example-progressBar.html rename to example-singleServer-progressBar.html index 6e0f53a..1d1f940 100644 --- a/example-progressBar.html +++ b/example-singleServer-progressBar.html @@ -4,6 +4,50 @@ HTML5 Speedtest + + + -

HTML5 Speedtest

-
+
Download
@@ -210,6 +204,8 @@ function initUI(){
Source code - + - \ No newline at end of file + diff --git a/telemetry/idObfuscation.php b/results/idObfuscation.php similarity index 100% rename from telemetry/idObfuscation.php rename to results/idObfuscation.php diff --git a/results/index.php b/results/index.php index 475616e..ffddaaa 100644 --- a/results/index.php +++ b/results/index.php @@ -51,8 +51,8 @@ $MS_TEXT="ms"; $WATERMARK_TEXT="HTML5 Speedtest"; $id=$_GET["id"]; -include_once('../telemetry/telemetry_settings.php'); -require '../telemetry/idObfuscation.php'; +include_once('telemetry_settings.php'); +require 'idObfuscation.php'; if($enable_id_obfuscation) $id=deobfuscateId($id); $conn=null; $q=null; $ispinfo=null; $dl=null; $ul=null; $ping=null; $jit=null; diff --git a/telemetry/stats.php b/results/stats.php similarity index 97% rename from telemetry/stats.php rename to results/stats.php index 01922ea..8e7d2de 100644 --- a/telemetry/stats.php +++ b/results/stats.php @@ -2,9 +2,9 @@ session_start(); error_reporting(0); header('Content-Type: text/html; charset=utf-8'); -header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); -header("Cache-Control: post-check=0, pre-check=0", false); -header("Pragma: no-cache"); +header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, s-maxage=0'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); ?> diff --git a/telemetry/telemetry.php b/results/telemetry.php similarity index 93% rename from telemetry/telemetry.php rename to results/telemetry.php index 58825fa..7ef0839 100644 --- a/telemetry/telemetry.php +++ b/results/telemetry.php @@ -13,6 +13,10 @@ $ping=($_POST["ping"]); $jitter=($_POST["jitter"]); $log=($_POST["log"]); +header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, s-maxage=0'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + if($db_type=="mysql"){ $conn = new mysqli($MySql_hostname, $MySql_username, $MySql_password, $MySql_databasename) or die("1"); $stmt = $conn->prepare("INSERT INTO speedtest_users (ip,ispinfo,extra,ua,lang,dl,ul,ping,jitter,log) VALUES (?,?,?,?,?,?,?,?,?,?)") or die("2"); diff --git a/telemetry/telemetry_mysql.sql b/results/telemetry_mysql.sql similarity index 100% rename from telemetry/telemetry_mysql.sql rename to results/telemetry_mysql.sql diff --git a/telemetry/telemetry_postgresql.sql b/results/telemetry_postgresql.sql similarity index 100% rename from telemetry/telemetry_postgresql.sql rename to results/telemetry_postgresql.sql diff --git a/telemetry/telemetry_settings.php b/results/telemetry_settings.php similarity index 92% rename from telemetry/telemetry_settings.php rename to results/telemetry_settings.php index 9d152a0..038bfb8 100644 --- a/telemetry/telemetry_settings.php +++ b/results/telemetry_settings.php @@ -5,7 +5,7 @@ $stats_password="PASSWORD"; //password to login to stats.php. Change this!!! $enable_id_obfuscation=false; //if set to true, test IDs will be obfuscated to prevent users from guessing URLs of other tests // Sqlite3 settings -$Sqlite_db_file = "../../telemetry.sql"; +$Sqlite_db_file = "../../speedtest_telemetry.sql"; // Mysql settings $MySql_username="USERNAME"; @@ -21,4 +21,4 @@ $PostgreSql_databasename="DB_NAME"; //IMPORTANT: DO NOT ADD ANYTHING BELOW THIS PHP CLOSING TAG, NOT EVEN EMPTY LINES! -?> \ No newline at end of file +?> diff --git a/speedtest.js b/speedtest.js new file mode 100644 index 0000000..2c4dcaf --- /dev/null +++ b/speedtest.js @@ -0,0 +1,342 @@ +/* + HTML5 Speedtest - Main + by Federico Dossena + https://github.com/adolfintel/speedtest/ + GNU LGPLv3 License +*/ + +/* + This is the main interface between your webpage and the speedtest. + It hides the speedtest web worker to the page, and provides many convenient functions to control the test. + + The best way to learn how to use this is to look at the basic example, but here's some documentation. + + To initialize the test, create a new Speedtest object: + var s=new Speedtest(); + Now you can think of this as a finite state machine. These are the states (use getState() to see them): + - 0: here you can change the speedtest settings (such as test duration) with the setParameter("parameter",value) method. From here you can either start the test using start() (goes to state 3) or you can add multiple test points using addTestPoint(server) or addTestPoints(serverList) (goes to state 1). Additionally, this is the perfect moment to set up callbacks for the onupdate(data) and onend(aborted) events. + - 1: here you can add test points. You only need to do this if you want to use multiple test points. + A server is defined as an object like this: + { + name: "User friendly name", + server:"http://yourBackend.com/", <---- URL to your server. You can specify http:// or https://. If your server supports both, just write // without the protocol + dlURL:"garbage.php" <----- path to garbage.php or its replacement on the server + ulURL:"empty.php" <----- path to empty.php or its replacement on the server + pingURL:"empty.php" <----- path to empty.php or its replacement on the server. This is used to ping the server by this selector + getIpURL:"getIP.php" <----- path to getIP.php or its replacement on the server + } + While in state 1, you can only add test points, you cannot change the test settings. When you're done, use selectServer(callback) to select the test point with the lowest ping. This is asynchronous, when it's done, it will call your callback function and move to state 2. Calling setSelectedServer(server) will manually select a server and move to state 2. + - 2: test point selected, ready to start the test. Use start() to begin, this will move to state 3 + - 3: test running. Here, your onupdate event calback will be called periodically, with data coming from the worker about speed and progress. A data object will be passed to your onupdate function, with the following items: + - dlStatus: download speed in mbps + - ulStatus: upload speed in mbps + - pingStatus: ping in ms + - jitterStatus: jitter in ms + - dlProgress: progress of the download test as a float 0-1 + - ulProgress: progress of the upload test as a float 0-1 + - pingProgress: progress of the ping/jitter test as a float 0-1 + - testState: state of the test (-1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=aborted) + - clientIp: IP address of the client performing the test (and optionally ISP and distance) + At the end of the test, the onend function will be called, with a boolean specifying whether the test was aborted or if it ended normally. + The test can be aborted at any time with abort(). + At the end of the test, it will move to state 4 + - 4: test finished. You can run it again by calling start() if you want. + */ + +function Speedtest() { + this._serverList = []; //when using multiple points of test, this is a list of test points + this._selectedServer = null; //when using multiple points of test, this is the selected server + this._settings = {}; //settings for the speedtest worker + this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done + console.log( + "HTML5 Speedtest by Federico Dossena v5.0 - https://github.com/adolfintel/speedtest" + ); +} + +Speedtest.prototype = { + constructor: Speedtest, + /** + * Returns the state of the test: 0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done + */ + getState: function() { + return this._state; + }, + /** + * Change one of the test settings from their defaults. + * - parameter: string with the name of the parameter that you want to set + * - value: new value for the parameter + * + * Invalid values or nonexistant parameters will be ignored by the speedtest worker. + */ + setParameter: function(parameter, value) { + if (this._state != 0) + throw "You cannot change the test settings after adding server or starting the test"; + this._settings[parameter] = value; + }, + /** + * Used internally to check if a server object contains all the required elements. + * Also fixes the server URL if needed. + */ + _checkServerDefinition: function(server) { + try { + if (typeof server.name !== "string") + throw "Name string missing from server definition (name)"; + if (typeof server.server !== "string") + throw "Server address string missing from server definition (server)"; + if (server.server.charAt(server.server.length - 1) != "/") + server.server += "/"; + if (server.server.indexOf("//") == 0) + server.server = location.protocol + server.server; + if (typeof server.dlURL !== "string") + throw "Download URL string missing from server definition (dlURL)"; + if (typeof server.ulURL !== "string") + throw "Upload URL string missing from server definition (ulURL)"; + if (typeof server.pingURL !== "string") + throw "Ping URL string missing from server definition (pingURL)"; + if (typeof server.getIpURL !== "string") + throw "GetIP URL string missing from server definition (getIpURL)"; + } catch (e) { + throw "Invalid server definition"; + } + }, + /** + * Add a test point (multiple points of test) + * server: the server to be added as an object. Must contain the following elements: + * { + * name: "User friendly name", + * server:"http://yourBackend.com/", URL to your server. You can specify http:// or https://. If your server supports both, just write // without the protocol + * dlURL:"garbage.php" path to garbage.php or its replacement on the server + * ulURL:"empty.php" path to empty.php or its replacement on the server + * pingURL:"empty.php" path to empty.php or its replacement on the server. This is used to ping the server by this selector + * getIpURL:"getIP.php" path to getIP.php or its replacement on the server + * } + */ + addTestPoint: function(server) { + this._checkServerDefinition(server); + if (this._state == 0) this._state = 1; + if (this._state != 1) throw "You can't add a server after server selection"; + this._settings.mpot = true; + this._serverList.push(server); + }, + /** + * Same as addTestPoint, but you can pass an array of servers + */ + addTestPoints: function(list) { + for (var i = 0; i < list.length; i++) this.addTestPoint(list[i]); + }, + /** + * Returns the selected server (multiple points of test) + */ + getSelectedServer: function() { + if (this._state < 2 || this._selectedServer == null) + throw "No server is selected"; + return this._selectedServer; + }, + /** + * Manually selects one of the test points (multiple points of test) + */ + setSelectedServer: function(server) { + this._checkServerDefinition(server); + if (this._state == 3) + throw "You can't select a server while the test is running"; + this._selectedServer = server; + this._state = 2; + }, + /** + * Automatically selects a server from the list of added test points. The server with the lowest ping will be chosen. (multiple points of test) + * The process is asynchronous and the passed result callback function will be called when it's done, then the test can be started. + */ + selectServer: function(result) { + if (this._state != 1) { + if (this._state == 0) throw "No test points added"; + if (this._state == 2) throw "Server already selected"; + if (this._state >= 3) + throw "You can't select a server while the test is running"; + } + if (this._selectServerCalled) throw "selectServer already called"; else this._selectServerCalled=true; + /*this function goes through a list of servers. For each server, the ping is measured, then the server with the function result is called with the best server, or null if all the servers were down. + */ + var select = function(serverList, result) { + //pings the specified URL, then calls the function result. Result will receive a parameter which is either the time it took to ping the URL, or -1 if something went wrong. + var PING_TIMEOUT = 2000; + var USE_PING_TIMEOUT = true; //will be disabled on unsupported browsers + if (/MSIE.(\d+\.\d+)/i.test(navigator.userAgent)) { + //IE11 doesn't support XHR timeout + USE_PING_TIMEOUT = false; + } + var ping = function(url, result) { + url += (url.match(/\?/) ? "&" : "?") + "cors=true"; + var xhr = new XMLHttpRequest(); + var t = new Date().getTime(); + xhr.onload = function() { + if (xhr.responseText.length == 0) { + //we expect an empty response + var instspd = new Date().getTime() - t; //rough timing estimate + try { + //try to get more accurate timing using performance API + var p = performance.getEntriesByName(url); + p = p[p.length - 1]; + var d = p.responseStart - p.requestStart; + if (d <= 0) d = p.duration; + if (d > 0 && d < instspd) instspd = d; + } catch (e) {} + result(instspd); + } else result(-1); + }.bind(this); + xhr.onerror = function() { + result(-1); + }.bind(this); + xhr.open("GET", url); + if (USE_PING_TIMEOUT) { + try { + xhr.timeout = PING_TIMEOUT; + xhr.ontimeout = xhr.onerror; + } catch (e) {} + } + xhr.send(); + }.bind(this); + + //this function repeatedly pings a server to get a good estimate of the ping. When it's done, it calls the done function without parameters. At the end of the execution, the server will have a new parameter called pingT, which is either the best ping we got from the server or -1 if something went wrong. + var PINGS = 3, //up to 3 pings are performed, unless the server is down... + SLOW_THRESHOLD = 500; //...or one of the pings is above this threshold + var checkServer = function(server, done) { + var i = 0; + server.pingT = -1; + if (server.server.indexOf(location.protocol) == -1) done(); + else { + var nextPing = function() { + if (i++ == PINGS) { + done(); + return; + } + ping( + server.server + server.pingURL, + function(t) { + if (t >= 0) { + if (t < server.pingT || server.pingT == -1) server.pingT = t; + if (t < SLOW_THRESHOLD) nextPing(); + else done(); + } else done(); + }.bind(this) + ); + }.bind(this); + nextPing(); + } + }.bind(this); + //check servers in list, one by one + var i = 0; + var done = function() { + var bestServer = null; + for (var i = 0; i < serverList.length; i++) { + if ( + serverList[i].pingT != -1 && + (bestServer == null || serverList[i].pingT < bestServer.pingT) + ) + bestServer = serverList[i]; + } + result(bestServer); + }.bind(this); + var nextServer = function() { + if (i == serverList.length) { + done(); + return; + } + checkServer(serverList[i++], nextServer); + }.bind(this); + nextServer(); + }.bind(this); + + //parallel server selection + var CONCURRENCY = 6; + var serverLists = []; + for (var i = 0; i < CONCURRENCY; i++) { + serverLists[i] = []; + } + for (var i = 0; i < this._serverList.length; i++) { + serverLists[i % CONCURRENCY].push(this._serverList[i]); + } + var completed = 0; + var bestServer = null; + for (var i = 0; i < CONCURRENCY; i++) { + select( + serverLists[i], + function(server) { + if (server != null) { + if (bestServer == null || server.pingT < bestServer.pingT) + bestServer = server; + } + completed++; + if (completed == CONCURRENCY) { + this._selectedServer = bestServer; + this._state = 2; + if (result) result(bestServer); + } + }.bind(this) + ); + } + }, + /** + * Starts the test. + * During the test, the onupdate(data) callback function will be called periodically with data from the worker. + * At the end of the test, the onend(aborted) function will be called with a boolean telling you if the test was aborted or if it ended normally. + */ + start: function() { + if (this._state == 3) throw "Test already running"; + this.worker = new Worker("speedtest_worker.js?r=" + Math.random()); + this.worker.onmessage = function(e) { + if (e.data === this._prevData) return; + else this._prevData = e.data; + var data = JSON.parse(e.data); + try { + if (this.onupdate) this.onupdate(data); + } catch (e) { + console.error("Speedtest onupdate event threw exception: " + e); + } + if (data.testState >= 4) { + try { + if (this.onend) this.onend(data.testState == 5); + } catch (e) { + console.error("Speedtest onend event threw exception: " + e); + } + clearInterval(this.updater); + this._state = 4; + } + }.bind(this); + this.updater = setInterval( + function() { + this.worker.postMessage("status"); + }.bind(this), + 200 + ); + if (this._state == 1) + throw "When using multiple points of test, you must call selectServer before starting the test"; + if (this._state == 2) { + this._settings.url_dl = + this._selectedServer.server + this._selectedServer.dlURL; + this._settings.url_ul = + this._selectedServer.server + this._selectedServer.ulURL; + this._settings.url_ping = + this._selectedServer.server + this._selectedServer.pingURL; + this._settings.url_getIp = + this._selectedServer.server + this._selectedServer.getIpURL; + if (typeof this._settings.telemetry_extra !== "undefined") { + this._settings.telemetry_extra = JSON.stringify({ + server: this._selectedServer.name, + extra: this._settings.telemetry_extra + }); + } else + this._settings.telemetry_extra = JSON.stringify({ + server: this._selectedServer.name + }); + } + this._state = 3; + this.worker.postMessage("start " + JSON.stringify(this._settings)); + }, + /** + * Aborts the test while it's running. + */ + abort: function() { + if (this._state < 3) throw "You cannot abort a test that's not started yet"; + if (this._state < 4) this.worker.postMessage("abort"); + } +}; diff --git a/speedtest_worker.js b/speedtest_worker.js index 5dd080b..ad49571 100644 --- a/speedtest_worker.js +++ b/speedtest_worker.js @@ -1,12 +1,12 @@ /* - HTML5 Speedtest v4.7.2 + HTML5 Speedtest - Worker by Federico Dossena https://github.com/adolfintel/speedtest/ GNU LGPLv3 License */ // data reported to main thread -var testStatus = -1; // -1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=abort/error +var testState = -1; // -1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=abort var dlStatus = ""; // download speed in megabit/s with 2 decimal digits var ulStatus = ""; // upload speed in megabit/s with 2 decimal digits var pingStatus = ""; // ping in milliseconds with 2 decimal digits @@ -37,6 +37,7 @@ function twarn(s) { // test settings. can be overridden by sending specific values with the start command var settings = { + mpot: false, //set to true when in MPOT mode test_order: "IP_D_U", //order in which tests will be performed as a string. D=Download, U=Upload, P=Ping+Jitter, I=IP, _=1 second delay time_ul_max: 15, // max duration of upload test in seconds time_dl_max: 15, // max duration of download test in seconds @@ -44,10 +45,10 @@ var settings = { time_ulGraceTime: 3, //time to wait in seconds before actually measuring ul speed (wait for buffers to fill) time_dlGraceTime: 1.5, //time to wait in seconds before actually measuring dl speed (wait for TCP window to increase) count_ping: 10, // number of pings to perform in ping test - url_dl: "garbage.php", // path to a large file or garbage.php, used for download test. must be relative to this js file - url_ul: "empty.php", // path to an empty file, used for upload test. must be relative to this js file - url_ping: "empty.php", // path to an empty file, used for ping test. must be relative to this js file - url_getIp: "getIP.php", // path to getIP.php relative to this js file, or a similar thing that outputs the client's ip + url_dl: "backend/garbage.php", // path to a large file or garbage.php, used for download test. must be relative to this js file + url_ul: "backend/empty.php", // path to an empty file, used for upload test. must be relative to this js file + url_ping: "backend/empty.php", // path to an empty file, used for ping test. must be relative to this js file + url_getIp: "backend/getIP.php", // path to getIP.php relative to this js file, or a similar thing that outputs the client's ip getIp_ispInfo: true, //if set to true, the server will include ISP info with the IP address getIp_ispInfo_distance: "km", //km or mi=estimate distance from server in km/mi; set to false to disable distance estimation. getIp_ispInfo must be enabled in order for this to work xhr_dlMultistream: 6, // number of download streams to use (can be different if enable_quirks is active) @@ -62,7 +63,7 @@ var settings = { overheadCompensationFactor: 1.06, //can be changed to compensatie for transport overhead. (see doc.md for some other values) useMebibits: false, //if set to true, speed will be reported in mebibits/s instead of megabits/s telemetry_level: 0, // 0=disabled, 1=basic (results only), 2=full (results and timing) 3=debug (results+log) - url_telemetry: "telemetry/telemetry.php", // path to the script that adds telemetry data to the database + url_telemetry: "results/telemetry.php", // path to the script that adds telemetry data to the database telemetry_extra: "" //extra data that can be passed to the telemetry through the settings }; @@ -80,7 +81,7 @@ function url_sep(url) { /* listener for commands from main thread to this worker. commands: - -status: returns the current status as a JSON string containing testStatus, dlStatus, ulStatus, pingStatus, clientIp, jitterStatus, dlProgress, ulProgress, pingProgress + -status: returns the current status as a JSON string containing testState, dlStatus, ulStatus, pingStatus, clientIp, jitterStatus, dlProgress, ulProgress, pingProgress -abort: aborts the current test -start: starts the test. optionally, settings can be passed as JSON. example: start {"time_ul_max":"10", "time_dl_max":"10", "count_ping":"50"} @@ -91,7 +92,7 @@ this.addEventListener("message", function(e) { // return status postMessage( JSON.stringify({ - testState: testStatus, + testState: testState, dlStatus: dlStatus, ulStatus: ulStatus, pingStatus: pingStatus, @@ -104,9 +105,9 @@ this.addEventListener("message", function(e) { }) ); } - if (params[0] === "start" && testStatus === -1) { + if (params[0] === "start" && testState === -1) { // start new test - testStatus = 0; + testState = 0; try { // parse settings, if present var s = {}; @@ -160,12 +161,16 @@ this.addEventListener("message", function(e) { //Chrome mobile introduced a limitation somewhere around version 65, we have to limit XHR upload size to 4 megabytes settings.xhr_ul_blob_megabytes = 4; } + if (/^((?!chrome|android|crios|fxios).)*safari/i.test(ua)) { + //Safari also needs the IE11 workaround but only for the MPOT version + settings.forceIE11Workaround = true; + } //telemetry_level has to be parsed and not just copied if (typeof s.telemetry_level !== "undefined") settings.telemetry_level = s.telemetry_level === "basic" ? 1 : s.telemetry_level === "full" ? 2 : s.telemetry_level === "debug" ? 3 : 0; // telemetry level //transform test_order to uppercase, just in case settings.test_order = settings.test_order.toUpperCase(); } catch (e) { - twarn("Possible error in custom test settings. Some settings may not be applied. Exception: " + e); + twarn("Possible error in custom test settings. Some settings might not have been applied. Exception: " + e); } // run the tests tverb(JSON.stringify(settings)); @@ -175,15 +180,15 @@ this.addEventListener("message", function(e) { uRun = false, pRun = false; var runNextTest = function() { - if (testStatus == 5) return; + if (testState == 5) return; if (test_pointer >= settings.test_order.length) { //test is finished if (settings.telemetry_level > 0) sendTelemetry(function(id) { - testStatus = 4; + testState = 4; if (id != null) testId = id; }); - else testStatus = 4; + else testState = 4; return; } switch (settings.test_order.charAt(test_pointer)) { @@ -204,7 +209,7 @@ this.addEventListener("message", function(e) { runNextTest(); return; } else dRun = true; - testStatus = 1; + testState = 1; dlTest(runNextTest); } break; @@ -215,7 +220,7 @@ this.addEventListener("message", function(e) { runNextTest(); return; } else uRun = true; - testStatus = 3; + testState = 3; ulTest(runNextTest); } break; @@ -226,7 +231,7 @@ this.addEventListener("message", function(e) { runNextTest(); return; } else pRun = true; - testStatus = 2; + testState = 2; pingTest(runNextTest); } break; @@ -249,11 +254,15 @@ this.addEventListener("message", function(e) { runNextTest = null; if (interval) clearInterval(interval); // clear timer if present if (settings.telemetry_level > 1) sendTelemetry(function() {}); - testStatus = 5; + testState = 5; //set test as aborted dlStatus = ""; ulStatus = ""; pingStatus = ""; - jitterStatus = ""; // set test as aborted + jitterStatus = ""; + clientIp = ""; + dlProgress = 0; + ulProgress = 0; + pingProgress = 0; } }); // stops all XHR activity, aggressively @@ -306,7 +315,7 @@ function getIp(done) { tlog("getIp failed, took " + (new Date().getTime() - startT) + "ms"); done(); }; - xhr.open("GET", settings.url_getIp + url_sep(settings.url_getIp) + (settings.getIp_ispInfo ? "isp=true" + (settings.getIp_ispInfo_distance ? "&distance=" + settings.getIp_ispInfo_distance + "&" : "&") : "&") + "r=" + Math.random(), true); + xhr.open("GET", settings.url_getIp + url_sep(settings.url_getIp) + (settings.mpot ? "cors=true&" : "") + (settings.getIp_ispInfo ? "isp=true" + (settings.getIp_ispInfo_distance ? "&distance=" + settings.getIp_ispInfo_distance + "&" : "&") : "&") + "r=" + Math.random(), true); xhr.send(); } // download test, calls done function when it's over @@ -325,14 +334,14 @@ function dlTest(done) { var testStream = function(i, delay) { setTimeout( function() { - if (testStatus !== 1) return; // delayed stream ended up starting after the end of the download test + if (testState !== 1) return; // delayed stream ended up starting after the end of the download test tverb("dl test stream started " + i + " " + delay); var prevLoaded = 0; // number of bytes loaded last time onprogress was called var x = new XMLHttpRequest(); xhr[i] = x; xhr[i].onprogress = function(event) { tverb("dl stream progress event " + i + " " + event.loaded); - if (testStatus !== 1) { + if (testState !== 1) { try { x.abort(); } catch (e) {} @@ -366,7 +375,7 @@ function dlTest(done) { if (settings.xhr_dlUseBlob) xhr[i].responseType = "blob"; else xhr[i].responseType = "arraybuffer"; } catch (e) {} - xhr[i].open("GET", settings.url_dl + url_sep(settings.url_dl) + "r=" + Math.random() + "&ckSize=" + settings.garbagePhp_chunkSize, true); // random string to prevent caching + xhr[i].open("GET", settings.url_dl + url_sep(settings.url_dl) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random() + "&ckSize=" + settings.garbagePhp_chunkSize, true); // random string to prevent caching xhr[i].send(); }.bind(this), 1 + delay @@ -416,7 +425,6 @@ function dlTest(done) { 200 ); } - // upload test, calls done function whent it's over var ulCalled = false; // used to prevent multiple accidental calls to ulTest function ulTest(done) { @@ -441,132 +449,141 @@ function ulTest(done) { } catch (e) {} reqsmall.push(r); reqsmall = new Blob(reqsmall); - var totLoaded = 0.0, // total number of transmitted bytes - startT = new Date().getTime(), // timestamp when test was started - bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections) - graceTimeDone = false, //set to true after the grace time is past - failed = false; // set to true if a stream fails - xhr = []; - // function to create an upload stream. streams are slightly delayed so that they will not end at the same time - var testStream = function(i, delay) { - setTimeout( - function() { - if (testStatus !== 3) return; // delayed stream ended up starting after the end of the upload test - tverb("ul test stream started " + i + " " + delay); - var prevLoaded = 0; // number of bytes transmitted last time onprogress was called - var x = new XMLHttpRequest(); - xhr[i] = x; - var ie11workaround; - if (settings.forceIE11Workaround) ie11workaround = true; - else { - try { - xhr[i].upload.onprogress; - ie11workaround = false; - } catch (e) { - ie11workaround = true; - } - } - if (ie11workaround) { - // IE11 workarond: xhr.upload does not work properly, therefore we send a bunch of small 256k requests and use the onload event as progress. This is not precise, especially on fast connections - xhr[i].onload = xhr[i].onerror = function() { - tverb("ul stream progress event (ie11wa)"); - totLoaded += reqsmall.size; - testStream(i, 0); - }; - xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + "r=" + Math.random(), true); // random string to prevent caching - try{ - xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway) - }catch(e){} - try{ - xhr[i].setRequestHeader("Content-Type", "application/octet-stream"); //force content-type to application/octet-stream in case the server misinterprets it - }catch(e){} - xhr[i].send(reqsmall); - } else { - // REGULAR version, no workaround - xhr[i].upload.onprogress = function(event) { - tverb("ul stream progress event " + i + " " + event.loaded); - if (testStatus !== 3) { - try { - x.abort(); - } catch (e) {} - } // just in case this XHR is still running after the upload test - // progress event, add number of new loaded bytes to totLoaded - var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded; - if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case - totLoaded += loadDiff; - prevLoaded = event.loaded; - }.bind(this); - xhr[i].upload.onload = function() { - // this stream sent all the garbage data, start again - tverb("ul stream finished " + i); - testStream(i, 0); - }.bind(this); - xhr[i].upload.onerror = function() { - tverb("ul stream failed " + i); - if (settings.xhr_ignoreErrors === 0) failed = true; //abort + var testFunction = function() { + var totLoaded = 0.0, // total number of transmitted bytes + startT = new Date().getTime(), // timestamp when test was started + bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections) + graceTimeDone = false, //set to true after the grace time is past + failed = false; // set to true if a stream fails + xhr = []; + // function to create an upload stream. streams are slightly delayed so that they will not end at the same time + var testStream = function(i, delay) { + setTimeout( + function() { + if (testState !== 3) return; // delayed stream ended up starting after the end of the upload test + tverb("ul test stream started " + i + " " + delay); + var prevLoaded = 0; // number of bytes transmitted last time onprogress was called + var x = new XMLHttpRequest(); + xhr[i] = x; + var ie11workaround; + if (settings.forceIE11Workaround) ie11workaround = true; + else { try { - xhr[i].abort(); + xhr[i].upload.onprogress; + ie11workaround = false; + } catch (e) { + ie11workaround = true; + } + } + if (ie11workaround) { + // IE11 workarond: xhr.upload does not work properly, therefore we send a bunch of small 256k requests and use the onload event as progress. This is not precise, especially on fast connections + xhr[i].onload = xhr[i].onerror = function() { + tverb("ul stream progress event (ie11wa)"); + totLoaded += reqsmall.size; + testStream(i, 0); + }; + xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching + try { + xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway) } catch (e) {} - delete xhr[i]; - if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream - }.bind(this); - // send xhr - xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + "r=" + Math.random(), true); // random string to prevent caching - try{ - xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway) - }catch(e){} - try{ - xhr[i].setRequestHeader("Content-Type", "application/octet-stream"); //force content-type to application/octet-stream in case the server misinterprets it - }catch(e){} - xhr[i].send(req); + //No Content-Type header in MPOT branch because it triggers bugs in some browsers + xhr[i].send(reqsmall); + } else { + // REGULAR version, no workaround + xhr[i].upload.onprogress = function(event) { + tverb("ul stream progress event " + i + " " + event.loaded); + if (testState !== 3) { + try { + x.abort(); + } catch (e) {} + } // just in case this XHR is still running after the upload test + // progress event, add number of new loaded bytes to totLoaded + var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded; + if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case + totLoaded += loadDiff; + prevLoaded = event.loaded; + }.bind(this); + xhr[i].upload.onload = function() { + // this stream sent all the garbage data, start again + tverb("ul stream finished " + i); + testStream(i, 0); + }.bind(this); + xhr[i].upload.onerror = function() { + tverb("ul stream failed " + i); + if (settings.xhr_ignoreErrors === 0) failed = true; //abort + try { + xhr[i].abort(); + } catch (e) {} + delete xhr[i]; + if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream + }.bind(this); + // send xhr + xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching + try { + xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway) + } catch (e) {} + //No Content-Type header in MPOT branch because it triggers bugs in some browsers + xhr[i].send(req); + } + }.bind(this), + 1 + ); + }.bind(this); + // open streams + for (var i = 0; i < settings.xhr_ulMultistream; i++) { + testStream(i, settings.xhr_multistreamDelay * i); + } + // every 200ms, update ulStatus + interval = setInterval( + function() { + tverb("UL: " + ulStatus + (graceTimeDone ? "" : " (in grace time)")); + var t = new Date().getTime() - startT; + if (graceTimeDone) ulProgress = (t + bonusT) / (settings.time_ul_max * 1000); + if (t < 200) return; + if (!graceTimeDone) { + if (t > 1000 * settings.time_ulGraceTime) { + if (totLoaded > 0) { + // if the connection is so slow that we didn't get a single chunk yet, do not reset + startT = new Date().getTime(); + bonusT = 0; + totLoaded = 0.0; + } + graceTimeDone = true; + } + } else { + var speed = totLoaded / (t / 1000.0); + if (settings.time_auto) { + //decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here + var bonus = (6.4 * speed) / 100000; + bonusT += bonus > 800 ? 800 : bonus; + } + //update status + ulStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits + if ((t + bonusT) / 1000.0 > settings.time_ul_max || failed) { + // test is over, stop streams and timer + if (failed || isNaN(ulStatus)) ulStatus = "Fail"; + clearRequests(); + clearInterval(interval); + ulProgress = 1; + tlog("ulTest: " + ulStatus + ", took " + (new Date().getTime() - startT) + "ms"); + done(); + } } }.bind(this), - 1 + 200 ); }.bind(this); - // open streams - for (var i = 0; i < settings.xhr_ulMultistream; i++) { - testStream(i, settings.xhr_multistreamDelay * i); - } - // every 200ms, update ulStatus - interval = setInterval( - function() { - tverb("UL: " + ulStatus + (graceTimeDone ? "" : " (in grace time)")); - var t = new Date().getTime() - startT; - if (graceTimeDone) ulProgress = (t + bonusT) / (settings.time_ul_max * 1000); - if (t < 200) return; - if (!graceTimeDone) { - if (t > 1000 * settings.time_ulGraceTime) { - if (totLoaded > 0) { - // if the connection is so slow that we didn't get a single chunk yet, do not reset - startT = new Date().getTime(); - bonusT = 0; - totLoaded = 0.0; - } - graceTimeDone = true; - } - } else { - var speed = totLoaded / (t / 1000.0); - if (settings.time_auto) { - //decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here - var bonus = (6.4 * speed) / 100000; - bonusT += bonus > 800 ? 800 : bonus; - } - //update status - ulStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits - if ((t + bonusT) / 1000.0 > settings.time_ul_max || failed) { - // test is over, stop streams and timer - if (failed || isNaN(ulStatus)) ulStatus = "Fail"; - clearRequests(); - clearInterval(interval); - ulProgress = 1; - tlog("ulTest: " + ulStatus + ", took " + (new Date().getTime() - startT) + "ms"); - done(); - } - } - }.bind(this), - 200 - ); + if (settings.mpot) { + tverb("Sending POST request before performing upload test"); + xhr = []; + xhr[0] = new XMLHttpRequest(); + xhr[0].onload = xhr[0].onerror = function() { + tverb("POST request sent, starting upload test"); + testFunction(); + }.bind(this); + xhr[0].open("POST", settings.url_ul); + xhr[0].send(); + } else testFunction(); } // ping+jitter test, function done is called when it's over var ptCalled = false; // used to prevent multiple accidental calls to pingTest @@ -608,8 +625,8 @@ function pingTest(done) { } } //noticed that some browsers randomly have 0ms ping - if(instspd<1) instspd=prevInstspd; - if(instspd<1) instspd=1; + if (instspd < 1) instspd = prevInstspd; + if (instspd < 1) instspd = 1; var instjitter = Math.abs(instspd - prevInstspd); if (i === 1) ping = instspd; /* first ping, can't tell jitter yet*/ else { @@ -658,7 +675,7 @@ function pingTest(done) { } }.bind(this); // send xhr - xhr[0].open("GET", settings.url_ping + url_sep(settings.url_ping) + "r=" + Math.random(), true); // random string to prevent caching + xhr[0].open("GET", settings.url_ping + url_sep(settings.url_ping) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching xhr[0].send(); }.bind(this); doPing(); // start first ping @@ -686,7 +703,7 @@ function sendTelemetry(done) { console.log("TELEMETRY ERROR " + xhr.status); done(null); }; - xhr.open("POST", settings.url_telemetry + url_sep(settings.url_telemetry) + "r=" + Math.random(), true); + xhr.open("POST", settings.url_telemetry + url_sep(settings.url_telemetry) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); var telemetryIspInfo = { processedString: clientIp, rawIspInfo: typeof ispInfo === "object" ? ispInfo : "" diff --git a/speedtest_worker.min.js b/speedtest_worker.min.js deleted file mode 100644 index 2143af2..0000000 --- a/speedtest_worker.min.js +++ /dev/null @@ -1 +0,0 @@ -var testStatus=-1,dlStatus="",ulStatus="",pingStatus="",jitterStatus="",clientIp="",dlProgress=0,ulProgress=0,pingProgress=0,testId=null,log="";function tlog(s){2<=settings.telemetry_level&&(log+=Date.now()+": "+s+"\n")}function tverb(s){3<=settings.telemetry_level&&(log+=Date.now()+": "+s+"\n")}function twarn(s){2<=settings.telemetry_level&&(log+=Date.now()+" WARN: "+s+"\n"),console.warn(s)}var settings={test_order:"IP_D_U",time_ul_max:15,time_dl_max:15,time_auto:!0,time_ulGraceTime:3,time_dlGraceTime:1.5,count_ping:10,url_dl:"garbage.php",url_ul:"empty.php",url_ping:"empty.php",url_getIp:"getIP.php",getIp_ispInfo:!0,getIp_ispInfo_distance:"km",xhr_dlMultistream:6,xhr_ulMultistream:3,xhr_multistreamDelay:300,xhr_ignoreErrors:1,xhr_dlUseBlob:!1,xhr_ul_blob_megabytes:20,garbagePhp_chunkSize:100,enable_quirks:!0,ping_allowPerformanceApi:!0,overheadCompensationFactor:1.06,useMebibits:!1,telemetry_level:0,url_telemetry:"telemetry/telemetry.php",telemetry_extra:""},xhr=null,interval=null,test_pointer=0;function url_sep(url){return url.match(/\?/)?"&":"?"}function clearRequests(){if(tverb("stopping pending XHRs"),xhr){for(var i=0;i=settings.test_order.length)0settings.time_dl_max||failed)&&((failed||isNaN(dlStatus))&&(dlStatus="Fail"),clearRequests(),clearInterval(interval),dlProgress=1,tlog("dlTest: "+dlStatus+", took "+((new Date).getTime()-startT)+"ms"),done())}else t>1e3*settings.time_dlGraceTime&&(0settings.time_ul_max||failed)&&((failed||isNaN(ulStatus))&&(ulStatus="Fail"),clearRequests(),clearInterval(interval),ulProgress=1,tlog("ulTest: "+ulStatus+", took "+((new Date).getTime()-startT)+"ms"),done())}else t>1e3*settings.time_ulGraceTime&&(0