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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions client-src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,13 @@ const formatURL = (objURL: {
const createSocketURL = (
parsedURL: URL & { fromCurrentScript?: boolean },
): string => {
// Blob workers have an empty hostname; their origin is the page that created them.
const pageLocation =
self.location.protocol === 'blob:' &&
self.location.origin &&
self.location.origin !== 'null'
? new URL(self.location.origin)
: self.location;
let { hostname } = parsedURL;

// Node.js module parses it as `::`
Expand All @@ -597,20 +604,20 @@ const createSocketURL = (
// see: https://github.com/webpack/webpack-dev-server/pull/384
if (
isInAddrAny &&
self.location.hostname &&
self.location.protocol.indexOf('http') === 0
pageLocation.hostname &&
pageLocation.protocol.indexOf('http') === 0
) {
hostname = self.location.hostname;
hostname = pageLocation.hostname;
}

let socketURLProtocol = parsedURL.protocol || self.location.protocol;
let socketURLProtocol = parsedURL.protocol || pageLocation.protocol;

// When https is used in the app, secure web sockets are always necessary because the browser doesn't accept non-secure web sockets.
if (
socketURLProtocol === 'auto:' ||
(hostname && isInAddrAny && self.location.protocol === 'https:')
(hostname && isInAddrAny && pageLocation.protocol === 'https:')
) {
socketURLProtocol = self.location.protocol;
socketURLProtocol = pageLocation.protocol;
}

socketURLProtocol = socketURLProtocol.replace(
Expand Down Expand Up @@ -643,14 +650,14 @@ const createSocketURL = (
// so we need to fall back to the default if they are not provided
const socketURLHostname = (
hostname ||
self.location.hostname ||
pageLocation.hostname ||
'localhost'
).replace(/^\[(.*)\]$/, '$1');

let socketURLPort = parsedURL.port;

if (!socketURLPort || socketURLPort === '0') {
socketURLPort = self.location.port;
socketURLPort = pageLocation.port;
}

// If path is provided it'll be passed in via the resourceQuery as a
Expand Down
98 changes: 98 additions & 0 deletions tests/createSocketURL.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { rstest } from 'rstack/test';

rstest.mock('../client-src/socket.js', () => ({ default: rstest.fn() }));

let createSocketURL: typeof import('../client-src/index').createSocketURL;

beforeAll(async () => {
rstest.stubGlobal('BUILD_HASH', 'test');
rstest.stubGlobal('RESOURCE_QUERY', '?hostname=0.0.0.0&protocol=ws:&port=0');
rstest.stubGlobal('self', {
location: new URL('http://localhost/'),
addEventListener: rstest.fn(),
});
({ createSocketURL } = await import('../client-src/index'));
});

afterAll(() => rstest.unstubAllGlobals());

describe('createSocketURL', () => {
it.each([
['https://example.com', 'ws:', '0.0.0.0', '0', 'wss://example.com/ws'],
['http://localhost:8080', 'ws:', '0.0.0.0', '0', 'ws://localhost:8080/ws'],
[
'https://example.com:8443',
'ws:',
'0.0.0.0',
'0',
'wss://example.com:8443/ws',
],
['https://example.com', 'auto:', '0.0.0.0', '', 'wss://example.com/ws'],
['https://example.com', '', '', '', 'wss://example.com/ws'],
['https://example.com', 'ws:', '::', '0', 'wss://example.com/ws'],
['https://example.com', 'ws:', '[::]', '0', 'wss://example.com/ws'],
[
'https://example.com',
'ws:',
'dev.example.com',
'9000',
'ws://dev.example.com:9000/ws',
],
[
'http://localhost:8080',
'wss:',
'dev.example.com',
'443',
'wss://dev.example.com:443/ws',
],
[
'http://localhost:8080',
'wss:',
'0.0.0.0',
'9000',
'wss://localhost:9000/ws',
],
])(
'uses blob origin %s with protocol=%s, hostname=%s, port=%s',
(origin, protocol, hostname, port, expected) => {
const location = new URL(`blob:${origin}/worker-id`);
expect(location.hostname).toBe('');
rstest.stubGlobal('self', { location });
expect(createSocketURL({ protocol, hostname, port } as URL)).toBe(
expected,
);
},
);

it('preserves a configured path and credentials', () => {
rstest.stubGlobal('self', {
location: new URL('blob:https://example.com/worker-id'),
});
expect(
createSocketURL({
protocol: 'ws:',
hostname: '0.0.0.0',
port: '0',
pathname: '/build/sockjs-node',
username: 'user',
password: 'password',
} as URL),
).toBe('wss://user:password@example.com/build/sockjs-node');
});

it.each([
['https://example.com:8443/page', 'wss://example.com:8443/ws'],
['http://localhost:8080/worker.js', 'ws://localhost:8080/ws'],
['blob:null/worker-id', 'ws://0.0.0.0/ws'],
['data:text/javascript,', 'ws://0.0.0.0/ws'],
])('preserves existing behavior for %s', (href, expected) => {
rstest.stubGlobal('self', { location: new URL(href) });
expect(
createSocketURL({
protocol: 'ws:',
hostname: '0.0.0.0',
port: '0',
} as URL),
).toBe(expected);
});
});