Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fetch protocol #1036

Merged
merged 26 commits into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
afb5188
rebuild protobufs on clean build
stbrody Nov 24, 2021
3ab5c0e
feat: Add fetch protocol
stbrody Nov 24, 2021
9559b2b
update readme
stbrody Nov 24, 2021
7ceb9c7
fix comment
stbrody Nov 29, 2021
5a9eea0
wip: use a class to have a stateful instance of fetch protocol
stbrody Nov 29, 2021
2686e5e
can register multiple lookup functions
stbrody Nov 29, 2021
dbb3315
update tests
stbrody Nov 29, 2021
3d73cc0
update docs/comments
stbrody Nov 29, 2021
d75ef08
Use error when missing key handler instead of just returning null
stbrody Nov 29, 2021
0126366
underscore prefix private methods/variables
stbrody Nov 29, 2021
839d349
small comment cleanup
stbrody Nov 29, 2021
daf7232
minor
stbrody Nov 29, 2021
d1627a9
revert generated protobuf code from other modules
stbrody Jan 7, 2022
9a13fb9
Merge remote-tracking branch 'origin' into feat/fetch-protocol
stbrody Jan 7, 2022
5882a0e
fix test setup
stbrody Jan 7, 2022
d658007
use error codes
stbrody Jan 7, 2022
db9990f
Merge remote-tracking branch 'origin/master' into feat/fetch-protocol
achingbrain Jan 24, 2022
ef2d860
chore: fix linting and tests
achingbrain Jan 24, 2022
bb61c9e
chore: refactor for consistency with identify service
achingbrain Jan 24, 2022
30f7e59
chore: make fetch command top level
achingbrain Jan 24, 2022
da00c4d
chore: document fetch method
achingbrain Jan 24, 2022
a005361
chore: document register/unregister
achingbrain Jan 24, 2022
839a487
chore: put prepare back
achingbrain Jan 24, 2022
24e1ecb
chore: reuse existing error code
achingbrain Jan 24, 2022
d03018b
chore: update fetch protocol name in line with spec
achingbrain Jan 24, 2022
bb04a19
chore: linting
achingbrain Jan 24, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@
"scripts": {
"lint": "aegir lint",
"build": "aegir build",
"build:proto": "npm run build:proto:circuit && npm run build:proto:identify && npm run build:proto:plaintext && npm run build:proto:address-book && npm run build:proto:proto-book && npm run build:proto:peer-record && npm run build:proto:envelope",
"build:proto": "npm run build:proto:circuit && npm run build:proto:fetch && npm run build:proto:identify && npm run build:proto:plaintext && npm run build:proto:address-book && npm run build:proto:proto-book && npm run build:proto:peer-record && npm run build:proto:envelope",
"build:proto:circuit": "pbjs -t static-module -w commonjs -r libp2p-circuit --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/circuit/protocol/index.js ./src/circuit/protocol/index.proto",
"build:proto:fetch": "pbjs -t static-module -w commonjs -r libp2p-fetch --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/fetch/proto.js ./src/fetch/proto.proto",
"build:proto:identify": "pbjs -t static-module -w commonjs -r libp2p-identify --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/identify/message.js ./src/identify/message.proto",
"build:proto:plaintext": "pbjs -t static-module -w commonjs -r libp2p-plaintext --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/insecure/proto.js ./src/insecure/proto.proto",
"build:proto:address-book": "pbjs -t static-module -w commonjs -r libp2p-address-book --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/peer-store/persistent/pb/address-book.js ./src/peer-store/persistent/pb/address-book.proto",
"build:proto:proto-book": "pbjs -t static-module -w commonjs -r libp2p-proto-book --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/peer-store/persistent/pb/proto-book.js ./src/peer-store/persistent/pb/proto-book.proto",
"build:proto:peer-record": "pbjs -t static-module -w commonjs -r libp2p-peer-record --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/record/peer-record/peer-record.js ./src/record/peer-record/peer-record.proto",
"build:proto:envelope": "pbjs -t static-module -w commonjs -r libp2p-envelope --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/record/envelope/envelope.js ./src/record/envelope/envelope.proto",
"build:proto-types": "npm run build:proto-types:circuit && npm run build:proto-types:identify && npm run build:proto-types:plaintext && npm run build:proto-types:address-book && npm run build:proto-types:proto-book && npm run build:proto-types:peer-record && npm run build:proto-types:envelope",
"build:proto-types": "npm run build:proto-types:circuit && npm run build:proto-types:fetch && npm run build:proto-types:identify && npm run build:proto-types:plaintext && npm run build:proto-types:address-book && npm run build:proto-types:proto-book && npm run build:proto-types:peer-record && npm run build:proto-types:envelope",
"build:proto-types:circuit": "pbts -o src/circuit/protocol/index.d.ts src/circuit/protocol/index.js",
"build:proto-types:fetch": "pbts -o src/fetch/proto.d.ts src/fetch/proto.js",
"build:proto-types:identify": "pbts -o src/identify/message.d.ts src/identify/message.js",
"build:proto-types:plaintext": "pbts -o src/insecure/proto.d.ts src/insecure/proto.js",
"build:proto-types:address-book": "pbts -o src/peer-store/persistent/pb/address-book.d.ts src/peer-store/persistent/pb/address-book.js",
Expand Down
32 changes: 32 additions & 0 deletions src/fetch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
libp2p-fetch JavaScript Implementation
=====================================

> Libp2p fetch protocol JavaScript implementation

## Overview

An implementation of the Fetch protocol as described here: https://github.com/libp2p/specs/tree/master/fetch

The fetch protocol is a simple protocol for requesting a value corresponding to a key from a peer.

## Usage

```javascript
var Fetch = require('libp2p/src/fetch')

/**
* Given a key (as a string) returns a value (as a Uint8Array), or null if the key isn't found.
* @param key - a string
* @returns value - a Uint8Array value that corresponds to the given key, or null if the key doesn't
* have a corresponding value.
*/
function lookup(key) {
stbrody marked this conversation as resolved.
Show resolved Hide resolved
// app specific callback to lookup key-value pairs.
}

Fetch.mount(libp2p, lookup) // Enable this peer to respond to fetch requests

const value = await Fetch(libp2p, peerDst, key)

Fetch.unmount(libp2p)
```
7 changes: 7 additions & 0 deletions src/fetch/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict'

module.exports = {
PROTOCOL: '/ipfs/fetch/0.0.1', // deprecated
PROTOCOL_VERSION: '0.0.1',
PROTOCOL_NAME: 'fetch'
}
106 changes: 106 additions & 0 deletions src/fetch/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
'use strict'

const debug = require('debug')
const log = Object.assign(debug('libp2p:fetch'), {
error: debug('libp2p:fetch:err')
})
const lp = require('it-length-prefixed')
const { FetchRequest, FetchResponse } = require('./proto')
// @ts-ignore it-handshake does not export types
const handshake = require('it-handshake')

const { PROTOCOL_NAME, PROTOCOL_VERSION } = require('./constants')


/**
* @typedef {import('../')} Libp2p
* @typedef {import('multiaddr').Multiaddr} Multiaddr
* @typedef {import('peer-id')} PeerId
* @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxedStream} MuxedStream
* @typedef {(key: string) => Promise<Uint8Array | null>} LookupFunction
*/

/**
* Ping a given peer and wait for its response, getting the operation latency.
stbrody marked this conversation as resolved.
Show resolved Hide resolved
*
* @param {Libp2p} node
* @param {PeerId|Multiaddr} peer
* @param {string} key
* @returns {Promise<Uint8Array | null>}
*/
async function fetch (node, peer, key) {
const protocol = `/${node._config.protocolPrefix}/${PROTOCOL_NAME}/${PROTOCOL_VERSION}`
// @ts-ignore multiaddr might not have toB58String
log('dialing %s to %s', protocol, peer.toB58String ? peer.toB58String() : peer)

const connection = await node.dial(peer)
const { stream } = await connection.newStream(protocol)
const shake = handshake(stream)

// send message
const request = new FetchRequest({ identifier: key })
shake.write(lp.encode.single(FetchRequest.encode(request).finish()))

// read response
const response = FetchResponse.decode((await lp.decode.fromReader(shake.reader).next()).value.slice())
switch (response.status) {
case (FetchResponse.StatusCode.OK): {
return response.data
}
case (FetchResponse.StatusCode.NOT_FOUND): {
return null
}
case (FetchResponse.StatusCode.ERROR): {
throw new Error('Error in fetch protocol response')
}
default: {
throw new Error('Unreachable case')
}
}
}

/**
* Invoked when a fetch request is received. Reads the request message off the given stream and
* responds based on looking up the key in the request via the lookup callback.
* @param {MuxedStream} stream
* @param {LookupFunction} lookup
*/
async function handleRequest(stream, lookup) {
const shake = handshake(stream)
const request = FetchRequest.decode((await lp.decode.fromReader(shake.reader).next()).value.slice())

let response
const data = await lookup(request.identifier)
stbrody marked this conversation as resolved.
Show resolved Hide resolved
if (data) {
response = new FetchResponse({ status: FetchResponse.StatusCode.OK, data })
} else {
response = new FetchResponse({ status: FetchResponse.StatusCode.NOT_FOUND })
}

shake.write(lp.encode.single(FetchResponse.encode(response).finish()))
}

/**
* Subscribe fetch protocol handler. Must be given a lookup function callback that can be used
* to lookup a value (of type Uint8Array) from a given key (of type string). The lookup function
* should return null if the key isn't found.
*
* @param {Libp2p} node
* @param {LookupFunction} lookup
*/
function mount (node, lookup) {
node.handle(`/${node._config.protocolPrefix}/${PROTOCOL_NAME}/${PROTOCOL_VERSION}`, ({ stream }) => handleRequest(stream, lookup))
}

/**
* Unsubscribe fetch protocol handler.
*
* @param {Libp2p} node
*/
function unmount (node) {
node.unhandle(`/${node._config.protocolPrefix}/${PROTOCOL_NAME}/${PROTOCOL_VERSION}`)
}

exports = module.exports = fetch
exports.mount = mount
exports.unmount = unmount
134 changes: 134 additions & 0 deletions src/fetch/proto.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as $protobuf from "protobufjs";
/** Properties of a FetchRequest. */
export interface IFetchRequest {

/** FetchRequest identifier */
identifier?: (string|null);
}

/** Represents a FetchRequest. */
export class FetchRequest implements IFetchRequest {

/**
* Constructs a new FetchRequest.
* @param [p] Properties to set
*/
constructor(p?: IFetchRequest);

/** FetchRequest identifier. */
public identifier: string;

/**
* Encodes the specified FetchRequest message. Does not implicitly {@link FetchRequest.verify|verify} messages.
* @param m FetchRequest message or plain object to encode
* @param [w] Writer to encode to
* @returns Writer
*/
public static encode(m: IFetchRequest, w?: $protobuf.Writer): $protobuf.Writer;

/**
* Decodes a FetchRequest message from the specified reader or buffer.
* @param r Reader or buffer to decode from
* @param [l] Message length if known beforehand
* @returns FetchRequest
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): FetchRequest;

/**
* Creates a FetchRequest message from a plain object. Also converts values to their respective internal types.
* @param d Plain object
* @returns FetchRequest
*/
public static fromObject(d: { [k: string]: any }): FetchRequest;

/**
* Creates a plain object from a FetchRequest message. Also converts values to other types if specified.
* @param m FetchRequest
* @param [o] Conversion options
* @returns Plain object
*/
public static toObject(m: FetchRequest, o?: $protobuf.IConversionOptions): { [k: string]: any };

/**
* Converts this FetchRequest to JSON.
* @returns JSON object
*/
public toJSON(): { [k: string]: any };
}

/** Properties of a FetchResponse. */
export interface IFetchResponse {

/** FetchResponse status */
status?: (FetchResponse.StatusCode|null);

/** FetchResponse data */
data?: (Uint8Array|null);
}

/** Represents a FetchResponse. */
export class FetchResponse implements IFetchResponse {

/**
* Constructs a new FetchResponse.
* @param [p] Properties to set
*/
constructor(p?: IFetchResponse);

/** FetchResponse status. */
public status: FetchResponse.StatusCode;

/** FetchResponse data. */
public data: Uint8Array;

/**
* Encodes the specified FetchResponse message. Does not implicitly {@link FetchResponse.verify|verify} messages.
* @param m FetchResponse message or plain object to encode
* @param [w] Writer to encode to
* @returns Writer
*/
public static encode(m: IFetchResponse, w?: $protobuf.Writer): $protobuf.Writer;

/**
* Decodes a FetchResponse message from the specified reader or buffer.
* @param r Reader or buffer to decode from
* @param [l] Message length if known beforehand
* @returns FetchResponse
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): FetchResponse;

/**
* Creates a FetchResponse message from a plain object. Also converts values to their respective internal types.
* @param d Plain object
* @returns FetchResponse
*/
public static fromObject(d: { [k: string]: any }): FetchResponse;

/**
* Creates a plain object from a FetchResponse message. Also converts values to other types if specified.
* @param m FetchResponse
* @param [o] Conversion options
* @returns Plain object
*/
public static toObject(m: FetchResponse, o?: $protobuf.IConversionOptions): { [k: string]: any };

/**
* Converts this FetchResponse to JSON.
* @returns JSON object
*/
public toJSON(): { [k: string]: any };
}

export namespace FetchResponse {

/** StatusCode enum. */
enum StatusCode {
OK = 0,
NOT_FOUND = 1,
ERROR = 2
}
}
Loading