Skip to content

Commit

Permalink
Refactoring seed nodes to NodeMapping, added parsers for seed nodes s…
Browse files Browse the repository at this point in the history
…ources
  • Loading branch information
joshuakarp committed Nov 19, 2021
1 parent ba219af commit d44346f
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 16 deletions.
86 changes: 85 additions & 1 deletion src/bin/options.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 '<seed-nodes>' 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 == '<seed-nodes>') {
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 <address>',
'Client Host Address',
Expand Down Expand Up @@ -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 };
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
3 changes: 3 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class ErrorStateVersionMismatch extends ErrorPolykey {}

class ErrorInvalidId extends ErrorPolykey {}

class ErrorInvalidConfigEnvironment extends ErrorPolykey {}

export {
ErrorPolykey,
ErrorPolykeyAgentNotRunning,
Expand All @@ -45,6 +47,7 @@ export {
ErrorUndefinedBehaviour,
ErrorStateVersionMismatch,
ErrorInvalidId,
ErrorInvalidConfigEnvironment,
};

/**
Expand Down
8 changes: 8 additions & 0 deletions src/network/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/
Expand Down Expand Up @@ -348,6 +355,7 @@ export {
buildAddress,
parseAddress,
isValidHost,
isValidHostname,
resolveHost,
resolvesZeroIP,
serializeNetworkMessage,
Expand Down
7 changes: 4 additions & 3 deletions src/nodes/NodeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ClaimIdString } from '../claims/types';
import type {
NodeId,
NodeAddress,
NodeMapping,
NodeData,
NodeBucket,
NodeConnectionMap,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -128,7 +129,7 @@ class NodeManager {
seedNodes = {},
fresh = false,
}: {
seedNodes?: NodeBucket;
seedNodes?: NodeMapping;
fresh?: boolean;
} = {}) {
this.logger.info('Starting Node Manager');
Expand All @@ -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');
Expand Down
5 changes: 5 additions & 0 deletions src/nodes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ type NodeAddress = {
port: Port;
};

type NodeMapping = {
[key: string]: NodeAddress;
};

type NodeData = {
id: NodeId;
address: NodeAddress;
Expand Down Expand Up @@ -83,6 +87,7 @@ type NodeGraphOp =
export type {
NodeId,
NodeAddress,
NodeMapping,
NodeData,
NodeClaim,
NodeInfo,
Expand Down
24 changes: 12 additions & 12 deletions tests/nodes/NodeManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down

0 comments on commit d44346f

Please sign in to comment.