Skip to content
Merged
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
3 changes: 2 additions & 1 deletion apps/api-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ bun run --filter api-service dev
| `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_NAME` | `DB_PORT` `5432`, others required | UNS indexer Postgres database, read only |
| `DB_PASS` | none | its password, handled as a [secret](../../packages/service-kit/#secrets) |
| `UNS_DOMAINS_CACHE_TTL_SECONDS` | `30` | how long the domain list is served from memory |
| `HARDWARE_ENDPOINT_ENABLED` | `false` | `true` serves `POST /hardware`; any value other than `true` or `false` stops the service at startup |
| `PORT`, `LOG_LEVEL` | | see [service-kit](../../packages/service-kit/#environment) |

The Nomad jobs set `GEO_IP_SERVICE_URL` from Consul, as [operations](../../operations/#reaching-it-from-another-job) describes.
Expand All @@ -54,7 +55,7 @@ The Nomad jobs set `GEO_IP_SERVICE_URL` from Consul, as [operations](../../opera
| `GET /relays?fingerprints=<a>,<b>` | the same for every known relay in the list; `400` without fingerprints |
| `GET /relay-map/` | H3 cells holding relays: `index`, `relayCount`, `geo` (cell center) and `boundary` |
| `GET /fingerprint-map/` | per relay fingerprint: `hexId`, `coordinates` (cell center), country, region, city and autonomous system |
| `POST /hardware` | validates hardware relay info, which must include an `ATEC` entry in `serNums`, and stores it in Onionoo; `400` with the list of validation errors |
| `POST /hardware` | only with `HARDWARE_ENDPOINT_ENABLED=true`, otherwise the not found page: validates hardware relay info, which must include an `ATEC` entry in `serNums`, and stores it in Onionoo; `400` with the list of validation errors |
| `GET /operators` | operator addresses; `?withDomains=true` adds the `.anyone` domains each one owns |
| `GET /anyone-domains` | every `.anyone` domain with its token id and owner |
| `GET /healthz` | service health, see [Health](#health) |
Expand Down
20 changes: 18 additions & 2 deletions apps/api-service/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GeoIpClient, type GeoLocation } from '@anyone-protocol/geo-ip-client';
import { createLogger } from '@anyone-protocol/service-kit';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { cellToBoundary } from 'h3-js';
import { startServer } from './app';
import { type ServerOptions, startServer } from './app';
import { Onionoo, type Relay } from './onionoo';
import { OperatorRegistry } from './operator-registry';
import { StubGeoIpService, StubUpstreams } from './testing/stubs';
Expand Down Expand Up @@ -83,7 +83,7 @@ let server: ReturnType<typeof startServer>;
let clock: number;
let domainRows: () => Promise<UnsTokenRow[]>;

function serve() {
function serve(options: Partial<ServerOptions> = {}) {
const logger = createLogger({ service: 'api-service', write: () => {} });
server = startServer({
port: 0,
Expand All @@ -98,6 +98,7 @@ function serve() {
operatorRegistry: new OperatorRegistry({ nodeUrl: upstreams.url, processId: PROCESS_ID, cacheTtlMs: 0, logger, read: { retryDelayMs: 1 } }),
unsDomains: new UnsDomains({ rows: () => domainRows(), cacheTtlMs: 0, logger }),
logger,
...options,
});
return server;
}
Expand Down Expand Up @@ -299,7 +300,22 @@ describe('when geo-ip-service is down', () => {
});
});

describe('POST /hardware when the endpoint is off', () => {
it('answers 404 by default and does not call Onionoo', async () => {
const response = await post('/hardware', JSON.stringify({ fingerprint: 'A'.repeat(40) }));

expect(response.status).toBe(404);
expect(await response.text()).toContain('Cannot POST /hardware');
expect(upstreams.requests).toHaveLength(0);
});
});

describe('POST /hardware', () => {
beforeEach(async () => {
await server.stop(true);
serve({ hardwareEnabled: true });
});

const hardwareInfo = {
id: 'id',
company: 'company',
Expand Down
25 changes: 15 additions & 10 deletions apps/api-service/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ServerOptions {
operatorRegistry: OperatorRegistry;
unsDomains: UnsDomains;
logger: Logger;
hardwareEnabled?: boolean;
}

type Handler = (request: BunRequest) => Response | Promise<Response>;
Expand Down Expand Up @@ -46,7 +47,7 @@ function withTrailingSlashes(routes: Routes): Routes {
return Object.fromEntries(Object.entries(routes).flatMap(([path, methods]) => [[path, methods], [`${path}/`, methods]]));
}

export function startServer({ port, onionoo, relayMetrics, geo, hexagonResolution, operatorRegistry, unsDomains, logger }: ServerOptions) {
export function startServer({ port, onionoo, relayMetrics, geo, hexagonResolution, operatorRegistry, unsDomains, logger, hardwareEnabled = false }: ServerOptions) {
const health = createHealthReporter('api-service', { checks: { geoIp: () => geoIpCheck(geo) } });

const answer =
Expand Down Expand Up @@ -107,17 +108,21 @@ export function startServer({ port, onionoo, relayMetrics, geo, hexagonResolutio
}),
},

'/hardware': {
POST: answer('Error posting hardware info', async (request) => {
const body = await readJsonBody(request);
if (body instanceof Response) return body;
...(hardwareEnabled
? {
'/hardware': {
POST: answer('Error posting hardware info', async (request) => {
const body = await readJsonBody(request);
if (body instanceof Response) return body;

const errors = validateHardwareInfo(body);
if (errors.length > 0) return json(request, { errors }, 400);
const errors = validateHardwareInfo(body);
if (errors.length > 0) return json(request, { errors }, 400);

return relayedText(request, await onionoo.updateHardwareInfo(body as HardwareInfo));
}),
},
return relayedText(request, await onionoo.updateHardwareInfo(body as HardwareInfo));
}),
},
}
: {}),

'/operators': {
GET: answer('Error querying operators', async (request) => {
Expand Down
9 changes: 9 additions & 0 deletions apps/api-service/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,18 @@ describe('loadConfig', () => {
OPERATOR_REGISTRY_CACHE_TTL_SECONDS: 0,
DB_PORT: 5432,
UNS_DOMAINS_CACHE_TTL_SECONDS: 30,
HARDWARE_ENDPOINT_ENABLED: 'false',
});
});

it('turns the hardware endpoint on only for true', () => {
expect(loadConfig({ ...REQUIRED, HARDWARE_ENDPOINT_ENABLED: 'true' }).HARDWARE_ENDPOINT_ENABLED).toBe('true');
});

it.each(['yes', '1', 'TRUE', 'on'])('refuses HARDWARE_ENDPOINT_ENABLED=%s', (value) => {
expect(() => loadConfig({ ...REQUIRED, HARDWARE_ENDPOINT_ENABLED: value })).toThrow('HARDWARE_ENDPOINT_ENABLED must be one of true, false');
});

it.each(['HB_URL', 'OPERATOR_REGISTRY_PROCESS_ID', 'ONIONOO_INSTANCE', 'VICTORIA_METRICS_ADDRESS', 'GEO_IP_SERVICE_URL', 'DB_HOST'])('fails closed without %s', (name) => {
expect(() => loadConfig({ ...REQUIRED, [name]: undefined })).toThrow(`${name} is required`);
});
Expand Down
1 change: 1 addition & 0 deletions apps/api-service/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const apiServiceEnv = {
DB_PASS: env.secret(env.string({ optional: true })),
DB_NAME: env.string(),
UNS_DOMAINS_CACHE_TTL_SECONDS: env.integer({ default: 30 }),
HARDWARE_ENDPOINT_ENABLED: env.oneOf(['true', 'false'], { default: 'false' }),
};

export type ApiServiceConfig = ReturnType<typeof loadConfig>;
Expand Down
2 changes: 2 additions & 0 deletions apps/api-service/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ const server = startServer({
}),
unsDomains: new UnsDomains({ rows: unsTokenRows(database), cacheTtlMs: config.UNS_DOMAINS_CACHE_TTL_SECONDS * SECOND_MS, logger }),
logger,
hardwareEnabled: config.HARDWARE_ENDPOINT_ENABLED === 'true',
});

logger.info('Listening', {
port: server.port,
onionoo: config.ONIONOO_INSTANCE,
operatorRegistryProcessId: config.OPERATOR_REGISTRY_PROCESS_ID,
geoIpService: config.GEO_IP_SERVICE_URL,
hardwareEndpointEnabled: config.HARDWARE_ENDPOINT_ENABLED === 'true',
});

handleShutdownSignals(
Expand Down
Loading