diff --git a/src/bin/options.ts b/src/bin/options.ts index 3216f6fa31..1ba987eebf 100644 --- a/src/bin/options.ts +++ b/src/bin/options.ts @@ -1,3 +1,9 @@ +import type { NodeAddress, NodeId, NodeMapping } from '../nodes/types'; +import type { Host, Hostname, Port } from '../network/types'; +import * as nodesUtils from '../nodes/utils'; +import * as networkUtils from '../network/utils'; +import * as errors from '../errors'; + import commander from 'commander'; import config from '../config'; @@ -9,6 +15,76 @@ function parseNumber(v: string): number { return num; } +/** + * Seed nodes expected to be of form 'nodeId1@host:port;nodeId2@host:port;...' + * By default, any specified seed nodes (in CLI option, or environment variable) + * will overwrite the default nodes in src/config.ts. + * Special flag '' in the content indicates that the default seed + * nodes should be added to the starting seed nodes instead of being overwritten. + */ +function parseSeedNodes(rawSeedNodes: string): NodeMapping { + const seedNodeMappings: NodeMapping = {}; + // If specifically set no seed nodes, then ensure we start with none + if (rawSeedNodes == '') return seedNodeMappings; + const semicolonSeedNodes = rawSeedNodes.split(';'); + for (const rawSeedNode of semicolonSeedNodes) { + // Empty string will occur if there's an extraneous ';' (e.g. at end of env) + if (rawSeedNode == '') continue; + // Append the default seed nodes if we encounter the special flag + if (rawSeedNode == '') { + const defaultSeedNodes = getDefaultSeedNodes(); + for (const id in defaultSeedNodes) { + seedNodeMappings[id] = defaultSeedNodes[id]; + } + } + const idHostPort = rawSeedNode.split(/[@:]/); + if (idHostPort.length != 3) { + throw new commander.InvalidOptionArgumentError(`${rawSeedNode} is not of format 'nodeId@host:port'`) + } + if (!nodesUtils.isNodeId(idHostPort[0])) { + throw new commander.InvalidOptionArgumentError(`${idHostPort[0]} is not a valid node ID`); + } + if (!networkUtils.isValidHostname(idHostPort[1])) { + throw new commander.InvalidOptionArgumentError(`${idHostPort[1]} is not a valid hostname`); + } + const port = parseNumber(idHostPort[2]); + const seedNodeId = idHostPort[0] as NodeId; + const seedNodeAddress: NodeAddress = { + host: idHostPort[1] as Host | Hostname, + port: port as Port, + }; + seedNodeMappings[seedNodeId] = seedNodeAddress; + } + return seedNodeMappings; +} + +/** + * Acquires the default seed nodes from src/config.ts. + */ +function getDefaultSeedNodes(): NodeMapping { + const seedNodes: NodeMapping = {}; + let source; + switch (config.environment) { + case 'testnet': + source = config.seedNodesTest; + break; + case 'mainnet': + source = config.seedNodesMain; + break; + default: + throw new errors.ErrorInvalidConfigEnvironment(); + } + for (const id in source) { + const seedNodeId = id as NodeId; + const seedNodeAddress: NodeAddress = { + host: source[seedNodeId].host as Host | Hostname, + port: source[seedNodeId].port as Port, + } + seedNodes[seedNodeId] = seedNodeAddress; + } + return seedNodes; +} + const clientHost = new commander.Option( '-ch, --client-host
', 'Client Host Address', @@ -45,4 +121,12 @@ const ingressPort = new commander.Option( .env('PK_INGRESS_PORT') .default(config.defaults.clientPort); -export { clientHost, clientPort, ingressHost, ingressPort, hostParser }; +const seedNodes = new commander.Option( + '-sn, --seed-nodes [nodeId1@host:port;nodeId2@host:port;...]', + 'Seed node address mappings', +) + .argParser(parseSeedNodes) + .env('PK_SEED_NODES') + .default(getDefaultSeedNodes()); + +export { clientHost, clientPort, ingressHost, ingressPort, seedNodes, hostParser }; diff --git a/src/config.ts b/src/config.ts index 2298b620b0..ce54f82a65 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,6 +15,13 @@ const config = { clientHost: 'localhost', clientPort: 0, }, + environment: 'testnet', + seedNodesTest: { + 'SEEDNODE1' : { host: 'testnet.polykey.io', port: 1314 }, + }, + seedNodesMain: { + 'SEEDNODE1' : { host: 'testnet.polykey.io', port: 1314 }, + }, }; export default config; diff --git a/src/errors.ts b/src/errors.ts index d795bd1bb2..2e4ba01b19 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -36,6 +36,8 @@ class ErrorStateVersionMismatch extends ErrorPolykey {} class ErrorInvalidId extends ErrorPolykey {} +class ErrorInvalidConfigEnvironment extends ErrorPolykey {} + export { ErrorPolykey, ErrorPolykeyAgentNotRunning, @@ -45,6 +47,7 @@ export { ErrorUndefinedBehaviour, ErrorStateVersionMismatch, ErrorInvalidId, + ErrorInvalidConfigEnvironment, }; /** diff --git a/src/network/utils.ts b/src/network/utils.ts index 744dc3bad8..bfc197bcf5 100644 --- a/src/network/utils.ts +++ b/src/network/utils.ts @@ -63,6 +63,13 @@ function parseAddress(address: string): [Host, Port] { return isIPv4 || isIPv6; } +/** + * Validates that a provided hostname is valid, as per RFC 1123. + */ +function isValidHostname(hostname: string): boolean { + return (hostname.match(/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/)) ? true : false; +} + /** * Resolves a provided hostname to its respective IP address (type Host). */ @@ -348,6 +355,7 @@ export { buildAddress, parseAddress, isValidHost, + isValidHostname, resolveHost, resolvesZeroIP, serializeNetworkMessage, diff --git a/src/nodes/NodeManager.ts b/src/nodes/NodeManager.ts index 5ac6e4d0a2..8ae22b01c7 100644 --- a/src/nodes/NodeManager.ts +++ b/src/nodes/NodeManager.ts @@ -6,6 +6,7 @@ import type { ClaimIdString } from '../claims/types'; import type { NodeId, NodeAddress, + NodeMapping, NodeData, NodeBucket, NodeConnectionMap, @@ -52,7 +53,7 @@ class NodeManager { // Active connections to other nodes protected connections: NodeConnectionMap = new Map(); // Node ID -> node address mappings for the seed nodes - protected seedNodes: NodeBucket = {}; + protected seedNodes: NodeMapping = {}; static async createNodeManager({ db, @@ -128,7 +129,7 @@ class NodeManager { seedNodes = {}, fresh = false, }: { - seedNodes?: NodeBucket; + seedNodes?: NodeMapping; fresh?: boolean; } = {}) { this.logger.info('Starting Node Manager'); @@ -139,7 +140,7 @@ class NodeManager { // Add the seed nodes to the NodeGraph and establish connections to them for (const id in seedNodes) { const seedNodeId = id as NodeId; - await this.nodeGraph.setNode(seedNodeId, seedNodes[seedNodeId].address); + await this.nodeGraph.setNode(seedNodeId, seedNodes[seedNodeId]); await this.getConnectionToNode(seedNodeId); } this.logger.info('Started Node Manager'); diff --git a/src/nodes/types.ts b/src/nodes/types.ts index e5815aa647..0e55a7af1c 100644 --- a/src/nodes/types.ts +++ b/src/nodes/types.ts @@ -13,6 +13,10 @@ type NodeAddress = { port: Port; }; +type NodeMapping = { + [key: string]: NodeAddress; +}; + type NodeData = { id: NodeId; address: NodeAddress; @@ -83,6 +87,7 @@ type NodeGraphOp = export type { NodeId, NodeAddress, + NodeMapping, NodeData, NodeClaim, NodeInfo, diff --git a/tests/nodes/NodeManager.test.ts b/tests/nodes/NodeManager.test.ts index 7a08e2dc04..bb8d7f7003 100644 --- a/tests/nodes/NodeManager.test.ts +++ b/tests/nodes/NodeManager.test.ts @@ -147,7 +147,7 @@ describe('NodeManager', () => { await target.start({}); targetNodeId = target.keys.getNodeId(); targetNodeAddress = { - ip: target.revProxy.getIngressHost(), + host: target.revProxy.getIngressHost(), port: target.revProxy.getIngressPort(), }; await nodeManager.setNode(targetNodeId, targetNodeAddress); @@ -213,7 +213,7 @@ describe('NodeManager', () => { async () => { // Add the dummy node await nodeManager.setNode(dummyNode, { - ip: '125.0.0.1' as Host, + host: '125.0.0.1' as Host, port: 55555 as Port, }); // @ts-ignore accessing protected NodeConnectionMap @@ -249,7 +249,7 @@ describe('NodeManager', () => { }); const serverNodeId = server.nodes.getNodeId(); let serverNodeAddress: NodeAddress = { - ip: server.revProxy.getIngressHost(), + host: server.revProxy.getIngressHost(), port: server.revProxy.getIngressPort(), }; await nodeManager.setNode(serverNodeId, serverNodeAddress); @@ -264,7 +264,7 @@ describe('NodeManager', () => { await server.start({}); // Update the node address (only changes because we start and stop) serverNodeAddress = { - ip: server.revProxy.getIngressHost(), + host: server.revProxy.getIngressHost(), port: server.revProxy.getIngressPort(), }; await nodeManager.setNode(serverNodeId, serverNodeAddress); @@ -291,7 +291,7 @@ describe('NodeManager', () => { // Case 1: node already exists in the local node graph (no contact required) const nodeId = nodeId1; const nodeAddress: NodeAddress = { - ip: '127.0.0.1' as Host, + host: '127.0.0.1' as Host, port: 11111 as Port, }; await nodeManager.setNode(nodeId, nodeAddress); @@ -306,12 +306,12 @@ describe('NodeManager', () => { // Case 2: node can be found on the remote node const nodeId = nodeId1; const nodeAddress: NodeAddress = { - ip: '127.0.0.1' as Host, + host: '127.0.0.1' as Host, port: 11111 as Port, }; const server = await testUtils.setupRemoteKeynode({ logger: logger }); await nodeManager.setNode(server.nodes.getNodeId(), { - ip: server.revProxy.getIngressHost(), + host: server.revProxy.getIngressHost(), port: server.revProxy.getIngressPort(), } as NodeAddress); await server.nodes.setNode(nodeId, nodeAddress); @@ -329,14 +329,14 @@ describe('NodeManager', () => { const nodeId = nodeId1; const server = await testUtils.setupRemoteKeynode({ logger: logger }); await nodeManager.setNode(server.nodes.getNodeId(), { - ip: server.revProxy.getIngressHost(), + host: server.revProxy.getIngressHost(), port: server.revProxy.getIngressPort(), } as NodeAddress); // Add a dummy node to the server node graph database // Server will not be able to connect to this node (the only node in its // database), and will therefore not be able to locate the node. await server.nodes.setNode(dummyNode, { - ip: '127.0.0.2' as Host, + host: '127.0.0.2' as Host, port: 22222 as Port, } as NodeAddress); // So unfindableNode cannot be found @@ -351,7 +351,7 @@ describe('NodeManager', () => { test('knows node (true and false case)', async () => { // Known node const nodeAddress1: NodeAddress = { - ip: '127.0.0.1' as Host, + host: '127.0.0.1' as Host, port: 11111 as Port, }; await nodeManager.setNode(nodeId1, nodeAddress1); @@ -386,7 +386,7 @@ describe('NodeManager', () => { }); xNodeId = x.nodes.getNodeId(); xNodeAddress = { - ip: x.revProxy.getIngressHost(), + host: x.revProxy.getIngressHost(), port: x.revProxy.getIngressPort(), }; xPublicKey = x.keys.getRootKeyPairPem().publicKey; @@ -396,7 +396,7 @@ describe('NodeManager', () => { }); yNodeId = y.nodes.getNodeId(); yNodeAddress = { - ip: y.revProxy.getIngressHost(), + host: y.revProxy.getIngressHost(), port: y.revProxy.getIngressPort(), }; yPublicKey = y.keys.getRootKeyPairPem().publicKey;