samoke that connects to the newly created iniversa network
This commit is contained in:
parent
391754bcc4
commit
ab2cc9c1bf
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KubernetesApiProvider">{}</component>
|
||||
<component name="ProjectRootManager" version="2">
|
||||
|
||||
@ -14,14 +14,15 @@ Generate a Universa key smoke test:
|
||||
npm start -- --generate-key
|
||||
```
|
||||
|
||||
Important. ___We miss an actual topology file, so this one is not working yet__.
|
||||
|
||||
Test a Universa network connection with `src/indigo_demo_2025.json`:
|
||||
Test the Universa network with the topology in `src/universa.json`:
|
||||
|
||||
```sh
|
||||
npm start -- --test-network
|
||||
npm run test:network
|
||||
```
|
||||
|
||||
The test verifies each node's signed topology, opens an authenticated session,
|
||||
and requires a Universa `sping`/`spong` response from a network quorum.
|
||||
|
||||
## Check
|
||||
|
||||
```sh
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"test:network": "bash scripts/test-network.sh",
|
||||
"check": "node --check src/index.js"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
219
scripts/test-network.js
Normal file
219
scripts/test-network.js
Normal file
@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { basename, dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const {
|
||||
Boss,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
SymmetricKey,
|
||||
decode64,
|
||||
encode64,
|
||||
randomBytes
|
||||
} = require('unicrypto');
|
||||
|
||||
const projectDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const topologyFile = resolve(projectDirectory, 'src/universa.json');
|
||||
const cryptoDist = resolve(projectDirectory, 'node_modules/unicrypto/dist');
|
||||
const requestTimeoutMs = 5000;
|
||||
const clientVersion = 3;
|
||||
|
||||
function describeError(error) {
|
||||
if (error instanceof Error) return error.message;
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch {
|
||||
return String(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function withWasmLoader(callback) {
|
||||
const savedFetch = globalThis.fetch;
|
||||
const savedRead = globalThis.read;
|
||||
const savedReadBinary = globalThis.readBinary;
|
||||
|
||||
globalThis.fetch = undefined;
|
||||
globalThis.read = (fileName, binary) => {
|
||||
const data = readFileSync(resolve(cryptoDist, basename(fileName)));
|
||||
return binary ? data : data.toString('binary');
|
||||
};
|
||||
globalThis.readBinary = (fileName) => (
|
||||
readFileSync(resolve(cryptoDist, basename(fileName)))
|
||||
);
|
||||
|
||||
try {
|
||||
return await callback(savedFetch);
|
||||
} finally {
|
||||
globalThis.fetch = savedFetch;
|
||||
if (savedRead === undefined) delete globalThis.read;
|
||||
else globalThis.read = savedRead;
|
||||
if (savedReadBinary === undefined) delete globalThis.readBinary;
|
||||
else globalThis.readBinary = savedReadBinary;
|
||||
}
|
||||
}
|
||||
|
||||
function loadTopology() {
|
||||
const topology = JSON.parse(readFileSync(topologyFile, 'utf8'));
|
||||
const nodes = Array.isArray(topology) ? topology : topology.list;
|
||||
|
||||
if (!Array.isArray(nodes) || nodes.length === 0) {
|
||||
throw new Error(`${topologyFile} must contain a non-empty node list`);
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
if (!node.number || !node.key || !node.direct_urls?.length) {
|
||||
throw new Error(`Invalid node entry in ${topologyFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
async function request(fetchImpl, method, url, params) {
|
||||
const options = {
|
||||
method,
|
||||
signal: AbortSignal.timeout(requestTimeoutMs)
|
||||
};
|
||||
|
||||
if (method === 'POST') {
|
||||
options.headers = { 'Content-Type': 'application/json' };
|
||||
options.body = JSON.stringify({ requestData64: encode64(Boss.dump(params)) });
|
||||
}
|
||||
|
||||
const response = await fetchImpl(url, options);
|
||||
if (!response.ok) throw new Error(`${method} ${url} returned HTTP ${response.status}`);
|
||||
|
||||
const answer = Boss.load(new Uint8Array(await response.arrayBuffer()));
|
||||
if (answer?.result !== 'ok') throw answer || new Error(`Invalid response from ${url}`);
|
||||
return answer.response;
|
||||
}
|
||||
|
||||
function packedNodeId(node) {
|
||||
const key = typeof node.key === 'string' ? node.key : encode64(node.key);
|
||||
return `${node.number}:${key}`;
|
||||
}
|
||||
|
||||
async function verifySignedTopology(fetchImpl, node, publicKey, expectedNodeIds) {
|
||||
const response = await request(fetchImpl, 'GET', `${node.direct_urls[0]}/topology`);
|
||||
const packed = response?.packed_data;
|
||||
const signature = response?.signature;
|
||||
|
||||
if (!packed || !signature || !await publicKey.verifyExtended(signature, packed)) {
|
||||
throw new Error('invalid topology signature');
|
||||
}
|
||||
|
||||
const advertised = Boss.load(packed)?.nodes;
|
||||
if (!Array.isArray(advertised)) throw new Error('signed topology has no node list');
|
||||
|
||||
const advertisedNodeIds = new Set(advertised.map(packedNodeId));
|
||||
for (const expectedNodeId of expectedNodeIds) {
|
||||
if (!advertisedNodeIds.has(expectedNodeId)) {
|
||||
throw new Error('signed topology does not contain every configured node');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openSession(fetchImpl, node, publicKey, privateKey) {
|
||||
const baseUrl = node.direct_urls[0];
|
||||
const signatureOptions = { pssHash: 'sha512' };
|
||||
const clientNonce = randomBytes(47);
|
||||
const clientKey = await privateKey.publicKey.pack();
|
||||
const connection = await request(fetchImpl, 'POST', `${baseUrl}/connect`, {
|
||||
client_key: clientKey,
|
||||
client_version: clientVersion
|
||||
});
|
||||
const serverVersion = connection.server_version || 1;
|
||||
const version = Math.min(serverVersion, clientVersion);
|
||||
const sessionId = connection.session_id;
|
||||
const authenticationData = Boss.dump({
|
||||
client_nonce: clientNonce,
|
||||
server_nonce: connection.server_nonce,
|
||||
client_version: clientVersion,
|
||||
server_version: serverVersion
|
||||
});
|
||||
const token = await request(fetchImpl, 'POST', `${baseUrl}/get_token`, {
|
||||
data: authenticationData,
|
||||
signature: await privateKey.sign(authenticationData, signatureOptions),
|
||||
session_id: sessionId
|
||||
});
|
||||
|
||||
if (!await publicKey.verify(token.data, token.signature, signatureOptions)) {
|
||||
throw new Error('invalid authentication signature');
|
||||
}
|
||||
|
||||
const tokenData = Boss.load(token.data);
|
||||
if (encode64(tokenData.client_nonce) !== encode64(clientNonce)) {
|
||||
throw new Error('authentication nonce mismatch');
|
||||
}
|
||||
|
||||
const decryptedToken = Boss.load(await privateKey.decrypt(tokenData.encrypted_token));
|
||||
const sessionKey = new SymmetricKey({ keyBytes: decryptedToken.sk });
|
||||
|
||||
return async (name, params = {}) => {
|
||||
const data = Boss.dump({ command: name, params });
|
||||
const encrypted = version >= 2
|
||||
? await sessionKey.etaEncrypt(data)
|
||||
: await sessionKey.encrypt(data);
|
||||
const response = await request(fetchImpl, 'POST', `${baseUrl}/command`, {
|
||||
command: 'command',
|
||||
params: encrypted,
|
||||
session_id: sessionId
|
||||
});
|
||||
const decrypted = version >= 2
|
||||
? await sessionKey.etaDecrypt(response.result)
|
||||
: await sessionKey.decrypt(response.result);
|
||||
const result = Boss.load(decrypted);
|
||||
if (result.error) throw result.error;
|
||||
return result.result;
|
||||
};
|
||||
}
|
||||
|
||||
async function testNode(fetchImpl, node, privateKey, expectedNodeIds) {
|
||||
const keyBytes = typeof node.key === 'string' ? decode64(node.key) : node.key;
|
||||
const publicKey = await PublicKey.unpack(keyBytes);
|
||||
await verifySignedTopology(fetchImpl, node, publicKey, expectedNodeIds);
|
||||
const command = await openSession(fetchImpl, node, publicKey, privateKey);
|
||||
const hello = await command('hello');
|
||||
if (hello?.status !== 'OK') throw new Error(`hello returned ${hello?.status || 'no status'}`);
|
||||
const ping = await command('sping');
|
||||
if (ping?.sping !== 'spong') throw new Error('sping did not return spong');
|
||||
}
|
||||
|
||||
async function main(fetchImpl) {
|
||||
const nodes = loadTopology();
|
||||
const expectedNodeIds = new Set(nodes.map(packedNodeId));
|
||||
const quorum = Math.max(1, Math.ceil(nodes.length * 0.4));
|
||||
const privateKey = await PrivateKey.generate({ strength: 2048 });
|
||||
|
||||
console.log(`Testing ${nodes.length} Universa nodes from ${topologyFile}`);
|
||||
const results = await Promise.all(nodes.map(async (node) => {
|
||||
try {
|
||||
await testNode(fetchImpl, node, privateKey, expectedNodeIds);
|
||||
console.log(`PASS ${node.name}: signed topology, authenticated hello, sping=spong`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`FAIL ${node.name}: ${describeError(error)}`);
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
|
||||
const passed = results.filter(Boolean).length;
|
||||
if (passed < quorum) {
|
||||
throw new Error(`Universa network quorum failed: ${passed}/${nodes.length} nodes passed; ${quorum} required`);
|
||||
}
|
||||
console.log(`Universa network is functioning: ${passed}/${nodes.length} nodes passed (${quorum} required)`);
|
||||
}
|
||||
|
||||
try {
|
||||
await withWasmLoader(async (fetchImpl) => {
|
||||
if (!fetchImpl) throw new Error('This test requires the Node.js fetch API');
|
||||
await main(fetchImpl);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(describeError(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
5
scripts/test-network.sh
Executable file
5
scripts/test-network.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
exec node scripts/test-network.js
|
||||
105
src/index.js
105
src/index.js
@ -2,14 +2,17 @@
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { createConnection } from 'node:net';
|
||||
import { basename, dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Command } from 'commander';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
const topologyFile = resolve(scriptDirectory, 'indigo_demo_2025.json');
|
||||
const topologyFile = resolve(scriptDirectory, 'universa.json');
|
||||
const universaCoreDist = resolve(scriptDirectory, '../node_modules/universa-core/dist');
|
||||
const networkTimeoutMs = 20000;
|
||||
const endpointProbeTimeoutMs = 3000;
|
||||
|
||||
function withTimeout(promise, timeoutMs, message) {
|
||||
let timeoutId;
|
||||
@ -50,14 +53,42 @@ async function withUniversaWasmLoader(callback) {
|
||||
}
|
||||
|
||||
function loadUniversaCore() {
|
||||
const window = globalThis.window;
|
||||
globalThis.window = globalThis;
|
||||
const windowDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
||||
const navigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
|
||||
const localStorageDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'localStorage');
|
||||
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: globalThis
|
||||
});
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
configurable: true,
|
||||
value: {
|
||||
hardwareConcurrency: 1,
|
||||
languages: ['en-US']
|
||||
}
|
||||
});
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return require('universa-core');
|
||||
} finally {
|
||||
if (window === undefined) delete globalThis.window;
|
||||
else globalThis.window = window;
|
||||
if (windowDescriptor) Object.defineProperty(globalThis, 'window', windowDescriptor);
|
||||
else delete globalThis.window;
|
||||
|
||||
if (navigatorDescriptor) Object.defineProperty(globalThis, 'navigator', navigatorDescriptor);
|
||||
else delete globalThis.navigator;
|
||||
|
||||
if (localStorageDescriptor) Object.defineProperty(globalThis, 'localStorage', localStorageDescriptor);
|
||||
else delete globalThis.localStorage;
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,18 +101,72 @@ function generatePrivateKey() {
|
||||
|
||||
async function loadTopology(Topology) {
|
||||
return withUniversaWasmLoader(async () => {
|
||||
const packedTopology = JSON.parse(readFileSync(topologyFile, 'utf8'));
|
||||
const packedTopology = readTopology();
|
||||
return Topology.load(packedTopology);
|
||||
});
|
||||
}
|
||||
|
||||
function readTopology() {
|
||||
const parsedTopology = JSON.parse(readFileSync(topologyFile, 'utf8'));
|
||||
const packedTopology = Array.isArray(parsedTopology)
|
||||
? { list: parsedTopology, updated: Math.floor(Date.now() / 1000) }
|
||||
: parsedTopology;
|
||||
|
||||
if (!Array.isArray(packedTopology.list)) {
|
||||
throw new Error(`Topology file must contain a node array or an object with a list array: ${topologyFile}`);
|
||||
}
|
||||
|
||||
return packedTopology;
|
||||
}
|
||||
|
||||
async function probeEndpoint(url) {
|
||||
return new Promise((resolveProbe) => {
|
||||
const endpoint = new URL(url);
|
||||
const port = endpoint.port || (endpoint.protocol === 'https:' ? 443 : 80);
|
||||
const socket = createConnection({
|
||||
host: endpoint.hostname,
|
||||
port: Number(port),
|
||||
timeout: endpointProbeTimeoutMs
|
||||
});
|
||||
|
||||
function finish(ok, error = '') {
|
||||
socket.destroy();
|
||||
resolveProbe({ url, ok, error });
|
||||
}
|
||||
|
||||
socket.once('connect', () => finish(true));
|
||||
socket.once('timeout', () => finish(false, 'TCP connection timed out'));
|
||||
socket.once('error', (error) => finish(false, error.message));
|
||||
});
|
||||
}
|
||||
|
||||
async function assertReachableTopology(directConnection) {
|
||||
const topology = readTopology();
|
||||
const urls = topology.list.flatMap((node) => (
|
||||
directConnection ? node.direct_urls : node.domain_urls
|
||||
) || []);
|
||||
|
||||
if (urls.length === 0) {
|
||||
throw new Error(`Topology file does not contain ${directConnection ? 'direct_urls' : 'domain_urls'} entries: ${topologyFile}`);
|
||||
}
|
||||
|
||||
const results = await Promise.all(urls.map(probeEndpoint));
|
||||
if (results.some((result) => result.ok)) return;
|
||||
|
||||
const details = results
|
||||
.map(({ url, error }) => ` ${url}/topology: ${error}`)
|
||||
.join('\n');
|
||||
|
||||
throw new Error(`No reachable topology endpoints in ${topologyFile}:\n${details}`);
|
||||
}
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('u-js-sample')
|
||||
.description('Example Node.js console script with universa-core functions.')
|
||||
.option('--generate-key', 'generate a Universa private key and print its short address')
|
||||
.option('--test-network', 'generate a key and connect to the Universa network from indigo_demo_2025.json')
|
||||
.option('--test-network', `generate a key and connect to the Universa network from ${basename(topologyFile)}`)
|
||||
.action(async (options) => {
|
||||
if (options.generateKey || options.testNetwork) {
|
||||
const privateKey = await generatePrivateKey();
|
||||
@ -90,10 +175,12 @@ program
|
||||
if (options.testNetwork) {
|
||||
const { Network, Topology } = loadUniversaCore();
|
||||
const topology = await loadTopology(Topology);
|
||||
const network = new Network(privateKey, { topology, directConnection: true });
|
||||
const directConnection = true;
|
||||
const network = new Network(privateKey, { topology, directConnection });
|
||||
|
||||
console.log(`Connecting with topology: ${topologyFile}`);
|
||||
await withTimeout(network.connect(), 20000, 'Network connection timed out');
|
||||
await assertReachableTopology(directConnection);
|
||||
await withTimeout(network.connect(), networkTimeoutMs, 'Network connection timed out');
|
||||
console.log(`Connected to ${network.size()} nodes`);
|
||||
|
||||
const response = await withTimeout(network.command('sping'), 10000, 'Network sping command timed out');
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"number": 1,
|
||||
"domain_urls": [
|
||||
"https://31.41.155.11"
|
||||
],
|
||||
"name": "node-01",
|
||||
"direct_urls": [
|
||||
"http://31.41.155.11:8080"
|
||||
],
|
||||
"key": "HggcAQABxAAB5IfycVepakIjOt3c5eyLFswYH8dokrmHTyRIgaCSz+EY+mm8hM2Cn1PJjyiJj4K1A8CPN9p9mj6z/yyGSH6pvW3HMGs6Az7KvhGbZJlsFP0tNy6A3YQEWP/XToSqtT3f0Ji/r2nYw2zEgnGvOBkkcyN/EEslnxTj1R3fZlaSHh9Uem6NFk62ZxSBj8hJkvfHbTB/XZ2z8wtpwcAJClylITbbCSg9RdXoD8ZwCJMaCWasxfdo5+sMYTcABYFErbw8ndeQ5aL3N2+XiFM8WO5wQnd3SNTYUQGucBkXO5zqmZjnreVOWIsQtKzjwH4WIi57kz+87V72TbDrX0jYMYsOSw=="
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"domain_urls": [
|
||||
"https://37.9.5.102"
|
||||
],
|
||||
"name": "node-02",
|
||||
"direct_urls": [
|
||||
"http://37.9.5.102:8080"
|
||||
],
|
||||
"key": "HggcAQABxAAB3GST2bRm2SrwypfKZRplqDwLUvDF7DoRgVMZfzOHzYodHfIpNrRKZfJdr63UdpK6359G1TAAHm52UaLCaCqBZNuFDqE9s9fEe+2UoSzoMKGG0NtgFD2HdDhuNWw4LnqZiZ5lPlZuUKtxVCzhlI0SNweBKMn0oopjdOYfx7hkPHVB2TyADXPrcnxFHEpc6USjJgP3aHPgum4CxYD09KTF59t5W+B+rDpTd/ro3ENBSFi9xlt4qMrLx5KzIdm8IqcsEm+mTYS5mx0F4e6wlCiXElwGe1NdXNwb/A8S2JBSUKP2CfSKdJtB0D7GHFnMuF3+u29/pykBOvlUoUa4S5QxJQ=="
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"domain_urls": [
|
||||
"https://37.9.4.133"
|
||||
],
|
||||
"name": "node-03",
|
||||
"direct_urls": [
|
||||
"http://37.9.4.133:8080"
|
||||
],
|
||||
"key": "HggcAQABxAABtTajk8wkzstB+mmj09fkP/a9IqX4tVuuXnd/fO6a3TAjNbulqtkvVyfhu2DeUdUFiYP9kc+YQmHsIe5T/xZgdecWAbHKT0Ok530nLju8dog+75j6aCqD+winCCb+s7dzAYNIsKS0QgBPPWyBUyDL+di17s2Z5U2mibV+sXQYfWU4Gy02WNuavC8QZ0gA7K3Ah6ohR/Yn37WpiWvS+kQOnZF/q/8LYRQgxdDrztzTgpouggwk/1VJ181fYcXq/54n+L5yV2M0GZy7B942ce07MV6EGmQJu4eoOQi8DN3Ee30WOuIIiEyiQLMJjva0uxvkXmlftsr8rGiLXB+kiMd2fQ=="
|
||||
},
|
||||
{
|
||||
"number": 4,
|
||||
"domain_urls": [
|
||||
"https://37.9.4.137"
|
||||
],
|
||||
"name": "node-04",
|
||||
"direct_urls": [
|
||||
"http://37.9.4.137:8080"
|
||||
],
|
||||
"key": "HggcAQABxAABkGzNgQnnNh/aNR3zQujD/6mgcqqfZ3tl14DDLhXLunUhc8hkn3y+ViukIj1qbkPi6Ps5irDZ2TzVfepoVWqX+XkI8wUbXKpDN/zWYx9kW4/soA4C7KS6fS/NFsFflUyzDNQeQXmiQkPTNnn43sdSqYb6Zr3I7Wa6mEwIP7MakKo6SbJQ/gTdXPlKWnU1nGJISMowCfhawYQLZgI5URsgS3VV7Owb3Yo4IZ0YNDTceboY4ljYuc/R2LqJcGjcWfSweaHV2T64RY6yQt1W0h2KweK99cflJdh/QSCqBhRdSYYfgKDlmp2ZTvrrfj0bbz2mPBh5JG5RQe+O9MzT0Is4ew=="
|
||||
}
|
||||
],
|
||||
"updated": 1742911675
|
||||
}
|
||||
46
src/universa.json
Normal file
46
src/universa.json
Normal file
@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"number": 1,
|
||||
"domain_urls": [
|
||||
"https://38.60.255.83"
|
||||
],
|
||||
"name": "node-01",
|
||||
"direct_urls": [
|
||||
"http://38.60.255.83:8080"
|
||||
],
|
||||
"key": "HggcAQABxAAB5IfycVepakIjOt3c5eyLFswYH8dokrmHTyRIgaCSz+EY+mm8hM2Cn1PJjyiJj4K1A8CPN9p9mj6z/yyGSH6pvW3HMGs6Az7KvhGbZJlsFP0tNy6A3YQEWP/XToSqtT3f0Ji/r2nYw2zEgnGvOBkkcyN/EEslnxTj1R3fZlaSHh9Uem6NFk62ZxSBj8hJkvfHbTB/XZ2z8wtpwcAJClylITbbCSg9RdXoD8ZwCJMaCWasxfdo5+sMYTcABYFErbw8ndeQ5aL3N2+XiFM8WO5wQnd3SNTYUQGucBkXO5zqmZjnreVOWIsQtKzjwH4WIi57kz+87V72TbDrX0jYMYsOSw=="
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"domain_urls": [
|
||||
"https://38.60.238.117"
|
||||
],
|
||||
"name": "node-02",
|
||||
"direct_urls": [
|
||||
"http://38.60.238.117:8080"
|
||||
],
|
||||
"key": "HggcAQABxAAB3GST2bRm2SrwypfKZRplqDwLUvDF7DoRgVMZfzOHzYodHfIpNrRKZfJdr63UdpK6359G1TAAHm52UaLCaCqBZNuFDqE9s9fEe+2UoSzoMKGG0NtgFD2HdDhuNWw4LnqZiZ5lPlZuUKtxVCzhlI0SNweBKMn0oopjdOYfx7hkPHVB2TyADXPrcnxFHEpc6USjJgP3aHPgum4CxYD09KTF59t5W+B+rDpTd/ro3ENBSFi9xlt4qMrLx5KzIdm8IqcsEm+mTYS5mx0F4e6wlCiXElwGe1NdXNwb/A8S2JBSUKP2CfSKdJtB0D7GHFnMuF3+u29/pykBOvlUoUa4S5QxJQ=="
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"domain_urls": [
|
||||
"https://130.94.0.186"
|
||||
],
|
||||
"name": "node-03",
|
||||
"direct_urls": [
|
||||
"http://130.94.0.186:8080"
|
||||
],
|
||||
"key": "HggcAQABxAABtTajk8wkzstB+mmj09fkP/a9IqX4tVuuXnd/fO6a3TAjNbulqtkvVyfhu2DeUdUFiYP9kc+YQmHsIe5T/xZgdecWAbHKT0Ok530nLju8dog+75j6aCqD+winCCb+s7dzAYNIsKS0QgBPPWyBUyDL+di17s2Z5U2mibV+sXQYfWU4Gy02WNuavC8QZ0gA7K3Ah6ohR/Yn37WpiWvS+kQOnZF/q/8LYRQgxdDrztzTgpouggwk/1VJ181fYcXq/54n+L5yV2M0GZy7B942ce07MV6EGmQJu4eoOQi8DN3Ee30WOuIIiEyiQLMJjva0uxvkXmlftsr8rGiLXB+kiMd2fQ=="
|
||||
},
|
||||
{
|
||||
"number": 4,
|
||||
"domain_urls": [
|
||||
"https://38.60.208.39"
|
||||
],
|
||||
"name": "node-04",
|
||||
"direct_urls": [
|
||||
"http://38.60.208.39:8080"
|
||||
],
|
||||
"key": "HggcAQABxAABkGzNgQnnNh/aNR3zQujD/6mgcqqfZ3tl14DDLhXLunUhc8hkn3y+ViukIj1qbkPi6Ps5irDZ2TzVfepoVWqX+XkI8wUbXKpDN/zWYx9kW4/soA4C7KS6fS/NFsFflUyzDNQeQXmiQkPTNnn43sdSqYb6Zr3I7Wa6mEwIP7MakKo6SbJQ/gTdXPlKWnU1nGJISMowCfhawYQLZgI5URsgS3VV7Owb3Yo4IZ0YNDTceboY4ljYuc/R2LqJcGjcWfSweaHV2T64RY6yQt1W0h2KweK99cflJdh/QSCqBhRdSYYfgKDlmp2ZTvrrfj0bbz2mPBh5JG5RQe+O9MzT0Is4ew=="
|
||||
}
|
||||
]
|
||||
Loading…
x
Reference in New Issue
Block a user