2020-05-14 15:20:39 +03:00
|
|
|
/*
|
2020-05-14 17:30:17 +03:00
|
|
|
* Copyright 2020 Fluence Labs Limited
|
2020-05-14 15:20:39 +03:00
|
|
|
*
|
2020-05-14 17:30:17 +03:00
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
2020-05-14 15:20:39 +03:00
|
|
|
*
|
2020-05-14 17:30:17 +03:00
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
2020-05-14 15:20:39 +03:00
|
|
|
*
|
2020-05-14 17:30:17 +03:00
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
2020-05-14 15:20:39 +03:00
|
|
|
*/
|
2023-02-13 17:41:35 +03:00
|
|
|
import { PeerIdB58 } from '@fluencelabs/interfaces';
|
2022-05-18 15:33:24 +03:00
|
|
|
import { pipe } from 'it-pipe';
|
2023-10-02 19:39:13 +07:00
|
|
|
import { decode, encode } from 'it-length-prefixed';
|
2023-08-25 00:15:49 +07:00
|
|
|
import type { PeerId } from '@libp2p/interface/peer-id';
|
2023-02-13 17:41:35 +03:00
|
|
|
import { createLibp2p, Libp2p } from 'libp2p';
|
|
|
|
|
|
|
|
import { noise } from '@chainsafe/libp2p-noise';
|
2023-09-14 10:22:59 +07:00
|
|
|
import { yamux } from '@chainsafe/libp2p-yamux';
|
2023-02-13 17:41:35 +03:00
|
|
|
import { webSockets } from '@libp2p/websockets';
|
|
|
|
import { all } from '@libp2p/websockets/filters';
|
2023-10-02 19:39:13 +07:00
|
|
|
import { multiaddr, type Multiaddr } from '@multiformats/multiaddr';
|
2023-02-13 17:41:35 +03:00
|
|
|
|
|
|
|
import map from 'it-map';
|
|
|
|
import { fromString } from 'uint8arrays/from-string';
|
|
|
|
import { toString } from 'uint8arrays/to-string';
|
|
|
|
|
2023-03-11 00:03:34 +04:00
|
|
|
import { logger } from '../util/logger.js';
|
2023-04-03 21:52:40 +04:00
|
|
|
import { Subject } from 'rxjs';
|
|
|
|
import { throwIfHasNoPeerId } from '../util/libp2pUtils.js';
|
|
|
|
import { IConnection } from './interfaces.js';
|
|
|
|
import { IParticle } from '../particle/interfaces.js';
|
2023-10-10 23:26:44 +07:00
|
|
|
import { buildParticleMessage, Particle, serializeToString, verifySignature } from '../particle/Particle.js';
|
2023-09-14 10:22:59 +07:00
|
|
|
import { identifyService } from 'libp2p/identify';
|
|
|
|
import { pingService } from 'libp2p/ping';
|
2023-10-02 19:39:13 +07:00
|
|
|
import { unmarshalPublicKey } from '@libp2p/crypto/keys';
|
|
|
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
|
|
import { Stream } from '@libp2p/interface/connection';
|
|
|
|
import { KeyPair } from '../keypair/index.js';
|
2023-03-11 00:03:34 +04:00
|
|
|
|
|
|
|
const log = logger('connection');
|
2020-05-14 15:20:39 +03:00
|
|
|
|
2021-08-24 17:37:03 +03:00
|
|
|
export const PROTOCOL_NAME = '/fluence/particle/2.0.0';
|
2020-05-14 15:20:39 +03:00
|
|
|
|
2021-03-25 21:33:27 +03:00
|
|
|
/**
|
2023-04-03 21:52:40 +04:00
|
|
|
* Options to configure fluence relay connection
|
2021-03-25 21:33:27 +03:00
|
|
|
*/
|
2023-04-03 21:52:40 +04:00
|
|
|
export interface RelayConnectionConfig {
|
2021-03-25 21:33:27 +03:00
|
|
|
/**
|
2021-10-20 22:20:43 +03:00
|
|
|
* Peer id of the Fluence Peer
|
2021-03-25 21:33:27 +03:00
|
|
|
*/
|
2021-10-20 22:20:43 +03:00
|
|
|
peerId: PeerId;
|
2021-03-25 21:33:27 +03:00
|
|
|
|
|
|
|
/**
|
2021-10-20 22:20:43 +03:00
|
|
|
* Multiaddress of the relay to make connection to
|
2021-03-25 21:33:27 +03:00
|
|
|
*/
|
2023-04-03 21:52:40 +04:00
|
|
|
relayAddress: Multiaddr;
|
2021-03-25 21:33:27 +03:00
|
|
|
|
|
|
|
/**
|
2021-10-20 22:20:43 +03:00
|
|
|
* The dialing timeout in milliseconds
|
2021-03-25 21:33:27 +03:00
|
|
|
*/
|
2021-10-20 22:20:43 +03:00
|
|
|
dialTimeoutMs?: number;
|
2023-04-03 21:52:40 +04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The maximum number of inbound streams for the libp2p node.
|
|
|
|
* Default: 1024
|
|
|
|
*/
|
|
|
|
maxInboundStreams: number;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The maximum number of outbound streams for the libp2p node.
|
|
|
|
* Default: 1024
|
|
|
|
*/
|
|
|
|
maxOutboundStreams: number;
|
2022-08-05 16:43:19 +03:00
|
|
|
}
|
2021-10-20 22:20:43 +03:00
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
/**
|
|
|
|
* Implementation for JS peers which connects to Fluence through relay node
|
|
|
|
*/
|
2023-09-14 10:22:59 +07:00
|
|
|
export class RelayConnection implements IConnection {
|
2023-04-03 21:52:40 +04:00
|
|
|
private relayAddress: Multiaddr;
|
|
|
|
private lib2p2Peer: Libp2p | null = null;
|
|
|
|
|
|
|
|
constructor(private config: RelayConnectionConfig) {
|
|
|
|
this.relayAddress = multiaddr(this.config.relayAddress);
|
|
|
|
throwIfHasNoPeerId(this.relayAddress);
|
|
|
|
}
|
|
|
|
|
|
|
|
getRelayPeerId(): string {
|
|
|
|
// since we check for peer id in constructor, we can safely use ! here
|
|
|
|
return this.relayAddress.getPeerId()!;
|
|
|
|
}
|
|
|
|
|
|
|
|
supportsRelay(): boolean {
|
|
|
|
return true;
|
2022-08-05 16:43:19 +03:00
|
|
|
}
|
2021-04-13 15:11:52 +03:00
|
|
|
|
2023-04-03 21:52:40 +04:00
|
|
|
particleSource = new Subject<IParticle>();
|
|
|
|
|
|
|
|
async start(): Promise<void> {
|
|
|
|
// check if already started
|
|
|
|
if (this.lib2p2Peer !== null) {
|
|
|
|
return;
|
|
|
|
}
|
2021-04-13 15:11:52 +03:00
|
|
|
|
2023-09-14 19:53:37 +07:00
|
|
|
this.lib2p2Peer = await createLibp2p({
|
2023-04-03 21:52:40 +04:00
|
|
|
peerId: this.config.peerId,
|
2023-02-13 17:41:35 +03:00
|
|
|
transports: [
|
|
|
|
webSockets({
|
|
|
|
filter: all,
|
|
|
|
}),
|
|
|
|
],
|
2023-09-14 10:22:59 +07:00
|
|
|
streamMuxers: [yamux()],
|
2023-02-13 17:41:35 +03:00
|
|
|
connectionEncryption: [noise()],
|
2023-04-03 21:52:40 +04:00
|
|
|
connectionManager: {
|
|
|
|
dialTimeout: this.config.dialTimeoutMs,
|
|
|
|
},
|
2023-08-25 00:15:49 +07:00
|
|
|
connectionGater: {
|
|
|
|
// By default, this function forbids connections to private peers. For example multiaddr with ip 127.0.0.1 isn't allowed
|
2023-10-12 21:01:41 +07:00
|
|
|
denyDialMultiaddr: () => Promise.resolve(false),
|
2023-09-14 10:22:59 +07:00
|
|
|
},
|
|
|
|
services: {
|
2023-09-14 19:53:37 +07:00
|
|
|
identify: identifyService(),
|
2023-10-12 21:01:41 +07:00
|
|
|
ping: pingService(),
|
|
|
|
},
|
2020-05-14 15:20:39 +03:00
|
|
|
});
|
|
|
|
|
2023-09-14 19:53:37 +07:00
|
|
|
const supportedProtocols = (await this.lib2p2Peer.peerStore.get(this.lib2p2Peer.peerId)).protocols;
|
|
|
|
await this.lib2p2Peer.peerStore.patch(this.lib2p2Peer.peerId, {
|
2023-10-12 21:01:41 +07:00
|
|
|
protocols: [...supportedProtocols, PROTOCOL_NAME],
|
2023-09-14 19:53:37 +07:00
|
|
|
});
|
2023-10-12 21:01:41 +07:00
|
|
|
|
2023-04-03 21:52:40 +04:00
|
|
|
await this.connect();
|
|
|
|
}
|
|
|
|
|
|
|
|
async stop(): Promise<void> {
|
|
|
|
// check if already stopped
|
|
|
|
if (this.lib2p2Peer === null) {
|
|
|
|
return;
|
2022-08-05 16:43:19 +03:00
|
|
|
}
|
2020-05-14 15:20:39 +03:00
|
|
|
|
2023-04-03 21:52:40 +04:00
|
|
|
await this.lib2p2Peer.unhandle(PROTOCOL_NAME);
|
|
|
|
await this.lib2p2Peer.stop();
|
2020-05-14 15:20:39 +03:00
|
|
|
}
|
|
|
|
|
2023-04-03 21:52:40 +04:00
|
|
|
async sendParticle(nextPeerIds: PeerIdB58[], particle: IParticle): Promise<void> {
|
|
|
|
if (this.lib2p2Peer === null) {
|
|
|
|
throw new Error('Relay connection is not started');
|
|
|
|
}
|
2020-05-14 15:20:39 +03:00
|
|
|
|
2023-04-03 21:52:40 +04:00
|
|
|
if (nextPeerIds.length !== 1 && nextPeerIds[0] !== this.getRelayPeerId()) {
|
2022-08-05 16:43:19 +03:00
|
|
|
throw new Error(
|
|
|
|
`Relay connection only accepts peer id of the connected relay. Got: ${JSON.stringify(
|
|
|
|
nextPeerIds,
|
|
|
|
)} instead.`,
|
|
|
|
);
|
|
|
|
}
|
2021-10-20 22:20:43 +03:00
|
|
|
|
2023-09-14 10:22:59 +07:00
|
|
|
log.trace('sending particle...');
|
2023-09-14 19:53:37 +07:00
|
|
|
// Reusing active connection here
|
2023-04-03 21:52:40 +04:00
|
|
|
const stream = await this.lib2p2Peer.dialProtocol(this.relayAddress, PROTOCOL_NAME);
|
2023-09-14 10:22:59 +07:00
|
|
|
log.trace('created stream with id ', stream.id);
|
2023-02-13 17:41:35 +03:00
|
|
|
const sink = stream.sink;
|
2020-05-14 15:20:39 +03:00
|
|
|
|
2023-10-12 21:01:41 +07:00
|
|
|
await pipe([fromString(serializeToString(particle))], encode(), sink);
|
2023-09-14 10:22:59 +07:00
|
|
|
log.trace('data written to sink');
|
2020-05-14 15:20:39 +03:00
|
|
|
}
|
2023-10-12 21:01:41 +07:00
|
|
|
|
2023-10-02 19:39:13 +07:00
|
|
|
private async processIncomingMessage(msg: string, stream: Stream) {
|
|
|
|
let particle: Particle | undefined;
|
|
|
|
try {
|
|
|
|
particle = Particle.fromString(msg);
|
|
|
|
log.trace('got particle from stream with id %s and particle id %s', stream.id, particle.id);
|
|
|
|
const initPeerId = peerIdFromString(particle.initPeerId);
|
|
|
|
|
|
|
|
if (initPeerId.publicKey === undefined) {
|
2023-10-12 21:01:41 +07:00
|
|
|
log.error(
|
|
|
|
'cannot retrieve public key from init_peer_id. particle id: %s. init_peer_id: %s',
|
|
|
|
particle.id,
|
|
|
|
particle.initPeerId,
|
|
|
|
);
|
2023-10-02 19:39:13 +07:00
|
|
|
return;
|
|
|
|
}
|
2023-10-12 21:01:41 +07:00
|
|
|
|
|
|
|
const isVerified = await KeyPair.verifyWithPublicKey(
|
|
|
|
initPeerId.publicKey,
|
|
|
|
buildParticleMessage(particle),
|
|
|
|
particle.signature,
|
|
|
|
);
|
2023-10-02 19:39:13 +07:00
|
|
|
if (isVerified) {
|
|
|
|
this.particleSource.next(particle);
|
|
|
|
} else {
|
|
|
|
log.trace('particle signature is incorrect. rejecting particle with id: %s', particle.id);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
const particleId = particle?.id;
|
|
|
|
const particleIdMessage = typeof particleId === 'string' ? `. particle id: ${particleId}` : '';
|
|
|
|
log.error(`error on handling an incoming message: %O%s`, e, particleIdMessage);
|
|
|
|
}
|
|
|
|
}
|
2021-10-20 22:20:43 +03:00
|
|
|
|
2023-04-03 21:52:40 +04:00
|
|
|
private async connect() {
|
|
|
|
if (this.lib2p2Peer === null) {
|
|
|
|
throw new Error('Relay connection is not started');
|
|
|
|
}
|
2023-03-11 00:03:34 +04:00
|
|
|
|
2023-09-14 10:22:59 +07:00
|
|
|
await this.lib2p2Peer.handle(
|
2023-03-11 00:03:34 +04:00
|
|
|
[PROTOCOL_NAME],
|
2023-10-12 21:01:41 +07:00
|
|
|
async ({ connection, stream }) =>
|
|
|
|
pipe(
|
|
|
|
stream.source,
|
|
|
|
decode(),
|
|
|
|
(source) => map(source, (buf) => toString(buf.subarray())),
|
|
|
|
async (source) => {
|
|
|
|
try {
|
|
|
|
for await (const msg of source) {
|
|
|
|
await this.processIncomingMessage(msg, stream);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
log.error('connection closed: %j', e);
|
2022-08-05 16:43:19 +03:00
|
|
|
}
|
2023-10-12 21:01:41 +07:00
|
|
|
},
|
|
|
|
),
|
2023-04-03 21:52:40 +04:00
|
|
|
{
|
|
|
|
maxInboundStreams: this.config.maxInboundStreams,
|
|
|
|
maxOutboundStreams: this.config.maxOutboundStreams,
|
|
|
|
},
|
2023-03-11 00:03:34 +04:00
|
|
|
);
|
2022-08-05 16:43:19 +03:00
|
|
|
|
2023-04-03 21:52:40 +04:00
|
|
|
log.debug("dialing to the node with client's address: %s", this.lib2p2Peer.peerId.toString());
|
2021-10-20 22:20:43 +03:00
|
|
|
|
|
|
|
try {
|
2023-04-03 21:52:40 +04:00
|
|
|
await this.lib2p2Peer.dial(this.relayAddress);
|
2022-05-12 17:14:16 +03:00
|
|
|
} catch (e: any) {
|
|
|
|
if (e.name === 'AggregateError' && e._errors?.length === 1) {
|
2021-10-20 22:20:43 +03:00
|
|
|
const error = e._errors[0];
|
2023-04-03 21:52:40 +04:00
|
|
|
throw new Error(`Error dialing node ${this.relayAddress}:\n${error.code}\n${error.message}`);
|
2021-10-20 22:20:43 +03:00
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-06-19 14:29:06 +03:00
|
|
|
}
|