216 lines
9.8 KiB
JavaScript
216 lines
9.8 KiB
JavaScript
import net from 'net';
|
|
import request from 'supertest';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
import { createServer } from './server.js';
|
|
describe('Server', () => {
|
|
it('server starts and stops', async () => {
|
|
const server = createServer();
|
|
await new Promise(resolve => server.listen(resolve));
|
|
await new Promise(resolve => server.close(() => resolve()));
|
|
});
|
|
it('should redirect root requests to landing page', async () => {
|
|
const server = createServer();
|
|
const res = await request(server).get('/');
|
|
expect(res.headers.location).toBe('https://pipenet.dev/');
|
|
});
|
|
it('should support custom base domains', async () => {
|
|
const server = createServer({
|
|
domains: ['domain.example.com'],
|
|
});
|
|
const res = await request(server).get('/');
|
|
expect(res.headers.location).toBe('https://pipenet.dev/');
|
|
});
|
|
it('reject long domain name requests', async () => {
|
|
const server = createServer();
|
|
const res = await request(server).get('/thisdomainisoutsidethesizeofwhatweallowwhichissixtythreecharacters');
|
|
expect(res.body.message).toBe('Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.');
|
|
});
|
|
it('should upgrade websocket requests', async () => {
|
|
const hostname = 'websocket-test';
|
|
const server = createServer({
|
|
domains: ['example.com'],
|
|
});
|
|
await new Promise(resolve => server.listen(resolve));
|
|
const res = await request(server).get('/websocket-test');
|
|
const localTunnelPort = res.body.port;
|
|
const wss = await new Promise((resolve) => {
|
|
const wsServer = new WebSocketServer({ port: 0 }, () => {
|
|
resolve(wsServer);
|
|
});
|
|
});
|
|
const websocketServerPort = wss.address().port;
|
|
const ltSocket = net.createConnection({ port: localTunnelPort });
|
|
const wsSocket = net.createConnection({ port: websocketServerPort });
|
|
// Wait for both sockets to connect
|
|
await Promise.all([
|
|
new Promise(resolve => ltSocket.once('connect', resolve)),
|
|
new Promise(resolve => wsSocket.once('connect', resolve)),
|
|
]);
|
|
ltSocket.pipe(wsSocket).pipe(ltSocket);
|
|
wss.once('connection', (ws) => {
|
|
ws.once('message', (message) => {
|
|
ws.send(message);
|
|
});
|
|
});
|
|
const ws = new WebSocket('http://localhost:' + server.address().port, {
|
|
headers: {
|
|
host: hostname + '.example.com',
|
|
}
|
|
});
|
|
ws.on('open', () => {
|
|
ws.send('something');
|
|
});
|
|
await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => reject(new Error('WebSocket message timeout')), 10000);
|
|
ws.once('message', (msg) => {
|
|
clearTimeout(timeout);
|
|
expect(msg.toString()).toBe('something');
|
|
resolve();
|
|
});
|
|
ws.once('error', (err) => {
|
|
clearTimeout(timeout);
|
|
reject(err);
|
|
});
|
|
});
|
|
ws.close();
|
|
ltSocket.destroy();
|
|
wsSocket.destroy();
|
|
wss.close();
|
|
await new Promise(resolve => server.close(() => resolve()));
|
|
});
|
|
it('should support the /api/tunnels/:id/status endpoint', async () => {
|
|
const server = createServer();
|
|
await new Promise(resolve => server.listen(resolve));
|
|
// no such tunnel yet
|
|
const res = await request(server).get('/api/tunnels/foobar-test/status');
|
|
expect(res.statusCode).toBe(404);
|
|
// request a new client called foobar-test
|
|
await request(server).get('/foobar-test');
|
|
{
|
|
const res = await request(server).get('/api/tunnels/foobar-test/status');
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body).toEqual({
|
|
connectedSockets: 0,
|
|
});
|
|
}
|
|
await new Promise(resolve => server.close(() => resolve()));
|
|
});
|
|
it('should include CORS headers in responses', async () => {
|
|
const server = createServer();
|
|
const res = await request(server).get('/api/status');
|
|
expect(res.headers['access-control-allow-origin']).toBe('*');
|
|
expect(res.headers['access-control-allow-methods']).toBe('GET, POST, PUT, DELETE, OPTIONS');
|
|
expect(res.headers['access-control-allow-headers']).toBe('Content-Type, Authorization');
|
|
});
|
|
it('should handle OPTIONS preflight requests', async () => {
|
|
const server = createServer();
|
|
const res = await request(server).options('/api/status');
|
|
expect(res.statusCode).toBe(204);
|
|
expect(res.headers['access-control-allow-origin']).toBe('*');
|
|
});
|
|
describe('hooks', () => {
|
|
it('should call onTunnelCreated when a tunnel is created', async () => {
|
|
const onTunnelCreated = vi.fn();
|
|
const server = createServer({ onTunnelCreated });
|
|
await new Promise(resolve => server.listen(resolve));
|
|
await request(server).get('/test-tunnel');
|
|
expect(onTunnelCreated).toHaveBeenCalledTimes(1);
|
|
expect(onTunnelCreated).toHaveBeenCalledWith(expect.objectContaining({
|
|
domain: expect.any(String),
|
|
id: 'test-tunnel',
|
|
url: expect.stringContaining('test-tunnel'),
|
|
}));
|
|
await new Promise(resolve => server.close(() => resolve()));
|
|
});
|
|
it('should include domain in tunnel hooks', async () => {
|
|
const onTunnelCreated = vi.fn();
|
|
const server = createServer({
|
|
domains: ['tunnel.example.com'],
|
|
onTunnelCreated,
|
|
});
|
|
await new Promise(resolve => server.listen(resolve));
|
|
await request(server)
|
|
.get('/domain-test')
|
|
.set('Host', 'tunnel.example.com');
|
|
expect(onTunnelCreated).toHaveBeenCalledTimes(1);
|
|
expect(onTunnelCreated).toHaveBeenCalledWith({
|
|
domain: 'tunnel.example.com',
|
|
id: 'domain-test',
|
|
url: 'http://domain-test.tunnel.example.com',
|
|
});
|
|
await new Promise(resolve => server.close(() => resolve()));
|
|
});
|
|
it('should call onTunnelClosed when a tunnel is closed', async () => {
|
|
const onTunnelCreated = vi.fn();
|
|
const onTunnelClosed = vi.fn();
|
|
const server = createServer({ onTunnelClosed, onTunnelCreated });
|
|
await new Promise(resolve => server.listen(resolve));
|
|
const res = await request(server).get('/close-test');
|
|
const localTunnelPort = res.body.port;
|
|
// Connect a socket to activate the tunnel
|
|
const socket = net.createConnection({ port: localTunnelPort });
|
|
await new Promise(resolve => socket.once('connect', resolve));
|
|
expect(onTunnelCreated).toHaveBeenCalledTimes(1);
|
|
expect(onTunnelClosed).not.toHaveBeenCalled();
|
|
// Close the socket to trigger tunnel close
|
|
socket.destroy();
|
|
// Wait for the close event to propagate (Client has a 1s grace timeout after offline)
|
|
await new Promise(resolve => setTimeout(resolve, 1200));
|
|
expect(onTunnelClosed).toHaveBeenCalledTimes(1);
|
|
expect(onTunnelClosed).toHaveBeenCalledWith(expect.objectContaining({
|
|
domain: expect.any(String),
|
|
id: 'close-test',
|
|
url: expect.stringContaining('close-test'),
|
|
}));
|
|
await new Promise(resolve => server.close(() => resolve()));
|
|
}, 5000);
|
|
it('should call onRequest when a request is proxied', async () => {
|
|
const onRequest = vi.fn();
|
|
const server = createServer({
|
|
domains: ['example.com'],
|
|
onRequest,
|
|
});
|
|
await new Promise(resolve => server.listen(resolve));
|
|
// Create a tunnel
|
|
const res = await request(server).get('/request-test');
|
|
const localTunnelPort = res.body.port;
|
|
// Create a simple echo server
|
|
const echoServer = net.createServer((socket) => {
|
|
socket.on('data', () => {
|
|
const response = 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK';
|
|
socket.write(response);
|
|
});
|
|
});
|
|
await new Promise(resolve => echoServer.listen(0, resolve));
|
|
const echoPort = echoServer.address().port;
|
|
// Connect tunnel socket to echo server
|
|
const ltSocket = net.createConnection({ port: localTunnelPort });
|
|
const echoSocket = net.createConnection({ port: echoPort });
|
|
await Promise.all([
|
|
new Promise(resolve => ltSocket.once('connect', resolve)),
|
|
new Promise(resolve => echoSocket.once('connect', resolve)),
|
|
]);
|
|
ltSocket.pipe(echoSocket).pipe(ltSocket);
|
|
// Make a request through the tunnel
|
|
await request(server)
|
|
.get('/some/path')
|
|
.set('Host', 'request-test.example.com');
|
|
expect(onRequest).toHaveBeenCalledTimes(1);
|
|
expect(onRequest).toHaveBeenCalledWith({
|
|
headers: expect.objectContaining({
|
|
host: 'request-test.example.com',
|
|
}),
|
|
method: 'GET',
|
|
path: '/some/path',
|
|
remoteAddress: expect.any(String),
|
|
tunnelId: 'request-test',
|
|
});
|
|
ltSocket.destroy();
|
|
echoSocket.destroy();
|
|
echoServer.close();
|
|
await new Promise(resolve => server.close(() => resolve()));
|
|
});
|
|
});
|
|
});
|
|
//# sourceMappingURL=server.spec.js.map
|