diff --git a/lib/socket-listeners.js b/lib/socket-listeners.js index 496faad..0e64a85 100644 --- a/lib/socket-listeners.js +++ b/lib/socket-listeners.js @@ -25,13 +25,14 @@ export function initListeners(hsyncClient) { port: l.port, targetHost: l.targetHost, targetPort: l.targetPort, + hasPassword: !!l.password, }; }); return retVal; } function addSocketListener(options = {}) { - const { port, targetPort, targetHost } = options; + const { port, targetPort, targetHost, password } = options; if (!targetHost) { throw new Error('no targetHost'); } @@ -134,6 +135,7 @@ export function initListeners(hsyncClient) { socketId: socket.socketId, port: targetPort || port, hostName: rpcPeer.hostName, + password, }); debug('connect result', result); socket.peerConnected = true; @@ -170,6 +172,7 @@ export function initListeners(hsyncClient) { targetHost: cleanHost, targetPort: targetPort || port, port, + password, }; socketListeners['p' + port] = listener; diff --git a/lib/socket-relays.js b/lib/socket-relays.js index a41448a..243b507 100644 --- a/lib/socket-relays.js +++ b/lib/socket-relays.js @@ -27,12 +27,13 @@ export function initRelays(hsyncClient) { whitelist: l.whitelist || '', blacklist: l.blacklist || '', hostName: l.targetHost, + hasPassword: !!l.password, }; }); return retVal; } - function connectSocket(peer, { port, socketId, hostName }) { + function connectSocket(peer, { port, socketId, hostName, password }) { debug('connectSocket', port, socketId, hostName); peer.notifications.oncloseRelaySocket((peer, { socketId }) => { @@ -51,6 +52,17 @@ export function initRelays(hsyncClient) { throw new Error('no relay found for port: ' + port); } + // Check password if relay requires one + if (relay.password) { + if (!password) { + throw new Error('password required for relay on port: ' + port); + } + if (relay.password !== password) { + throw new Error('invalid password for relay on port: ' + port); + } + debug('password verified for relay', port); + } + // TODO: check white and black lists on peer // const relayDataTopic = `msg/${hostName}/${hsyncClient.myHostName}/relayData/${socketId}`; @@ -92,10 +104,10 @@ export function initRelays(hsyncClient) { }); } - function addSocketRelay({ whitelist, blacklist, port, targetPort, targetHost }) { + function addSocketRelay({ whitelist, blacklist, port, targetPort, targetHost, password }) { targetPort = targetPort || port; targetHost = targetHost || 'localhost'; - debug('creating relay', whitelist, blacklist, port, targetPort, targetHost); + debug('creating relay', whitelist, blacklist, port, targetPort, targetHost, password ? '(password protected)' : ''); const newRelay = { whitelist, blacklist, @@ -103,6 +115,7 @@ export function initRelays(hsyncClient) { targetPort, targetHost, hostName: targetHost, + password, }; cachedRelays['p' + port] = newRelay; return newRelay; diff --git a/test/unit/socket-listeners.test.js b/test/unit/socket-listeners.test.js index 4654bfb..02fd77c 100644 --- a/test/unit/socket-listeners.test.js +++ b/test/unit/socket-listeners.test.js @@ -190,6 +190,7 @@ describe('socket-listeners', () => { port: 3000, targetHost: 'https://remote.example.com', targetPort: 4000, + hasPassword: false, }); }); @@ -315,5 +316,61 @@ describe('socket-listeners', () => { }) ); }); + + it('should pass password to connectSocket', async () => { + listeners.addSocketListener({ + port: 3000, + targetPort: 4000, + targetHost: 'https://remote.example.com', + password: 'secret123', + }); + + // Trigger socket connection + const connectionHandler = mockNet.createServer.mock.calls[0][0]; + connectionHandler(mockSocket); + + // Wait for connection attempt + await vi.waitFor(() => { + expect(mockRpcPeer.methods.connectSocket).toHaveBeenCalled(); + }); + + expect(mockRpcPeer.methods.connectSocket).toHaveBeenCalledWith( + expect.objectContaining({ + password: 'secret123', + }) + ); + }); + }); + + describe('addSocketListener with password', () => { + let listeners; + + beforeEach(() => { + listeners = initListeners(mockHsyncClient); + }); + + it('should store password in listener', () => { + const listener = listeners.addSocketListener({ + port: 3000, + targetHost: 'https://remote.example.com', + password: 'secret123', + }); + + expect(listener.password).toBe('secret123'); + }); + + it('should indicate hasPassword in getSocketListeners', () => { + listeners.addSocketListener({ + port: 3000, + targetHost: 'https://remote.example.com', + password: 'secret123', + }); + + const result = listeners.getSocketListeners(); + + expect(result[0].hasPassword).toBe(true); + // Password should NOT be exposed + expect(result[0].password).toBeUndefined(); + }); }); }); diff --git a/test/unit/socket-relays.test.js b/test/unit/socket-relays.test.js index 6d10dd6..0aa06d4 100644 --- a/test/unit/socket-relays.test.js +++ b/test/unit/socket-relays.test.js @@ -160,6 +160,7 @@ describe('socket-relays', () => { whitelist: 'allowed.com', blacklist: 'blocked.com', hostName: 'myserver.local', + hasPassword: false, }); }); @@ -311,5 +312,96 @@ describe('socket-relays', () => { await expect(connectPromise).rejects.toThrow('Connection failed'); }); + + it('should connect successfully with correct password', async () => { + relays.addSocketRelay({ + port: 3000, + password: 'secret123', + }); + + const result = await relays.connectSocket(mockPeer, { + port: 3000, + socketId: 'test-socket', + hostName: 'remote.example.com', + password: 'secret123', + }); + + expect(result.socketId).toBe('test-socket'); + }); + + it('should throw if password required but not provided', () => { + relays.addSocketRelay({ + port: 3000, + password: 'secret123', + }); + + expect(() => { + relays.connectSocket(mockPeer, { + port: 3000, + socketId: 'test-socket', + hostName: 'remote.example.com', + }); + }).toThrow('password required for relay on port: 3000'); + }); + + it('should throw if password is incorrect', () => { + relays.addSocketRelay({ + port: 3000, + password: 'secret123', + }); + + expect(() => { + relays.connectSocket(mockPeer, { + port: 3000, + socketId: 'test-socket', + hostName: 'remote.example.com', + password: 'wrongpassword', + }); + }).toThrow('invalid password for relay on port: 3000'); + }); + + it('should connect without password if relay has no password', async () => { + relays.addSocketRelay({ + port: 3000, + }); + + const result = await relays.connectSocket(mockPeer, { + port: 3000, + socketId: 'test-socket', + hostName: 'remote.example.com', + }); + + expect(result.socketId).toBe('test-socket'); + }); + }); + + describe('addSocketRelay with password', () => { + let relays; + + beforeEach(() => { + relays = initRelays(mockHsyncClient); + }); + + it('should store password in relay', () => { + const relay = relays.addSocketRelay({ + port: 3000, + password: 'secret123', + }); + + expect(relay.password).toBe('secret123'); + }); + + it('should indicate hasPassword in getSocketRelays', () => { + relays.addSocketRelay({ + port: 3000, + password: 'secret123', + }); + + const result = relays.getSocketRelays(); + + expect(result[0].hasPassword).toBe(true); + // Password should NOT be exposed + expect(result[0].password).toBeUndefined(); + }); }); });