Skip to content

Commit

Permalink
wip9
Browse files Browse the repository at this point in the history
  • Loading branch information
fredriklindberg committed Nov 11, 2023
1 parent 397862c commit d47e77b
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 25 deletions.
20 changes: 11 additions & 9 deletions src/ingress/http-ingress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export default class HttpIngress implements IngressBase {
}
this._agentCache.del(key);
agent.destroy();
agent.removeAllListeners();
this.logger.isDebugEnabled() &&
this.logger.withContext("tunnel", key).debug("http agent destroyed")
});
Expand Down Expand Up @@ -166,7 +167,8 @@ export default class HttpIngress implements IngressBase {
}
}
try {
return this.tunnelService.lookup(tunnelId);
const tunnel = await this.tunnelService.lookup(tunnelId);
return tunnel;
} catch (e) {
return false;
}
Expand Down Expand Up @@ -227,7 +229,7 @@ export default class HttpIngress implements IngressBase {
headers[HTTP_HEADER_X_REAL_IP] = headers[HTTP_HEADER_X_FORWARDED_FOR];

if (headers[HTTP_HEADER_EXPOSR_VIA]) {
headers[HTTP_HEADER_EXPOSR_VIA] = `${Node.identifier},${headers[HTTP_HEADER_EXPOSR_VIA] }`;
headers[HTTP_HEADER_EXPOSR_VIA] = `${Node.identifier},${headers[HTTP_HEADER_EXPOSR_VIA]}`;
} else {
headers[HTTP_HEADER_EXPOSR_VIA] = Node.identifier;
}
Expand Down Expand Up @@ -318,7 +320,7 @@ export default class HttpIngress implements IngressBase {
}

if (!tunnel.state.connected) {
httpResponse(502, {
httpResponse(503, {
error: ERROR_TUNNEL_NOT_CONNECTED,
});
return true;
Expand Down Expand Up @@ -370,10 +372,10 @@ export default class HttpIngress implements IngressBase {
res.statusCode = 429;
msg = ERROR_TUNNEL_TRANSPORT_REQUEST_LIMIT;
} else if (err.code == 'ECONNRESET') {
res.statusCode = 503;
res.statusCode = 502;
msg = ERROR_TUNNEL_TARGET_CON_REFUSED;
} else {
res.statusCode = 503;
res.statusCode = 502;
msg = ERROR_TUNNEL_TARGET_CON_FAILED;
}
res.end(JSON.stringify({error: msg}));
Expand Down Expand Up @@ -443,12 +445,12 @@ export default class HttpIngress implements IngressBase {
statusLine = 'Too Many Requests';
msg = ERROR_TUNNEL_TRANSPORT_REQUEST_LIMIT;
} else if ((err as any).code == 'ECONNRESET') {
statusCode = 503;
statusLine = 'Service Unavailable';
statusCode = 502;
statusLine = 'Bad Gateway';
msg = ERROR_TUNNEL_TARGET_CON_REFUSED;
} else {
statusCode = 503;
statusLine = 'Service Unavailable';
statusCode = 502;
statusLine = 'Bad Gateway';
msg = ERROR_TUNNEL_TARGET_CON_FAILED;
}
_canonicalHttpResponse(sock, req, {
Expand Down
29 changes: 19 additions & 10 deletions src/tunnel/tunnel-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ export default class TunnelService {
const updateHttp = async (): Promise<TunnelHttpIngressConfig> => {
if (!this.ingressService.enabled(IngressType.INGRESS_HTTP)) {
return {
enabled: this.ingressService.enabled(IngressType.INGRESS_HTTP),
enabled: false,
url: undefined,
urls: [],
alt_names: [],
Expand Down Expand Up @@ -475,19 +475,28 @@ export default class TunnelService {
return url.href;
});

return {
enabled: this.ingressService.enabled(IngressType.INGRESS_HTTP),
url: baseUrl.href,
urls: [
baseUrl.href,
...altUrls,
],
alt_names: altNames
if (tunnelConfig.ingress.http.enabled) {
return {
enabled: true,
url: baseUrl.href,
urls: [
baseUrl.href,
...altUrls,
],
alt_names: altNames
}
} else {
return {
enabled: false,
url: undefined,
urls: [],
alt_names: altNames
}
}
}

const updateSni = async (): Promise<TunnelIngressTypeConfig> => {
if (!this.ingressService.enabled(IngressType.INGRESS_SNI)) {
if (!this.ingressService.enabled(IngressType.INGRESS_SNI) || !tunnelConfig.ingress.sni.enabled) {
return {
enabled: this.ingressService.enabled(IngressType.INGRESS_SNI),
url: undefined,
Expand Down
205 changes: 199 additions & 6 deletions test/unit/ingress/test_http_ingress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import dns from 'dns/promises';
import AccountService from "../../../src/account/account-service.js";
import EventBus from "../../../src/cluster/eventbus.js";
import Config from "../../../src/config.js";
import IngressManager from "../../../src/ingress/ingress-manager.js";
import IngressManager, { IngressType } from "../../../src/ingress/ingress-manager.js";
import TunnelService from "../../../src/tunnel/tunnel-service.js";
import { createEchoHttpServer, initClusterService, initStorageService, wsSocketPair, wsmPair } from "../test-utils.js";
import sinon from 'sinon';
import net from 'net'
import http, { IncomingMessage } from 'http';
import http from 'http';
import Tunnel from '../../../src/tunnel/tunnel.js';
import Account from '../../../src/account/account.js';
import { StorageService } from '../../../src/storage/index.js';
Expand All @@ -18,6 +18,7 @@ import { WebSocketMultiplex } from '@exposr/ws-multiplex';
import WebSocketTransport from '../../../src/transport/ws/ws-transport.js';
import { Duplex } from 'stream';
import CustomError, { ERROR_TUNNEL_INGRESS_BAD_ALT_NAMES } from '../../../src/utils/errors.js';
import HttpIngress from '../../../src/ingress/http-ingress.js';

describe('http ingress', () => {
let clock: sinon.SinonFakeTimers;
Expand Down Expand Up @@ -273,6 +274,35 @@ describe('http ingress', () => {
await tunnelService.disconnect(tunnel.id, account.id);
});

it('agent timeout on idle', async () => {
assert(tunnel != undefined);
assert(account != undefined);

forwardTo("localhost", 20000);
await connectTunnel();

const {status, data} = await httpRequest({
hostname: 'localhost',
port: 10000,
method: 'GET',
path: '/',
headers: {
"Host": `${tunnel.id}.localhost.example`,
}
});

const instance: HttpIngress = IngressManager.getIngress(IngressType.INGRESS_HTTP) as HttpIngress;
let agent = instance["_agentCache"].get(tunnel.id);
assert(agent != undefined);

await clock.tickAsync(10000);

let agent2 = instance["_agentCache"].get(tunnel.id);
assert(agent2 == undefined);

await tunnelService.disconnect(tunnel.id, account.id);
});

it(`http ingress can handle websocket upgrades`, async () => {
assert(tunnel != undefined);
assert(account != undefined);
Expand Down Expand Up @@ -330,15 +360,15 @@ describe('http ingress', () => {
}
});

const done = (resolve: (value: any) => void) => {
const wsWait = new Promise((resolve: (value: any) => void) => {
req.on('upgrade', (res, socket, head) => {
const body = head.subarray(2);
resolve(body);
});
};
req.end();
req.end();
});

const wsRes = await new Promise(done);
const wsRes = await wsWait;

assert(wsRes.equals(Buffer.from("ws echo connected")), `did not get ws echo, got ${wsRes}`);
});
Expand Down Expand Up @@ -517,5 +547,168 @@ describe('http ingress', () => {
assert(headers['x-forwarded-proto'] == "http", `unexpected x-forwarded-proto, got ${headers['x-forwarded-proto']}`);
const forwarded = `by=_exposr;for=127.0.0.2;host=${tunnel.id}.localhost.example;proto=http`
assert(headers['forwarded'] == forwarded, `unexpected forwarded, got ${headers['forwarded']}`);
assert(headers['x-forwarded-host'] == `${tunnel.id}.localhost.example`, `${headers['x-forwarded-host']}`);
});

it('x-forwarded headers from request are read', async () => {
assert(tunnel != undefined);
assert(account != undefined);

forwardTo("localhost", 20000);
await connectTunnel();

sinon.stub(net.Socket.prototype, <any>'_getpeername').returns({
address: "127.0.0.2"
});

const {status, data} = await httpRequest({
hostname: 'localhost',
port: 10000,
method: 'GET',
path: '/headers',
headers: {
"Host": `${tunnel.id}.localhost.example`,
"x-forwarded-for": "127.0.0.3",
"x-forwarded-proto": "https",
}
});

sinon.restore();
await tunnelService.disconnect(tunnel.id, account.id);

const headers = JSON.parse(data);
assert(headers['x-forwarded-for'] == "127.0.0.3", `unexpected x-forwarded-for, got ${headers['x-forwarded-for']}`);
assert(headers['x-real-ip'] == "127.0.0.3", `unexpected x-real-ip, got ${headers['x-real-ip']}`);
assert(headers['x-forwarded-proto'] == "https", `unexpected x-forwarded-proto, got ${headers['x-forwarded-proto']}`);
const forwarded = `by=_exposr;for=127.0.0.3;host=${tunnel.id}.localhost.example;proto=https`
assert(headers['forwarded'] == forwarded, `unexpected forwarded, got ${headers['forwarded']}`);
});

it('exposr via header is added to request', async () => {
assert(tunnel != undefined);
assert(account != undefined);

forwardTo("localhost", 20000);
await connectTunnel();

const {status, data} = await httpRequest({
hostname: 'localhost',
port: 10000,
method: 'GET',
path: '/headers',
headers: {
"Host": `${tunnel.id}.localhost.example`,
}
});

await tunnelService.disconnect(tunnel.id, account.id);

const headers = JSON.parse(data);
assert(headers['exposr-via']?.length > 0, `via header not set`);
});

it('request loops returns 508', async () => {
assert(tunnel != undefined);
assert(account != undefined);

forwardTo("localhost", 10000);
await connectTunnel();

const {status, data} = await httpRequest({
hostname: 'localhost',
port: 10000,
method: 'GET',
path: '/headers',
headers: {
"Host": `${tunnel.id}.localhost.example`,
}
});

await tunnelService.disconnect(tunnel.id, account.id);

assert(status == 508, `expected status 508, got ${status}`);
});

it('un-responsive target returns 502', async () => {
assert(tunnel != undefined);
assert(account != undefined);

forwardTo("localhost", 20001);
await connectTunnel();

const {status, data} = await httpRequest({
hostname: 'localhost',
port: 10000,
method: 'GET',
path: '/headers',
headers: {
"Host": `${tunnel.id}.localhost.example`,
}
});

await tunnelService.disconnect(tunnel.id, account.id);

assert(status == 502, `expected status 502, got ${status}`);
});

it('connection to non-existing tunnel returns 404', async () => {
assert(tunnel != undefined);
assert(account != undefined);

const {status, data} = await httpRequest({
hostname: 'localhost',
port: 10000,
method: 'GET',
path: '/headers',
headers: {
"Host": `does-not-exist.localhost.example`,
}
});

assert(status == 404, `expected status 404, got ${status}`);
});

it('non-connected tunnel returns 503', async () => {
assert(tunnel != undefined);
assert(account != undefined);

const {status, data} = await httpRequest({
hostname: 'localhost',
port: 10000,
method: 'GET',
path: '/headers',
headers: {
"Host": `${tunnel.id}.localhost.example`,
}
});

assert(status == 503, `expected status 503, got ${status}`);
});

it('disabled ingress returns 403', async () => {
assert(tunnel != undefined);
assert(account != undefined);

tunnel = await tunnelService.update(tunnel.id, account?.id, (config) => {
config.ingress.http.enabled = false;
});

forwardTo("localhost", 20000);
await connectTunnel();

const {status, data} = await httpRequest({
hostname: 'localhost',
port: 10000,
method: 'GET',
path: '/headers',
headers: {
"Host": `${tunnel.id}.localhost.example`,
}
});

await tunnelService.disconnect(tunnel.id, account.id);

assert(status == 403, `expected status 403, got ${status}`);
});

});

0 comments on commit d47e77b

Please sign in to comment.