diff --git a/speedtest.js b/speedtest.js index f536c37ff..5cff6b7f0 100755 --- a/speedtest.js +++ b/speedtest.js @@ -239,7 +239,11 @@ Speedtest.prototype = { const checkServer = function(server, done) { let i = 0; server.pingT = -1; - if (server.server.indexOf(location.protocol) == -1) done(); + if ( + location.protocol === "https:" && + server.server.substring(0, 7).toLowerCase() === "http://" + ) + done(); else { const nextPing = function() { if (i++ == PINGS) { diff --git a/tests/e2e/server-selection.spec.js b/tests/e2e/server-selection.spec.js new file mode 100644 index 000000000..3926288d7 --- /dev/null +++ b/tests/e2e/server-selection.spec.js @@ -0,0 +1,113 @@ +const { test, expect } = require('@playwright/test'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +const speedtestSource = fs.readFileSync( + path.resolve(__dirname, '../../speedtest.js'), + 'utf8' +); + +function createHarness(protocol, responders = {}) { + const requests = []; + + function FakeXMLHttpRequest() { + this.responseText = ''; + this.timeout = 0; + } + + FakeXMLHttpRequest.prototype.open = function open(method, url) { + this._method = method; + this._url = url; + }; + + FakeXMLHttpRequest.prototype.send = function send() { + requests.push(this._url); + const baseUrl = this._url.replace(/[?&]cors=true$/, ''); + const outcome = responders[baseUrl] || 'error'; + + if (outcome === 'success') { + if (typeof this.onload === 'function') { + this.onload(); + } + return; + } + + if (typeof this.onerror === 'function') { + this.onerror(new Error(`Request failed for ${baseUrl}`)); + } + }; + + const context = { + console: { log() {} }, + Date, + location: { protocol }, + navigator: { userAgent: 'Playwright' }, + performance: { getEntriesByName: () => [] }, + XMLHttpRequest: FakeXMLHttpRequest + }; + + vm.createContext(context); + vm.runInContext(speedtestSource, context, { filename: 'speedtest.js' }); + + return { Speedtest: context.Speedtest, requests }; +} + +function createServer(server) { + return { + name: server, + server, + dlURL: 'garbage.php', + ulURL: 'empty.php', + pingURL: 'empty.php', + getIpURL: 'getIP.php' + }; +} + +function selectServer(speedtest) { + return new Promise((resolve) => { + speedtest.selectServer(resolve); + }); +} + +test.describe('Speedtest server selection protocol handling', () => { + test('allows HTTPS backends to be pinged from an HTTP frontend', async () => { + const secureServer = 'https://secure.example/'; + const { Speedtest, requests } = createHarness('http:', { + [`${secureServer}empty.php`]: 'success' + }); + + const speedtest = new Speedtest(); + speedtest.addTestPoint(createServer(secureServer)); + + const selectedServer = await selectServer(speedtest); + + expect(selectedServer.server).toBe(secureServer); + expect( + requests.some((url) => url.startsWith(`${secureServer}empty.php`)) + ).toBeTruthy(); + }); + + test('still skips insecure HTTP backends from an HTTPS frontend', async () => { + const insecureServer = 'http://insecure.example/'; + const secureServer = 'https://secure.example/'; + const { Speedtest, requests } = createHarness('https:', { + [`${insecureServer}empty.php`]: 'success', + [`${secureServer}empty.php`]: 'success' + }); + + const speedtest = new Speedtest(); + speedtest.addTestPoint(createServer(insecureServer)); + speedtest.addTestPoint(createServer(secureServer)); + + const selectedServer = await selectServer(speedtest); + + expect(selectedServer.server).toBe(secureServer); + expect( + requests.some((url) => url.startsWith(`${insecureServer}empty.php`)) + ).toBeFalsy(); + expect( + requests.some((url) => url.startsWith(`${secureServer}empty.php`)) + ).toBeTruthy(); + }); +});