Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit 461224d

Browse files
committed
fix(pubsub): new wire format in http rpc
Implements changes from ipfs/kubo#8183
1 parent afeb20d commit 461224d

File tree

6 files changed

+102
-28
lines changed

6 files changed

+102
-28
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
2+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
3+
import { base64url } from 'multiformats/bases/base64'
4+
import PeerId from 'peer-id'
5+
6+
/* HTTP RPC:
7+
* - sends PeerIds as Base58btc (legacy) or CIDv1
8+
* - wraps binary data in multibase. base64url is used to avoid issues
9+
* when a binary data is passed as search param in URL.
10+
* Historical context: https://github.com/ipfs/go-ipfs/issues/7939
11+
* Multibase wrapping introduced in: https://github.com/ipfs/go-ipfs/pull/8183
12+
*/
13+
14+
/**
15+
* @param {Array<string>} strings
16+
* @returns {Array<string>} strings
17+
*/
18+
const mbToTextArray = strings => {
19+
if (Array.isArray(strings)) {
20+
return strings.map(mbToText)
21+
}
22+
return strings
23+
}
24+
25+
/**
26+
* @param {string} mb
27+
* @returns {string}
28+
*/
29+
const mbToText = mb => uint8ArrayToString(mbToBytes(mb))
30+
31+
/**
32+
* @param {string} mb
33+
* @returns {Uint8Array}
34+
*/
35+
const mbToBytes = mb => base64url.decode(mb)
36+
37+
/**
38+
* @param {string} text
39+
* @returns {string}
40+
*/
41+
const toUrlSafeBase = text => base64url.encode(uint8ArrayFromString(text))
42+
43+
/**
44+
* Ensure uniform Peer ID representation in text
45+
*
46+
* @param {Array<string>} peerids
47+
* @returns {Array<string>} peerids
48+
*/
49+
const normalizePeerIds = peerids => {
50+
if (Array.isArray(peerids)) {
51+
return peerids.map(normalizePeerId)
52+
}
53+
return peerids
54+
}
55+
56+
/**
57+
* Ensure uniform Peer ID representation in text
58+
*
59+
* @param {string} peerid
60+
* @returns {string}
61+
*/
62+
const normalizePeerId = peerid => PeerId.parse(peerid).toB58String() // TODO: toString() when go-ipfs switch to CIDv1
63+
64+
export { mbToTextArray, mbToText, mbToBytes, toUrlSafeBase, normalizePeerIds, normalizePeerId }

packages/ipfs-http-client/src/pubsub/ls.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { configure } from '../lib/configure.js'
22
import { toUrlSearchParams } from '../lib/to-url-search-params.js'
3+
import { mbToTextArray } from '../lib/http-rpc-wire-format.js'
34

45
/**
56
* @typedef {import('../types').HTTPClientExtraOptions} HTTPClientExtraOptions
@@ -17,8 +18,7 @@ export const createLs = configure(api => {
1718
headers: options.headers
1819
})).json()
1920

20-
// TODO: unwrap topic names from multibase
21-
return Strings || []
21+
return mbToTextArray(Strings) || []
2222
}
2323
return ls
2424
})

packages/ipfs-http-client/src/pubsub/publish.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { configure } from '../lib/configure.js'
22
import { toUrlSearchParams } from '../lib/to-url-search-params.js'
33
import { multipartRequest } from 'ipfs-core-utils/multipart-request'
44
import { abortSignal } from '../lib/abort-signal.js'
5+
import { toUrlSafeBase } from '../lib/http-rpc-wire-format.js'
56
import { AbortController } from 'native-abort-controller'
67

78
/**
@@ -14,9 +15,8 @@ export const createPublish = configure(api => {
1415
* @type {PubsubAPI["publish"]}
1516
*/
1617
async function publish (topic, data, options = {}) {
17-
// TODO: wrap topic in multibase
1818
const searchParams = toUrlSearchParams({
19-
arg: topic,
19+
arg: toUrlSafeBase(topic),
2020
...options
2121
})
2222

packages/ipfs-http-client/src/pubsub/subscribe.js

+6-10
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
2-
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
31
import debug from 'debug'
42
import { configure } from '../lib/configure.js'
53
import { toUrlSearchParams } from '../lib/to-url-search-params.js'
4+
import { toUrlSafeBase, mbToTextArray, mbToBytes } from '../lib/http-rpc-wire-format.js'
65
const log = debug('ipfs-http-client:pubsub:subscribe')
76

87
/**
@@ -39,12 +38,11 @@ export const createSubscribe = (options, subsTracker) => {
3938
// is received. If this doesn't happen within 1 second assume success
4039
const ffWorkaround = setTimeout(() => done(), 1000)
4140

42-
// TODO: wrap topic in multibase
4341
// Do this async to not block Firefox
4442
api.post('pubsub/sub', {
4543
signal: options.signal,
4644
searchParams: toUrlSearchParams({
47-
arg: topic,
45+
arg: toUrlSafeBase(topic),
4846
...options
4947
}),
5048
headers: options.headers
@@ -95,13 +93,11 @@ async function readMessages (response, { onMessage, onEnd, onError }) {
9593
continue
9694
}
9795

98-
// TODO: multibase data, seqno and topics
99-
// TODO: parse string and get peerid bytes using libp2p lib
10096
onMessage({
101-
from: uint8ArrayToString(uint8ArrayFromString(msg.from, 'base64pad'), 'base58btc'),
102-
data: uint8ArrayFromString(msg.data, 'base64pad'),
103-
seqno: uint8ArrayFromString(msg.seqno, 'base64pad'),
104-
topicIDs: msg.topicIDs
97+
from: msg.from,
98+
data: mbToBytes(msg.data),
99+
seqno: mbToBytes(msg.seqno),
100+
topicIDs: mbToTextArray(msg.topicIDs)
105101
})
106102
} catch (/** @type {any} */ err) {
107103
err.message = `Failed to parse pubsub message: ${err.message}`

packages/ipfs-http-client/test/utils/factory.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const commonOptions = {
1616

1717
const commonOverrides = {
1818
go: {
19-
ipfsBin: isNode ? path() : undefined
19+
ipfsBin: isNode ? (process.env.IPFS_GO_EXEC || path()) : undefined
2020
}
2121
}
2222

packages/ipfs-http-server/src/api/resources/pubsub.js

+27-13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
66
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
77
import { streamResponse } from '../../utils/stream-response.js'
88
import pushable from 'it-pushable'
9+
import { base64url } from 'multiformats/bases/base64'
10+
11+
const preDecodeTopicFromHttpRpc = {
12+
assign: 'topic',
13+
/**
14+
* @param {import('../../types').Request} request
15+
* @param {import('@hapi/hapi').ResponseToolkit} _h
16+
*/
17+
method: async (request, _h) => {
18+
try {
19+
return uint8ArrayToString(base64url.decode(request.query.topic))
20+
} catch (/** @type {any} */ err) {
21+
throw Boom.boomify(err, { message: `Failed to decode topic from HTTP RPC form ${request.query.topic}` })
22+
}
23+
}
24+
}
925

1026
export const subscribeResource = {
1127
options: {
@@ -24,7 +40,8 @@ export const subscribeResource = {
2440
override: true,
2541
ignoreUndefined: true
2642
})
27-
}
43+
},
44+
pre: [preDecodeTopicFromHttpRpc]
2845
},
2946
/**
3047
* @param {import('../../types').Request} request
@@ -56,13 +73,11 @@ export const subscribeResource = {
5673
* @type {import('ipfs-core-types/src/pubsub').MessageHandlerFn}
5774
*/
5875
const handler = (msg) => {
59-
// TODO: data, seqno and topicIDs in multibase
60-
// TODO: from should use canonical toString from peerid libp2p lib
6176
output.push({
62-
from: uint8ArrayToString(uint8ArrayFromString(msg.from, 'base58btc'), 'base64pad'),
63-
data: uint8ArrayToString(msg.data, 'base64pad'),
64-
seqno: uint8ArrayToString(msg.seqno, 'base64pad'),
65-
topicIDs: msg.topicIDs
77+
from: msg.from, // TODO: switch to PeerId.parse(msg.from).toString() when go-ipfs defaults to CIDv1
78+
data: base64url.encode(msg.data),
79+
seqno: base64url.encode(msg.seqno),
80+
topicIDs: msg.topicIDs.map(t => base64url.encode(uint8ArrayFromString(t)))
6681
})
6782
}
6883

@@ -92,7 +107,7 @@ export const publishResource = {
92107
parse: false,
93108
output: 'stream'
94109
},
95-
pre: [{
110+
pre: [preDecodeTopicFromHttpRpc, {
96111
assign: 'data',
97112
/**
98113
* @param {import('../../types').Request} request
@@ -158,7 +173,7 @@ export const publishResource = {
158173
} = request
159174

160175
try {
161-
// TODO: unwrap topic from multibase?
176+
// TODO: move to reusable pre handler?
162177
await ipfs.pubsub.publish(topic, data, {
163178
signal,
164179
timeout
@@ -212,8 +227,7 @@ export const lsResource = {
212227
throw Boom.boomify(err, { message: 'Failed to list subscriptions' })
213228
}
214229

215-
// TODO: multibase topic names in Strings array
216-
return h.response({ Strings: subscriptions })
230+
return h.response({ Strings: subscriptions.map(s => base64url.encode(uint8ArrayFromString(s))) })
217231
}
218232
}
219233

@@ -232,7 +246,8 @@ export const peersResource = {
232246
override: true,
233247
ignoreUndefined: true
234248
})
235-
}
249+
},
250+
pre: [preDecodeTopicFromHttpRpc]
236251
},
237252
/**
238253
* @param {import('../../types').Request} request
@@ -256,7 +271,6 @@ export const peersResource = {
256271

257272
let peers
258273
try {
259-
// TODO: unwrap topic from multibase
260274
peers = await ipfs.pubsub.peers(topic, {
261275
signal,
262276
timeout

0 commit comments

Comments
 (0)