@multiformats/multiaddr
Version:
multiaddr implementation (binary + string representation of network addresses)
304 lines • 9.98 kB
JavaScript
import { base58btc } from 'multiformats/bases/base58';
import { CID } from 'multiformats/cid';
import { equals as uint8ArrayEquals } from 'uint8arrays/equals';
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
import { bytesToComponents, componentsToBytes, componentsToString, stringToComponents } from './components.js';
import { CODE_DNS, CODE_DNS4, CODE_DNS6, CODE_DNSADDR, CODE_IP4, CODE_IP6, CODE_IP6ZONE, CODE_P2P, CODE_P2P_CIRCUIT, CODE_TCP, CODE_UDP } from "./constants.js";
import { InvalidMultiaddrError, InvalidParametersError } from "./errors.js";
import { registry } from "./registry.js";
import { isMultiaddr, multiaddr, resolvers } from './index.js';
const inspect = Symbol.for('nodejs.util.inspect.custom');
export const symbol = Symbol.for('@multiformats/multiaddr');
const DNS_CODES = [
CODE_DNS,
CODE_DNS4,
CODE_DNS6,
CODE_DNSADDR
];
class NoAvailableResolverError extends Error {
constructor(message = 'No available resolver') {
super(message);
this.name = 'NoAvailableResolverError';
}
}
function toComponents(addr) {
if (addr == null) {
addr = '/';
}
if (isMultiaddr(addr)) {
return addr.getComponents();
}
if (addr instanceof Uint8Array) {
return bytesToComponents(addr);
}
if (typeof addr === 'string') {
addr = addr
.replace(/\/(\/)+/, '/')
.replace(/(\/)+$/, '');
if (addr === '') {
addr = '/';
}
return stringToComponents(addr);
}
if (Array.isArray(addr)) {
return addr;
}
throw new InvalidMultiaddrError('Must be a string, Uint8Array, Component[], or another Multiaddr');
}
/**
* Creates a {@link Multiaddr} from a {@link MultiaddrInput}
*/
export class Multiaddr {
[symbol] = true;
#components;
// cache string representation
#string;
// cache byte representation
#bytes;
constructor(addr = '/', options = {}) {
this.#components = toComponents(addr);
if (options.validate !== false) {
validate(this);
}
}
get bytes() {
if (this.#bytes == null) {
this.#bytes = componentsToBytes(this.#components);
}
return this.#bytes;
}
toString() {
if (this.#string == null) {
this.#string = componentsToString(this.#components);
}
return this.#string;
}
toJSON() {
return this.toString();
}
toOptions() {
let family;
let transport;
let host;
let port;
let zone = '';
for (const { code, name, value } of this.#components) {
if (code === CODE_IP6ZONE) {
zone = `%${value ?? ''}`;
}
// default to https when protocol & port are omitted from DNS addrs
if (DNS_CODES.includes(code)) {
transport = 'tcp';
port = 443;
host = `${value ?? ''}${zone}`;
family = code === CODE_DNS6 ? 6 : 4;
}
if (code === CODE_TCP || code === CODE_UDP) {
transport = name === 'tcp' ? 'tcp' : 'udp';
port = parseInt(value ?? '');
}
if (code === CODE_IP4 || code === CODE_IP6) {
transport = 'tcp';
host = `${value ?? ''}${zone}`;
family = code === CODE_IP6 ? 6 : 4;
}
}
if (family == null || transport == null || host == null || port == null) {
throw new Error('multiaddr must have a valid format: "/{ip4, ip6, dns4, dns6, dnsaddr}/{address}/{tcp, udp}/{port}".');
}
const opts = {
family,
host,
transport,
port
};
return opts;
}
getComponents() {
return [
...this.#components
];
}
protos() {
return this.#components.map(({ code, value }) => {
const codec = registry.getProtocol(code);
return {
code,
size: codec.size ?? 0,
name: codec.name,
resolvable: Boolean(codec.resolvable),
path: Boolean(codec.path)
};
});
}
protoCodes() {
return this.#components.map(({ code }) => code);
}
protoNames() {
return this.#components.map(({ name }) => name);
}
tuples() {
return this.#components.map(({ code, value }) => {
if (value == null) {
return [code];
}
const codec = registry.getProtocol(code);
const output = [code];
if (value != null) {
output.push(codec.valueToBytes?.(value) ?? uint8ArrayFromString(value));
}
return output;
});
}
stringTuples() {
return this.#components.map(({ code, value }) => {
if (value == null) {
return [code];
}
return [code, value];
});
}
encapsulate(addr) {
const ma = new Multiaddr(addr);
return new Multiaddr([
...this.#components,
...ma.getComponents()
], {
validate: false
});
}
decapsulate(addr) {
const addrString = addr.toString();
const s = this.toString();
const i = s.lastIndexOf(addrString);
if (i < 0) {
throw new InvalidParametersError(`Address ${this.toString()} does not contain subaddress: ${addr.toString()}`);
}
return new Multiaddr(s.slice(0, i), {
validate: false
});
}
decapsulateCode(code) {
let index;
for (let i = this.#components.length - 1; i > -1; i--) {
if (this.#components[i].code === code) {
index = i;
break;
}
}
return new Multiaddr(this.#components.slice(0, index), {
validate: false
});
}
getPeerId() {
try {
let tuples = [];
this.#components.forEach(({ code, value }) => {
if (code === CODE_P2P) {
tuples.push([code, value]);
}
// if this is a p2p-circuit address, return the target peer id if present
// not the peer id of the relay
if (code === CODE_P2P_CIRCUIT) {
tuples = [];
}
});
// Get the last ipfs tuple ['p2p', 'peerid string']
const tuple = tuples.pop();
if (tuple?.[1] != null) {
const peerIdStr = tuple[1];
// peer id is base58btc encoded string but not multibase encoded so add the `z`
// prefix so we can validate that it is correctly encoded
if (peerIdStr[0] === 'Q' || peerIdStr[0] === '1') {
return uint8ArrayToString(base58btc.decode(`z${peerIdStr}`), 'base58btc');
}
// try to parse peer id as CID
return uint8ArrayToString(CID.parse(peerIdStr).multihash.bytes, 'base58btc');
}
return null;
}
catch (e) {
return null;
}
}
getPath() {
for (const component of this.#components) {
const codec = registry.getProtocol(component.code);
if (!codec.path) {
continue;
}
return component.value ?? null;
}
return null;
}
equals(addr) {
return uint8ArrayEquals(this.bytes, addr.bytes);
}
async resolve(options) {
const resolvableProto = this.protos().find((p) => p.resolvable);
// Multiaddr is not resolvable?
if (resolvableProto == null) {
return [this];
}
const resolver = resolvers.get(resolvableProto.name);
if (resolver == null) {
throw new NoAvailableResolverError(`no available resolver for ${resolvableProto.name}`);
}
const result = await resolver(this, options);
return result.map(str => multiaddr(str));
}
nodeAddress() {
const options = this.toOptions();
if (options.transport !== 'tcp' && options.transport !== 'udp') {
throw new Error(`multiaddr must have a valid format - no protocol with name: "${options.transport}". Must have a valid transport protocol: "{tcp, udp}"`);
}
return {
family: options.family,
address: options.host,
port: options.port
};
}
isThinWaistAddress() {
if (this.#components.length !== 2) {
return false;
}
if (this.#components[0].code !== CODE_IP4 && this.#components[0].code !== CODE_IP6) {
return false;
}
if (this.#components[1].code !== CODE_TCP && this.#components[1].code !== CODE_UDP) {
return false;
}
return true;
}
/**
* Returns Multiaddr as a human-readable string
* https://nodejs.org/api/util.html#utilinspectcustom
*
* @example
* ```js
* import { multiaddr } from '@multiformats/multiaddr'
*
* console.info(multiaddr('/ip4/127.0.0.1/tcp/4001'))
* // 'Multiaddr(/ip4/127.0.0.1/tcp/4001)'
* ```
*/
[inspect]() {
return `Multiaddr(${this.toString()})`;
}
}
/**
* Ensures all multiaddr tuples are correct. Throws if any invalid protocols or
* values are encountered.
*/
export function validate(addr) {
addr.getComponents()
.forEach(component => {
const codec = registry.getProtocol(component.code);
if (component.value == null) {
return;
}
codec.validate?.(component.value);
});
}
//# sourceMappingURL=multiaddr.js.map