|
3 | 3 | import { Arrays, Objects } from 'cafe-utility' |
4 | 4 | import { IncomingMessage, ServerResponse, createServer } from 'http' |
5 | 5 | import fetch from 'node-fetch' |
| 6 | +import { Target, getHealthyTarget } from './target' |
6 | 7 | import { RequestContext, ResponseContext } from './types' |
7 | 8 | import { fetchWithTimeout, respondWithFetchPromise } from './utility' |
8 | 9 |
|
| 10 | +const PORT_ENV = 'ETHERPROXY_PORT' |
| 11 | +const EXPIRY_ENV = 'ETHERPROXY_EXPIRY' |
| 12 | +const TARGET_ENV = 'ETHERPROXY_TARGET' |
| 13 | +const DEFAULT_PORT = 9000 |
| 14 | +const DEFAULT_EXPIRY = 2000 |
| 15 | +const DEFAULT_TARGET = 'http://localhost:8545' |
| 16 | + |
9 | 17 | main() |
10 | 18 |
|
11 | 19 | function main() { |
12 | | - const port: number = parseInt(Arrays.getArgument(process.argv, 'port') as string) || parseInt((process.env.ETHERPROXY_PORT as string)) || 9000 |
13 | | - const target: string = Arrays.getArgument(process.argv, 'target') as string || process.env.ETHERPROXY_TARGET as string || "http://localhost:8545" |
14 | | - const expiry: number = parseInt(Arrays.getArgument(process.argv, 'expiry') as string) || parseInt((process.env.ETHERPROXY_EXPIRY as string)) || 2000 |
15 | | - |
| 20 | + const port = Arrays.getNumberArgument(process.argv, 'port', process.env, PORT_ENV) || DEFAULT_PORT |
| 21 | + const target = Arrays.getArgument(process.argv, 'target', process.env, TARGET_ENV) || DEFAULT_TARGET |
| 22 | + const expiry = Arrays.getNumberArgument(process.argv, 'expiry', process.env, EXPIRY_ENV) || DEFAULT_EXPIRY |
16 | 23 | const fastIndex = Objects.createFastIndex() |
| 24 | + const targets: Target[] = target.split(',').map(x => ({ |
| 25 | + url: x, |
| 26 | + lastErrorAt: 0 |
| 27 | + })) |
17 | 28 | const server = createServer(async (request: IncomingMessage, response: ServerResponse) => { |
| 29 | + request.on('error', error => { |
| 30 | + console.error(error) |
| 31 | + }) |
| 32 | + response.on('error', error => { |
| 33 | + console.error(error) |
| 34 | + }) |
18 | 35 | if (request.url === '/health' || request.url === '/readiness') { |
19 | | - try { |
20 | | - await fetch(target, { timeout: 10_000 }) |
21 | | - response.statusCode = 200 |
22 | | - response.end('200 OK') |
23 | | - } catch (error) { |
24 | | - console.error(error) |
25 | | - response.statusCode = 503 |
26 | | - response.end('503 Service Unavailable') |
| 36 | + for (let i = 0; i < targets.length; i++) { |
| 37 | + const target = getHealthyTarget(targets) |
| 38 | + try { |
| 39 | + await fetch(target.url, { timeout: 10_000 }) |
| 40 | + response.statusCode = 200 |
| 41 | + response.end('200 OK') |
| 42 | + return |
| 43 | + } catch (error) { |
| 44 | + target.lastErrorAt = Date.now() |
| 45 | + console.error(error) |
| 46 | + } |
27 | 47 | } |
| 48 | + response.statusCode = 503 |
| 49 | + response.end('503 Service Unavailable') |
28 | 50 | return |
29 | 51 | } |
30 | 52 | const chunks: Buffer[] = [] |
31 | 53 | request.on('data', (chunk: Buffer) => { |
32 | 54 | chunks.push(chunk) |
33 | 55 | }) |
34 | 56 | request.on('end', async () => { |
35 | | - try { |
36 | | - const context: RequestContext = { |
37 | | - method: request.method || 'GET', |
38 | | - url: target, |
39 | | - headers: request.headers as Record<string, string>, |
40 | | - body: Buffer.concat(chunks).toString('utf-8') |
41 | | - } |
42 | | - const parsedBody = JSON.parse(context.body) |
43 | | - const id = parsedBody.id |
44 | | - delete parsedBody.id |
45 | | - const key = JSON.stringify(parsedBody) |
46 | | - const cachedPromise = Objects.getFromFastIndexWithExpiracy(fastIndex, key) as Promise<ResponseContext> |
47 | | - if (cachedPromise) { |
48 | | - process.stdout.write(`[~] Cache hit: ${key}\n`) |
49 | | - await respondWithFetchPromise(id, response, cachedPromise) |
50 | | - return |
| 57 | + for (let i = 0; i < targets.length; i++) { |
| 58 | + const target = getHealthyTarget(targets) |
| 59 | + try { |
| 60 | + const context: RequestContext = { |
| 61 | + method: request.method || 'GET', |
| 62 | + url: target.url, |
| 63 | + headers: request.headers as Record<string, string>, |
| 64 | + body: Buffer.concat(chunks).toString('utf-8') |
| 65 | + } |
| 66 | + const parsedBody = JSON.parse(context.body) |
| 67 | + const id = parsedBody.id |
| 68 | + delete parsedBody.id |
| 69 | + const key = `${target.url}_${JSON.stringify(parsedBody)}` |
| 70 | + const cachedPromise = Objects.getFromFastIndexWithExpiracy( |
| 71 | + fastIndex, |
| 72 | + key |
| 73 | + ) as Promise<ResponseContext> |
| 74 | + if (cachedPromise) { |
| 75 | + process.stdout.write(`Cache hit: ${key}\n`) |
| 76 | + const successful = await respondWithFetchPromise(id, response, cachedPromise) |
| 77 | + if (successful) { |
| 78 | + return |
| 79 | + } else { |
| 80 | + target.lastErrorAt = Date.now() |
| 81 | + continue |
| 82 | + } |
| 83 | + } |
| 84 | + process.stdout.write(`Key: ${key}\n`) |
| 85 | + delete context.headers.host |
| 86 | + delete context.headers['user-agent'] |
| 87 | + delete context.headers['content-length'] |
| 88 | + const responsePromise = fetchWithTimeout(context.url, { |
| 89 | + method: context.method, |
| 90 | + headers: context.headers, |
| 91 | + body: context.body |
| 92 | + }) |
| 93 | + Objects.pushToFastIndexWithExpiracy(fastIndex as any, key, responsePromise, expiry) |
| 94 | + const successful = await respondWithFetchPromise(id, response, responsePromise) |
| 95 | + if (successful) { |
| 96 | + return |
| 97 | + } else { |
| 98 | + target.lastErrorAt = Date.now() |
| 99 | + continue |
| 100 | + } |
| 101 | + } catch (error) { |
| 102 | + target.lastErrorAt = Date.now() |
| 103 | + console.error(error) |
51 | 104 | } |
52 | | - process.stdout.write(`[~] Key: ${key}\n`) |
53 | | - delete context.headers.host |
54 | | - delete context.headers['user-agent'] |
55 | | - delete context.headers['content-length'] |
56 | | - const responsePromise = fetchWithTimeout(context.url, { |
57 | | - method: context.method, |
58 | | - headers: context.headers, |
59 | | - body: context.body |
60 | | - }) |
61 | | - Objects.pushToFastIndexWithExpiracy(fastIndex as any, key, responsePromise, expiry) |
62 | | - await respondWithFetchPromise(id, response, responsePromise) |
63 | | - } catch (error) { |
64 | | - console.error(error) |
65 | | - response.statusCode = 503 |
66 | | - response.end('503 Service Unavailable') |
67 | 105 | } |
| 106 | + response.statusCode = 503 |
| 107 | + response.end('503 Service Unavailable') |
68 | 108 | }) |
69 | 109 | }) |
70 | 110 | server.listen(port) |
71 | | - console.log(`[~] Etherproxy is running on port ${port}`) |
| 111 | + console.log(`Etherproxy is running on port ${port}`) |
72 | 112 | } |
0 commit comments