diff --git a/cmd/ipfs/daemon.go b/cmd/ipfs/daemon.go index 9a44c960b..86a50a867 100644 --- a/cmd/ipfs/daemon.go +++ b/cmd/ipfs/daemon.go @@ -292,9 +292,16 @@ func serveHTTPApi(req cmds.Request) (error, <-chan error) { return fmt.Errorf("serveHTTPApi: GetConfig() failed: %s", err), nil } - apiMaddr, err := ma.NewMultiaddr(cfg.Addresses.API) + apiAddr, _, err := req.Option(commands.ApiOption).String() if err != nil { - return fmt.Errorf("serveHTTPApi: invalid API address: %q (err: %s)", cfg.Addresses.API, err), nil + return fmt.Errorf("serveHTTPApi: %s", err), nil + } + if apiAddr == "" { + apiAddr = cfg.Addresses.API + } + apiMaddr, err := ma.NewMultiaddr(apiAddr) + if err != nil { + return fmt.Errorf("serveHTTPApi: invalid API address: %q (err: %s)", apiAddr, err), nil } apiLis, err := manet.Listen(apiMaddr) @@ -344,7 +351,11 @@ func serveHTTPApi(req cmds.Request) (error, <-chan error) { node, err := req.InvocContext().ConstructNode() if err != nil { - return fmt.Errorf("serveHTTPGateway: ConstructNode() failed: %s", err), nil + return fmt.Errorf("serveHTTPApi: ConstructNode() failed: %s", err), nil + } + + if err := node.Repo.SetAPIAddr(apiAddr); err != nil { + return fmt.Errorf("serveHTTPApi: SetAPIAddr() failed: %s", err), nil } errc := make(chan error) diff --git a/cmd/ipfs/main.go b/cmd/ipfs/main.go index c6c6da8e9..8dea4d5f5 100644 --- a/cmd/ipfs/main.go +++ b/cmd/ipfs/main.go @@ -23,6 +23,8 @@ import ( cmdsCli "github.com/ipfs/go-ipfs/commands/cli" cmdsHttp "github.com/ipfs/go-ipfs/commands/http" core "github.com/ipfs/go-ipfs/core" + coreCmds "github.com/ipfs/go-ipfs/core/commands" + repo "github.com/ipfs/go-ipfs/repo" config "github.com/ipfs/go-ipfs/repo/config" fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo" eventlog "github.com/ipfs/go-ipfs/thirdparty/eventlog" @@ -32,8 +34,10 @@ import ( // log is the command logger var log = eventlog.Logger("cmd/ipfs") -// signal to output help -var errHelpRequested = errors.New("Help Requested") +var ( + errUnexpectedApiOutput = errors.New("api returned unexpected output") + errApiVersionMismatch = errors.New("api version mismatch") +) const ( EnvEnableProfiling = "IPFS_PROF" @@ -292,8 +296,7 @@ func callCommand(ctx context.Context, req cmds.Request, root *cmds.Command, cmd return nil, err } - log.Debug("looking for running daemon...") - useDaemon, err := commandShouldRunOnDaemon(*details, req, root) + client, err := commandShouldRunOnDaemon(*details, req, root) if err != nil { return nil, err } @@ -310,28 +313,13 @@ func callCommand(ctx context.Context, req cmds.Request, root *cmds.Command, cmd } } - if useDaemon { - - cfg, err := req.InvocContext().GetConfig() - if err != nil { - return nil, err - } - - addr, err := ma.NewMultiaddr(cfg.Addresses.API) - if err != nil { - return nil, err - } - - log.Infof("Executing command on daemon running at %s", addr) - _, host, err := manet.DialArgs(addr) - if err != nil { - return nil, err - } - - client := cmdsHttp.NewClient(host) - + if client != nil { + log.Debug("Executing command via API") res, err = client.Send(req) if err != nil { + if isConnRefused(err) { + err = repo.ErrApiNotRunning + } return nil, err } @@ -380,48 +368,67 @@ func commandDetails(path []string, root *cmds.Command) (*cmdDetails, error) { // commandShouldRunOnDaemon determines, from commmand details, whether a // command ought to be executed on an IPFS daemon. // -// It returns true if the command should be executed on a daemon and false if +// It returns a client if the command should be executed on a daemon and nil if // it should be executed on a client. It returns an error if the command must // NOT be executed on either. -func commandShouldRunOnDaemon(details cmdDetails, req cmds.Request, root *cmds.Command) (bool, error) { +func commandShouldRunOnDaemon(details cmdDetails, req cmds.Request, root *cmds.Command) (cmdsHttp.Client, error) { path := req.Path() // root command. if len(path) < 1 { - return false, nil + return nil, nil } if details.cannotRunOnClient && details.cannotRunOnDaemon { - return false, fmt.Errorf("command disabled: %s", path[0]) + return nil, fmt.Errorf("command disabled: %s", path[0]) } if details.doesNotUseRepo && details.canRunOnClient() { - return false, nil + return nil, nil } - // at this point need to know whether daemon is running. we defer - // to this point so that some commands dont open files unnecessarily. - daemonLocked, err := fsrepo.LockedByOtherProcess(req.InvocContext().ConfigRoot) + // at this point need to know whether api is running. we defer + // to this point so that we dont check unnecessarily + + // did user specify an api to use for this command? + apiAddrStr, _, err := req.Option(coreCmds.ApiOption).String() if err != nil { - return false, err + return nil, err } - if daemonLocked { - - log.Info("a daemon is running...") - - if details.cannotRunOnDaemon { - e := "ipfs daemon is running. please stop it to run this command" - return false, cmds.ClientError(e) + client, err := getApiClient(req.InvocContext().ConfigRoot, apiAddrStr) + if err == repo.ErrApiNotRunning { + if apiAddrStr != "" && req.Command() != daemonCmd { + // if user SPECIFIED an api, and this cmd is not daemon + // we MUST use it. so error out. + return nil, err } - return true, nil + // ok for api not to be running + } else if err != nil { // some other api error + return nil, err + } + + if client != nil { // daemon is running + if details.cannotRunOnDaemon { + e := "cannot use API with this command." + + // check if daemon locked. legacy error text, for now. + daemonLocked, _ := fsrepo.LockedByOtherProcess(req.InvocContext().ConfigRoot) + if daemonLocked { + e = "ipfs daemon is running. please stop it to run this command" + } + + return nil, cmds.ClientError(e) + } + + return client, nil } if details.cannotRunOnClient { - return false, cmds.ClientError("must run on the ipfs daemon") + return nil, cmds.ClientError("must run on the ipfs daemon") } - return false, nil + return nil, nil } func isClientError(err error) bool { @@ -571,3 +578,92 @@ func profileIfEnabled() (func(), error) { } return func() {}, nil } + +// getApiClient checks the repo, and the given options, checking for +// a running API service. if there is one, it returns a client. +// otherwise, it returns errApiNotRunning, or another error. +func getApiClient(repoPath, apiAddrStr string) (cmdsHttp.Client, error) { + + if apiAddrStr == "" { + var err error + if apiAddrStr, err = fsrepo.APIAddr(repoPath); err != nil { + return nil, err + } + } + + addr, err := ma.NewMultiaddr(apiAddrStr) + if err != nil { + return nil, err + } + + client, err := apiClientForAddr(addr) + if err != nil { + return nil, err + } + + // make sure the api is actually running. + // this is slow, as it might mean an RTT to a remote server. + // TODO: optimize some way + if err := apiVersionMatches(client); err != nil { + return nil, err + } + + return client, nil +} + +// apiVersionMatches checks whether the api server is running the +// same version of go-ipfs. for now, only the exact same version of +// client + server work. In the future, we should use semver for +// proper API versioning! \o/ +func apiVersionMatches(client cmdsHttp.Client) (err error) { + ver, err := doVersionRequest(client) + if err != nil { + return err + } + + currv := config.CurrentVersionNumber + if ver.Version != currv { + return fmt.Errorf("%s (%s != %s)", errApiVersionMismatch, ver.Version, currv) + } + return nil +} + +func doVersionRequest(client cmdsHttp.Client) (*coreCmds.VersionOutput, error) { + cmd := coreCmds.VersionCmd + optDefs, err := cmd.GetOptions([]string{}) + if err != nil { + return nil, err + } + + req, err := cmds.NewRequest([]string{"version"}, nil, nil, nil, cmd, optDefs) + if err != nil { + return nil, err + } + + res, err := client.Send(req) + if err != nil { + if isConnRefused(err) { + err = repo.ErrApiNotRunning + } + return nil, err + } + + ver, ok := res.Output().(*coreCmds.VersionOutput) + if !ok { + return nil, errUnexpectedApiOutput + } + return ver, nil +} + +func apiClientForAddr(addr ma.Multiaddr) (cmdsHttp.Client, error) { + _, host, err := manet.DialArgs(addr) + if err != nil { + return nil, err + } + + return cmdsHttp.NewClient(host), nil +} + +func isConnRefused(err error) bool { + return strings.Contains(err.Error(), "connection refused") +} diff --git a/core/commands/root.go b/core/commands/root.go index c99f02660..38cfa8472 100644 --- a/core/commands/root.go +++ b/core/commands/root.go @@ -16,6 +16,10 @@ type TestOutput struct { Bar int } +const ( + ApiOption = "api" +) + var Root = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "global p2p merkle-dag filesystem", @@ -73,6 +77,7 @@ Use 'ipfs --help' to learn more about each command. cmds.BoolOption("help", "Show the full command help text"), cmds.BoolOption("h", "Show a short version of the command help text"), cmds.BoolOption("local", "L", "Run the command locally, instead of using the daemon"), + cmds.StringOption(ApiOption, "Overrides the routing option (dht, supernode)"), }, } diff --git a/repo/fsrepo/fsrepo.go b/repo/fsrepo/fsrepo.go index dc20b00b4..b3ea807ca 100644 --- a/repo/fsrepo/fsrepo.go +++ b/repo/fsrepo/fsrepo.go @@ -58,6 +58,7 @@ func (err NoRepoError) Error() string { const ( leveldbDirectory = "datastore" flatfsDirectory = "blocks" + apiFile = "api" ) var ( @@ -285,14 +286,53 @@ func Remove(repoPath string) error { // process. If true, then the repo cannot be opened by this process. func LockedByOtherProcess(repoPath string) (bool, error) { repoPath = path.Clean(repoPath) - - // TODO replace this with the "api" file - // https://github.com/ipfs/specs/tree/master/repo/fs-repo - // NB: the lock is only held when repos are Open return lockfile.Locked(repoPath) } +// APIAddr returns the registered API addr, according to the api file +// in the fsrepo. This is a concurrent operation, meaning that any +// process may read this file. modifying this file, therefore, should +// use "mv" to replace the whole file and avoid interleaved read/writes. +func APIAddr(repoPath string) (string, error) { + repoPath = path.Clean(repoPath) + apiFilePath := path.Join(repoPath, apiFile) + + // if there is no file, assume there is no api addr. + f, err := os.Open(apiFilePath) + if err != nil { + if os.IsNotExist(err) { + return "", repo.ErrApiNotRunning + } + return "", err + } + defer f.Close() + + // read up to 2048 bytes. io.ReadAll is a vulnerability, as + // someone could hose the process by putting a massive file there. + buf := make([]byte, 2048) + n, err := f.Read(buf) + if err != nil && err != io.EOF { + return "", err + } + + s := string(buf[:n]) + s = strings.TrimSpace(s) + return s, nil +} + +// SetAPIAddr writes the API Addr to the /api file. +func (r *FSRepo) SetAPIAddr(addr string) error { + f, err := os.Create(path.Join(r.path, apiFile)) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(addr) + return err +} + // openConfig returns an error if the config file is not present. func (r *FSRepo) openConfig() error { configFilename, err := config.Filename(r.path) diff --git a/repo/mock.go b/repo/mock.go index cfd8e4507..7446eeff8 100644 --- a/repo/mock.go +++ b/repo/mock.go @@ -35,3 +35,5 @@ func (m *Mock) GetConfigKey(key string) (interface{}, error) { func (m *Mock) Datastore() ds.ThreadSafeDatastore { return m.D } func (m *Mock) Close() error { return errTODO } + +func (m *Mock) SetAPIAddr(addr string) error { return errTODO } diff --git a/repo/repo.go b/repo/repo.go index d913c2b68..2536bc15a 100644 --- a/repo/repo.go +++ b/repo/repo.go @@ -1,12 +1,18 @@ package repo import ( + "errors" "io" datastore "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-datastore" + config "github.com/ipfs/go-ipfs/repo/config" ) +var ( + ErrApiNotRunning = errors.New("api not running") +) + type Repo interface { Config() *config.Config SetConfig(*config.Config) error @@ -16,5 +22,8 @@ type Repo interface { Datastore() datastore.ThreadSafeDatastore + // SetAPIAddr sets the API address in the repo. + SetAPIAddr(addr string) error + io.Closer }