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 #305: Libp2p support for REST API #349

Merged
merged 24 commits into from Mar 26, 2018
Merged

Feat #305: Libp2p support for REST API #349

merged 24 commits into from Mar 26, 2018

Conversation

hsanjuan
Copy link
Collaborator

@hsanjuan hsanjuan commented Mar 14, 2018

This does something very cool:

Since the cluster peer has a libp2p host, we can pipe the http traffic under encrypted libp2p streams and basically get encrypted traffic to/from the API for free, without needing to setup certificates.

Doing this has required a bunch of changes:

  • First we need to create the Host outside the Cluster component, so we can re-use it for the API component.
  • Then we need to have the API component take a host.
  • We also support that the API component creates a fully different host (with different key etc). But this is not default. But if the user provides ID, PrivateKey, ListenAddress we do that
  • Then if we want to use libp2p we use a gostream listener for libp2p-http protocol and use it with the standard http.Server
  • Then the rest client needs to be able to talk to this, rather than to http endpoint, so we create a host and use go-libp2p-http to register a custom transport and create a libp2p-powered http.Client with it
  • Then we need to have ipfs-cluster-ctl figure out when the --host parameter is an libp2p multiaddress (<...>/ipfs/peer_id), and configure the client accordingly.

The rest: expand the tests to use both plain and tunneled http, deal with PNet, use new libp2p host constructors, use new Pnet helpers and other cosmetics.

This is a very big PR, but the commits in correspond to each step, are self-contained to components and the log messages explain the change, so probably easy to check one by one.

Note, there will still be some refactoring/function splitting coming (I want to leave a green codeclimate).

Fixes #305 .

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
@hsanjuan hsanjuan self-assigned this Mar 14, 2018
@ghost ghost added the status/in-progress In progress label Mar 14, 2018
NewCluster() now takes an optional Host parameter.
The rationale is to allow to re-use an existing libp2p Host
when creating the cluster.

The NewClusterHost method now allows to create a host
with the options used by cluster.

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
This adds support for parameters to create a libp2p host
in the REST API configuration: ID, PrivateKey and ListenMultiaddr.

These parameters default to nil/empty and are ommited in the default
configuration. They are only supposed to be used when the user wants
the REST API to use a different libp2p host than a provided one (upcoming
changes).

Pnet protector not supported yet in this case. Underlying basic auth
should cover that front. Will implement if someone has a usecase.

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
This commits allows restapi to serve/tunnel http on a libp2p stream.

NewWitHost(...) allows to provide a libp2p host during initialization
which is then used to obtain a listener with go-libp2p-gostream.

Alternatively, if the configuration provides an ID, PrivateKey and Libp2pListenAddr,
a host is created directly by us and used to get the listener.

The protocol tag used is provided by the p2phttp library which will
be used by the client.

All tests now run against the libp2p node too.

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
This adds support for libp2p-tunneled http to the rest api component.

If PeerAddr is specified in the configuration, then we will create a
libp2p host and communicate with the API using that.

Tests run now in both http and libp2p mode.

Note: pnet support not included, but coming up

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
Also, re-use some convinient functions provided by pnet.

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
…that case.

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
@hsanjuan hsanjuan changed the title WIP: Feat/restapi libp2p Feat #305: Libp2p support for REST API Mar 15, 2018
@hsanjuan hsanjuan added need/review Needs a review and removed status/in-progress In progress labels Mar 15, 2018
@hsanjuan hsanjuan added this to the Open issues with difficulty:medium or lower milestone Mar 15, 2018
@coveralls
Copy link

coveralls commented Mar 15, 2018

Coverage Status

Coverage decreased (-0.2%) to 67.514% when pulling 8e7ca5a on feat/restapi-libp2p into 1abeff6 on master.

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
@ghost ghost added the status/in-progress In progress label Mar 16, 2018
License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
So they can serve as multi-module helpers without having circular deps.

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
Should reduce complexity (codeclimate).

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
Copy link
Contributor

@lanzafame lanzafame left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome @hsanjuan. A few minor nitpicks and questions. Otherwise, LGTM.

func (c *Client) setupHostname() error {
// When no host/port/multiaddress defined, we set the default
if c.config.APIAddr == nil && c.config.Host == "" && c.config.Port == "" {
c.config.APIAddr, _ = ma.NewMultiaddr(DefaultAPIAddr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not return the error from NewMultiaddr?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not feeling super guilty to panic when anyone sets DefaultAPIAddr to something stupid, but ok :)

resolveCtx, cancel := context.WithTimeout(c.ctx, c.config.Timeout)
defer cancel()
resolved, err := madns.Resolve(resolveCtx, c.config.APIAddr)
c.config.APIAddr = resolved[0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error check for madns.Resolve? If madns.Resolve fails, the slice access will panic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ops, yeah

@@ -19,7 +19,7 @@ func (c *Client) do(method, path string, body io.Reader, obj interface{}) error
}

func (c *Client) doRequest(method, path string, body io.Reader) (*http.Response, error) {
urlpath := c.urlPrefix + "/" + strings.TrimPrefix(path, "/")
urlpath := c.net + "://" + c.hostname + "/" + strings.TrimPrefix(path, "/")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a good use case for url.URL. @hsanjuan Thoughts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've considered it. It doesn't add much (other than lines of code). I'll keep it in mind if we ever need to escape query params in more than one place.

return err
}

// This should resolve addr too.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, is this a TODO comment or h.Peerstore().AddAddr() resolves the address when adding it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I thought it did, but it doesn't.

@@ -97,23 +122,19 @@ func (cfg *Config) Default() error {
// Validate makes sure that all fields in this Config have
// working values, at least in appearance.
func (cfg *Config) Validate() error {
if cfg.ListenAddr == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these if statements can be placed in a switch statement that has no expression, i.e.

	switch {
	case cfg.ReadTimeout < 0:
		return errors.New("restapi.read_timeout is invalid")
	case cfg.ReadHeaderTimeout < 0:
		return errors.New("restapi.read_header_timeout is invalid")
	<etc>
	}

See go wiki.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, and look better

@@ -202,13 +300,28 @@ func (cfg *Config) ToJSON() (raw []byte, err error) {
}()

jcfg := &jsonConfig{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to not move the field initialisations inside the struct initialisation?

jcfg := &jsonConfig{
    HTTPListenMultiaddress: cfg.HTTPListenAddr.String(),
    <etc>
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

marginally clearer to me when not all the fields in the object are initialized within the constructor. But ok.

return nil
}

// Make new host. Override existing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we providing a host.Host to NewAPIWithHost if it gets overridden regardless?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API component is passed in the main cluster component's host (see 488 in ipfs-cluster-service/main.go). If the api config doesn't specify an independent address for a new host to listen on then the api will reuse the same host. This was a bit hard to follow and though there is some documentation in the comment on line 198, perhaps we could add this to the docs somewhere. In particular would be good to remind users that even if they don't specify a libp2p listen addr in their api config they will still have libp2p listening enabled by default. Perhaps libp2p listening on the default host should be turned off or on by a flag.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, only gets overriden when there is configuration for a custom host. So configuration overrides provided.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ZenGround0 it doesn't hurt to have libp2p listening enabled by default with the cluster host. Notice that the custom-host options are hidden by default in default configs, meaning, I only want expert users (or those who really read the godocs) to use custom-host. Until we revamp the config, I think this is going to be more or less undocumented.

api/util.go Outdated
// encapsulates a new /ipfs/<peerID> address.
func Libp2pMultiaddrJoin(addr ma.Multiaddr, p peer.ID) ma.Multiaddr {
pidAddr, err := ma.NewMultiaddr("/ipfs/" + peer.IDB58Encode(p))
// let this break badly
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rename function to MustLibp2pMultiaddrJoin to convey that it will panic if it fails?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

config/util.go Outdated
Name string
}

// ParseDurations takes a time.Duration src and saves it to the given dst. into the given
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove leftover comment on the end of the line, into the given.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

configured with the --host option.
configured with the --host option. To use the secure libp2p-http
API endpoint, use "--host" with the full cluster libp2p listener
address (including the "/ipfs/<peerID>" part), and --secret.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be a bit clearer that --secret is referring to the CLUSTER_SECRET.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's in the help for the flag, but added explanation.

@hsanjuan hsanjuan mentioned this pull request Mar 20, 2018
Copy link
Collaborator

@ZenGround0 ZenGround0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still working through this, here are my comments halfway through commit 6.

@@ -56,9 +56,9 @@
"version": "2.3.6"
},
{
"author": "whyrusleeping",
"author": "hsanjuan",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this repo published somewhere now? I looked under your repos as you are the new author but couldn't find it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/gxed/go4-lock

I'm the packager, that's why my name is there. Honestly, it's difficult to figure out the true source of something with gx.

cluster.go Outdated
return nil, err

if host == nil {
h, err := NewClusterHost(ctx, cfg)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there something wrong with host, err = NewClusterHost(ctx, cfg)? It's a small change but would reduce noise a little bit if this is ok.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@@ -1097,71 +1095,6 @@ func (c *Cluster) Peers() []api.ID {
return peers
}

// makeHost makes a libp2p-host.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to see this going away

clusterhost.go Outdated
ma "github.com/multiformats/go-multiaddr"
)

// NewClusterHost creates a libp2p Host with the options in the from the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: "in the from the"

clusterhost.go Outdated
}

// EncodeProtectorKey converts a byte slice to its hex string representation.
func EncodeProtectorKey(secretBytes []byte) string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is simply a wrapper around hex.EncodeToString I am uncertain that a separate function is warranted for this.

}

func (api *API) setupLibp2p(ctx context.Context) error {
if api.config.Libp2pListenAddr == nil && api.host == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check for api.host seems gratuitous as the same check happens on line 175 and the code between here and there can execute without problem regardless.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right

return nil
}

// Make new host. Override existing
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API component is passed in the main cluster component's host (see 488 in ipfs-cluster-service/main.go). If the api config doesn't specify an independent address for a new host to listen on then the api will reuse the same host. This was a bit hard to follow and though there is some documentation in the comment on line 198, perhaps we could add this to the docs somewhere. In particular would be good to remind users that even if they don't specify a libp2p listen addr in their api config they will still have libp2p listening enabled by default. Perhaps libp2p listening on the default host should be turned off or on by a flag.

func NewAPI(cfg *Config) (*API, error) {
err := cfg.Validate()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't get called anymore when creating a new api component and I'm guessing it still should.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you' re right!

for _, a := range api.host.Addrs() {
logger.Infof(" - %s/ipfs/%s", a, api.host.ID().Pretty())
}
// TODO(h): Do we need a different server?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this resolved?

@@ -317,6 +434,9 @@ func (api *API) Shutdown() error {
// requests.
func (api *API) SetClient(c *rpc.Client) {
api.rpcClient = c

// One notification for http server and one for libp2p server.
api.rpcReady <- struct{}{}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will block indefinitely in the case where the api starts up with only an http server or only a libp2p server

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, I increased channel capacity to two elements.

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
@hsanjuan
Copy link
Collaborator Author

all comments addressed

Copy link
Contributor

@lanzafame lanzafame left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a comment typo.


// Make new host. Override existing
// Make new host. Override any provided existing one
// iif we have config for a custom one,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: iif and stray , at the end

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn' t make much sense anyways

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iff is fine, iif is not 👍

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
Copy link
Collaborator

@ZenGround0 ZenGround0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More comments

apiMAddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0")
h, err := libp2p.New(context.Background(), libp2p.ListenAddrs(apiMAddr))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok for libp2p and http to listen on the same network address?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Port 0 means it will get assigned a random port so, not the same address in the end.

httpResp, err := c.Do(req)
processResp(t, httpResp, err, resp)
}

type testF func(t *testing.T, url urlF)

func testBothEndpoints(t *testing.T, test testF) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function makes me really happy

}

func TestAPIPeerRemoveEndpoint(t *testing.T) {
rest := testAPI(t)
defer rest.Shutdown()

makeDelete(t, apiURL(rest)+"/peers/"+test.TestPeerID1.Pretty(), &struct{}{})
tf := func(t *testing.T, url urlF) {
makeDelete(t, rest, url(rest)+"/peers/"+test.TestPeerID1.Pretty(), &struct{}{})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if there is a better way for the test to ensure that the mock endpoint corresponding to Delete on /peers was called as a result of making this request. Right now I don't know if anything will visibly fail if this doesn't happen. I realize this doesn't pertain to this PR but the previous test code and so maybe a fix doesn't belong here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If hitting an unexistent endpoint (or it returns an error) processResp will fail, I think

if cfg.Timeout == 0 {
cfg.Timeout = DefaultTimeout
c.client = &http.Client{
Transport: tr,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not set c.transport to tr? What else would the transport field be used for.

)

func testClients(t *testing.T, api *rest.API, f func(*testing.T, *Client)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't these run in parallel?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if that makes you happy :)

@@ -81,7 +82,12 @@ func main() {
cli.StringFlag{
Name: "host, l",
Value: defaultHost,
Usage: "multiaddress of the IPFS Cluster service API",
Usage: "multiaddress of the IPFS Cluster REST API HTTP or LibP2P endpoint",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now this is a little misleading because both libp2p + default endpoints serve HTTP.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any ideas? I'm not sure how to explain this in a 120 char line.. (it's better explained above).

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
Copy link
Collaborator

@ZenGround0 ZenGround0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more small thing and then I am happy with this.

@@ -209,23 +213,9 @@ func (cfg *Config) loadHTTPOptions(jcfg *jsonConfig) error {
key := jcfg.SSLKeyFile
cfg.pathSSLCertFile = cert
cfg.pathSSLKeyFile = key
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like these lines can be deleted as cfg.tlsOptions() sets cfg.pathSSLCertFile and cfg.pathSSLKeyFile

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
Copy link
Collaborator

@ZenGround0 ZenGround0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Contributor

@lanzafame lanzafame left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@hsanjuan hsanjuan merged commit a4adce6 into master Mar 26, 2018
@hsanjuan hsanjuan deleted the feat/restapi-libp2p branch March 26, 2018 12:22
@ghost ghost removed the status/in-progress In progress label Mar 26, 2018
@hsanjuan hsanjuan mentioned this pull request Mar 27, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
need/review Needs a review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants