github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/juju/controller/register.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package controller
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"crypto/rand"
    10  	"encoding/asn1"
    11  	"encoding/base64"
    12  	"encoding/json"
    13  	"fmt"
    14  	"io"
    15  	"net/http"
    16  	"os"
    17  	"strings"
    18  
    19  	"github.com/juju/cmd"
    20  	"github.com/juju/errors"
    21  	"github.com/juju/utils"
    22  	"github.com/juju/utils/set"
    23  	"golang.org/x/crypto/nacl/secretbox"
    24  	"golang.org/x/crypto/ssh/terminal"
    25  	"gopkg.in/juju/names.v2"
    26  
    27  	"github.com/juju/juju/api"
    28  	"github.com/juju/juju/api/base"
    29  	"github.com/juju/juju/api/modelmanager"
    30  	"github.com/juju/juju/apiserver/params"
    31  	"github.com/juju/juju/cmd/juju/common"
    32  	"github.com/juju/juju/cmd/modelcmd"
    33  	"github.com/juju/juju/jujuclient"
    34  	"github.com/juju/juju/permission"
    35  )
    36  
    37  var noModelsMessage = `
    38  There are no models available. You can add models with
    39  "juju add-model", or you can ask an administrator or owner
    40  of a model to grant access to that model with "juju grant".
    41  `
    42  
    43  // NewRegisterCommand returns a command to allow the user to register a controller.
    44  func NewRegisterCommand() cmd.Command {
    45  	c := &registerCommand{}
    46  	c.apiOpen = c.APIOpen
    47  	c.listModelsFunc = c.listModels
    48  	c.store = jujuclient.NewFileClientStore()
    49  	return modelcmd.WrapBase(c)
    50  }
    51  
    52  // registerCommand logs in to a Juju controller and caches the connection
    53  // information.
    54  type registerCommand struct {
    55  	modelcmd.JujuCommandBase
    56  	apiOpen        api.OpenFunc
    57  	listModelsFunc func(_ jujuclient.ClientStore, controller, user string) ([]base.UserModel, error)
    58  	store          jujuclient.ClientStore
    59  	Arg            string
    60  
    61  	// onRunError is executed if non-nil if there is an error at the end
    62  	// of the Run method.
    63  	onRunError func()
    64  }
    65  
    66  var usageRegisterSummary = `
    67  Registers a controller.`[1:]
    68  
    69  var usageRegisterDetails = `
    70  The register command adds details of a controller to the local system.
    71  This is done either by completing the user registration process that
    72  began with the 'juju add-user' command, or by providing the DNS host
    73  name of a public controller.
    74  
    75  To complete the user registration process, you should have been provided
    76  with a base64-encoded blob of data (the output of 'juju add-user')
    77  which can be copied and pasted as the <string> argument to 'register'.
    78  You will be prompted for a password, which, once set, causes the
    79  registration string to be voided. In order to start using Juju the user
    80  can now either add a model or wait for a model to be shared with them.
    81  Some machine providers will require the user to be in possession of
    82  certain credentials in order to add a model.
    83  
    84  When adding a controller at a public address, authentication via some
    85  external third party (for example Ubuntu SSO) will be required, usually
    86  by using a web browser.
    87  
    88  Examples:
    89  
    90      juju register MFATA3JvZDAnExMxMDQuMTU0LjQyLjQ0OjE3MDcwExAxMC4xMjguMC4yOjE3MDcwBCBEFCaXerhNImkKKabuX5ULWf2Bp4AzPNJEbXVWgraLrAA=
    91  
    92      juju register public-controller.example.com
    93  
    94  See also: 
    95      add-user
    96      change-user-password
    97      unregister`
    98  
    99  // Info implements Command.Info
   100  // `register` may seem generic, but is seen as simple and without potential
   101  // naming collisions in any current or planned features.
   102  func (c *registerCommand) Info() *cmd.Info {
   103  	return &cmd.Info{
   104  		Name:    "register",
   105  		Args:    "<registration string>|<controller host name>",
   106  		Purpose: usageRegisterSummary,
   107  		Doc:     usageRegisterDetails,
   108  	}
   109  }
   110  
   111  // SetFlags implements Command.Init.
   112  func (c *registerCommand) Init(args []string) error {
   113  	if len(args) < 1 {
   114  		return errors.New("registration data missing")
   115  	}
   116  	c.Arg, args = args[0], args[1:]
   117  	if err := cmd.CheckEmpty(args); err != nil {
   118  		return errors.Trace(err)
   119  	}
   120  	return nil
   121  }
   122  
   123  // Run implements Command.Run.
   124  func (c *registerCommand) Run(ctx *cmd.Context) error {
   125  	err := c.run(ctx)
   126  	if err != nil && c.onRunError != nil {
   127  		c.onRunError()
   128  	}
   129  	return err
   130  }
   131  
   132  func (c *registerCommand) run(ctx *cmd.Context) error {
   133  	store := modelcmd.QualifyingClientStore{c.store}
   134  	registrationParams, err := c.getParameters(ctx, store)
   135  	if err != nil {
   136  		return errors.Trace(err)
   137  	}
   138  	controllerDetails, accountDetails, err := c.controllerDetails(ctx, registrationParams)
   139  	if err != nil {
   140  		return errors.Trace(err)
   141  	}
   142  	controllerName, err := c.updateController(
   143  		ctx,
   144  		store,
   145  		registrationParams.defaultControllerName,
   146  		controllerDetails,
   147  		accountDetails,
   148  	)
   149  	if err != nil {
   150  		return errors.Trace(err)
   151  	}
   152  	// Log into the controller to verify the credentials, and
   153  	// list the models available.
   154  	models, err := c.listModelsFunc(store, controllerName, accountDetails.User)
   155  	if err != nil {
   156  		return errors.Trace(err)
   157  	}
   158  	for _, model := range models {
   159  		owner := names.NewUserTag(model.Owner)
   160  		if err := store.UpdateModel(
   161  			controllerName,
   162  			jujuclient.JoinOwnerModelName(owner, model.Name),
   163  			jujuclient.ModelDetails{model.UUID},
   164  		); err != nil {
   165  			return errors.Annotate(err, "storing model details")
   166  		}
   167  	}
   168  	if err := store.SetCurrentController(controllerName); err != nil {
   169  		return errors.Trace(err)
   170  	}
   171  
   172  	fmt.Fprintf(
   173  		ctx.Stderr, "\nWelcome, %s. You are now logged into %q.\n",
   174  		friendlyUserName(accountDetails.User), controllerName,
   175  	)
   176  	return c.maybeSetCurrentModel(ctx, store, controllerName, accountDetails.User, models)
   177  }
   178  
   179  func friendlyUserName(user string) string {
   180  	u := names.NewUserTag(user)
   181  	if u.IsLocal() {
   182  		return u.Name()
   183  	}
   184  	return u.Id()
   185  }
   186  
   187  // controllerDetails returns controller and account details to be registered for the
   188  // given registration parameters.
   189  func (c *registerCommand) controllerDetails(ctx *cmd.Context, p *registrationParams) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) {
   190  	if p.publicHost != "" {
   191  		return c.publicControllerDetails(p.publicHost)
   192  	}
   193  	return c.nonPublicControllerDetails(ctx, p)
   194  }
   195  
   196  // publicControllerDetails returns controller and account details to be registered
   197  // for the given public controller host name.
   198  func (c *registerCommand) publicControllerDetails(host string) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) {
   199  	errRet := func(err error) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) {
   200  		return jujuclient.ControllerDetails{}, jujuclient.AccountDetails{}, err
   201  	}
   202  	apiAddr := host
   203  	if !strings.Contains(apiAddr, ":") {
   204  		apiAddr += ":443"
   205  	}
   206  	// Make a direct API connection because we don't yet know the
   207  	// controller UUID so can't store the thus-incomplete controller
   208  	// details to make a conventional connection.
   209  	//
   210  	// Unfortunately this means we'll connect twice to the controller
   211  	// but it's probably best to go through the conventional path the
   212  	// second time.
   213  	bclient, err := c.BakeryClient()
   214  	if err != nil {
   215  		return errRet(errors.Trace(err))
   216  	}
   217  	dialOpts := api.DefaultDialOpts()
   218  	dialOpts.BakeryClient = bclient
   219  	conn, err := c.apiOpen(&api.Info{
   220  		Addrs: []string{apiAddr},
   221  	}, dialOpts)
   222  	if err != nil {
   223  		return errRet(errors.Trace(err))
   224  	}
   225  	defer conn.Close()
   226  	user, ok := conn.AuthTag().(names.UserTag)
   227  	if !ok {
   228  		return errRet(errors.Errorf("logged in as %v, not a user", conn.AuthTag()))
   229  	}
   230  	// If we get to here, then we have a cached macaroon for the registered
   231  	// user. If we encounter an error after here, we need to clear it.
   232  	c.onRunError = func() {
   233  		if err := c.ClearControllerMacaroons([]string{apiAddr}); err != nil {
   234  			logger.Errorf("failed to clear macaroon: %v", err)
   235  		}
   236  	}
   237  	return jujuclient.ControllerDetails{
   238  			APIEndpoints:   []string{apiAddr},
   239  			ControllerUUID: conn.ControllerTag().Id(),
   240  		}, jujuclient.AccountDetails{
   241  			User:            user.Id(),
   242  			LastKnownAccess: conn.ControllerAccess(),
   243  		}, nil
   244  }
   245  
   246  // nonPublicControllerDetails returns controller and account details to be registered with
   247  // respect to the given registration parameters.
   248  func (c *registerCommand) nonPublicControllerDetails(ctx *cmd.Context, registrationParams *registrationParams) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) {
   249  	errRet := func(err error) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) {
   250  		return jujuclient.ControllerDetails{}, jujuclient.AccountDetails{}, err
   251  	}
   252  	// During registration we must set a new password. This has to be done
   253  	// atomically with the clearing of the secret key.
   254  	payloadBytes, err := json.Marshal(params.SecretKeyLoginRequestPayload{
   255  		registrationParams.newPassword,
   256  	})
   257  	if err != nil {
   258  		return errRet(errors.Trace(err))
   259  	}
   260  
   261  	// Make the registration call. If this is successful, the client's
   262  	// cookie jar will be populated with a macaroon that may be used
   263  	// to log in below without the user having to type in the password
   264  	// again.
   265  	req := params.SecretKeyLoginRequest{
   266  		Nonce: registrationParams.nonce[:],
   267  		User:  registrationParams.userTag.String(),
   268  		PayloadCiphertext: secretbox.Seal(
   269  			nil, payloadBytes,
   270  			&registrationParams.nonce,
   271  			&registrationParams.key,
   272  		),
   273  	}
   274  	resp, err := c.secretKeyLogin(registrationParams.controllerAddrs, req)
   275  	if err != nil {
   276  		return errRet(errors.Trace(err))
   277  	}
   278  
   279  	// Decrypt the response to authenticate the controller and
   280  	// obtain its CA certificate.
   281  	if len(resp.Nonce) != len(registrationParams.nonce) {
   282  		return errRet(errors.NotValidf("response nonce"))
   283  	}
   284  	var respNonce [24]byte
   285  	copy(respNonce[:], resp.Nonce)
   286  	payloadBytes, ok := secretbox.Open(nil, resp.PayloadCiphertext, &respNonce, &registrationParams.key)
   287  	if !ok {
   288  		return errRet(errors.NotValidf("response payload"))
   289  	}
   290  	var responsePayload params.SecretKeyLoginResponsePayload
   291  	if err := json.Unmarshal(payloadBytes, &responsePayload); err != nil {
   292  		return errRet(errors.Annotate(err, "unmarshalling response payload"))
   293  	}
   294  	user := registrationParams.userTag.Id()
   295  	ctx.Infof("Initial password successfully set for %s.", friendlyUserName(user))
   296  	// If we get to here, then we have a cached macaroon for the registered
   297  	// user. If we encounter an error after here, we need to clear it.
   298  	c.onRunError = func() {
   299  		if err := c.ClearControllerMacaroons(registrationParams.controllerAddrs); err != nil {
   300  			logger.Errorf("failed to clear macaroon: %v", err)
   301  		}
   302  	}
   303  	return jujuclient.ControllerDetails{
   304  			APIEndpoints:   registrationParams.controllerAddrs,
   305  			ControllerUUID: responsePayload.ControllerUUID,
   306  			CACert:         responsePayload.CACert,
   307  		}, jujuclient.AccountDetails{
   308  			User:            user,
   309  			LastKnownAccess: string(permission.LoginAccess),
   310  		}, nil
   311  }
   312  
   313  // updateController prompts for a controller name and updates the
   314  // controller and account details in the given client store.
   315  // It returns the name of the updated controller.
   316  func (c *registerCommand) updateController(
   317  	ctx *cmd.Context,
   318  	store jujuclient.ClientStore,
   319  	defaultControllerName string,
   320  	controllerDetails jujuclient.ControllerDetails,
   321  	accountDetails jujuclient.AccountDetails,
   322  ) (string, error) {
   323  	// Check that the same controller isn't already stored, so that we
   324  	// can avoid needlessly asking for a controller name in that case.
   325  	all, err := store.AllControllers()
   326  	if err != nil {
   327  		return "", errors.Trace(err)
   328  	}
   329  	for name, ctl := range all {
   330  		if ctl.ControllerUUID == controllerDetails.ControllerUUID {
   331  			// TODO(rogpeppe) lp#1614010 Succeed but override the account details in this case?
   332  			return "", errors.Errorf("controller is already registered as %q", name)
   333  		}
   334  	}
   335  	controllerName, err := c.promptControllerName(store, defaultControllerName, ctx.Stderr, ctx.Stdin)
   336  	if err != nil {
   337  		return "", errors.Trace(err)
   338  	}
   339  
   340  	if err := store.AddController(controllerName, controllerDetails); err != nil {
   341  		return "", errors.Trace(err)
   342  	}
   343  	if err := store.UpdateAccount(controllerName, accountDetails); err != nil {
   344  		return "", errors.Annotatef(err, "cannot update account information: %v", err)
   345  	}
   346  	return controllerName, nil
   347  }
   348  
   349  func (c *registerCommand) listModels(store jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) {
   350  	api, err := c.NewAPIRoot(store, controllerName, "")
   351  	if err != nil {
   352  		return nil, errors.Trace(err)
   353  	}
   354  	defer api.Close()
   355  	mm := modelmanager.NewClient(api)
   356  	return mm.ListModels(userName)
   357  }
   358  
   359  func (c *registerCommand) maybeSetCurrentModel(ctx *cmd.Context, store jujuclient.ClientStore, controllerName, userName string, models []base.UserModel) error {
   360  	if len(models) == 0 {
   361  		fmt.Fprint(ctx.Stderr, noModelsMessage)
   362  		return nil
   363  	}
   364  
   365  	// If we get to here, there is at least one model.
   366  	if len(models) == 1 {
   367  		// There is exactly one model shared,
   368  		// so set it as the current model.
   369  		model := models[0]
   370  		owner := names.NewUserTag(model.Owner)
   371  		modelName := jujuclient.JoinOwnerModelName(owner, model.Name)
   372  		err := store.SetCurrentModel(controllerName, modelName)
   373  		if err != nil {
   374  			return errors.Trace(err)
   375  		}
   376  		fmt.Fprintf(ctx.Stderr, "\nCurrent model set to %q.\n", modelName)
   377  		return nil
   378  	}
   379  	fmt.Fprintf(ctx.Stderr, `
   380  There are %d models available. Use "juju switch" to select
   381  one of them:
   382  `, len(models))
   383  	user := names.NewUserTag(userName)
   384  	ownerModelNames := make(set.Strings)
   385  	otherModelNames := make(set.Strings)
   386  	for _, model := range models {
   387  		if model.Owner == userName {
   388  			ownerModelNames.Add(model.Name)
   389  			continue
   390  		}
   391  		owner := names.NewUserTag(model.Owner)
   392  		modelName := common.OwnerQualifiedModelName(model.Name, owner, user)
   393  		otherModelNames.Add(modelName)
   394  	}
   395  	for _, modelName := range ownerModelNames.SortedValues() {
   396  		fmt.Fprintf(ctx.Stderr, "  - juju switch %s\n", modelName)
   397  	}
   398  	for _, modelName := range otherModelNames.SortedValues() {
   399  		fmt.Fprintf(ctx.Stderr, "  - juju switch %s\n", modelName)
   400  	}
   401  	return nil
   402  }
   403  
   404  type registrationParams struct {
   405  	// publicHost holds the host name of a public controller.
   406  	// If this is set, all other fields will be empty.
   407  	publicHost string
   408  
   409  	defaultControllerName string
   410  	userTag               names.UserTag
   411  	controllerAddrs       []string
   412  	key                   [32]byte
   413  	nonce                 [24]byte
   414  	newPassword           string
   415  }
   416  
   417  // getParameters gets all of the parameters required for registering, prompting
   418  // the user as necessary.
   419  func (c *registerCommand) getParameters(ctx *cmd.Context, store jujuclient.ClientStore) (*registrationParams, error) {
   420  	var params registrationParams
   421  	if strings.Contains(c.Arg, ".") || c.Arg == "localhost" {
   422  		// Looks like a host name - no URL-encoded base64 string should
   423  		// contain a dot and every public controller name should.
   424  		// Allow localhost for development purposes.
   425  		params.publicHost = c.Arg
   426  		// No need for password shenanigans if we're using a public controller.
   427  		return &params, nil
   428  	}
   429  	// Decode key, username, controller addresses from the string supplied
   430  	// on the command line.
   431  	decodedData, err := base64.URLEncoding.DecodeString(c.Arg)
   432  	if err != nil {
   433  		return nil, errors.Trace(err)
   434  	}
   435  	var info jujuclient.RegistrationInfo
   436  	if _, err := asn1.Unmarshal(decodedData, &info); err != nil {
   437  		return nil, errors.Trace(err)
   438  	}
   439  
   440  	params.controllerAddrs = info.Addrs
   441  	params.userTag = names.NewUserTag(info.User)
   442  	if len(info.SecretKey) != len(params.key) {
   443  		return nil, errors.NotValidf("secret key")
   444  	}
   445  	copy(params.key[:], info.SecretKey)
   446  	params.defaultControllerName = info.ControllerName
   447  
   448  	// Prompt the user for the new password to set.
   449  	newPassword, err := c.promptNewPassword(ctx.Stderr, ctx.Stdin)
   450  	if err != nil {
   451  		return nil, errors.Trace(err)
   452  	}
   453  	params.newPassword = newPassword
   454  
   455  	// Generate a random nonce for encrypting the request.
   456  	if _, err := rand.Read(params.nonce[:]); err != nil {
   457  		return nil, errors.Trace(err)
   458  	}
   459  
   460  	return &params, nil
   461  }
   462  
   463  func (c *registerCommand) secretKeyLogin(addrs []string, request params.SecretKeyLoginRequest) (*params.SecretKeyLoginResponse, error) {
   464  	apiContext, err := c.APIContext()
   465  	if err != nil {
   466  		return nil, errors.Annotate(err, "getting API context")
   467  	}
   468  
   469  	buf, err := json.Marshal(&request)
   470  	if err != nil {
   471  		return nil, errors.Annotate(err, "marshalling request")
   472  	}
   473  	r := bytes.NewReader(buf)
   474  
   475  	// Determine which address to use by attempting to open an API
   476  	// connection with each of the addresses. Note that we do not
   477  	// know the CA certificate yet, so we do not want to send any
   478  	// sensitive information. We make no attempt to log in until
   479  	// we can verify the server's identity.
   480  	opts := api.DefaultDialOpts()
   481  	opts.InsecureSkipVerify = true
   482  	conn, err := c.apiOpen(&api.Info{
   483  		Addrs:     addrs,
   484  		SkipLogin: true,
   485  		// NOTE(axw) CACert is required, but ignored if
   486  		// InsecureSkipVerify is set. We should try to
   487  		// bring together CACert and InsecureSkipVerify
   488  		// so they can be validated together.
   489  		CACert: "ignored",
   490  	}, opts)
   491  	if err != nil {
   492  		return nil, errors.Trace(err)
   493  	}
   494  	apiAddr := conn.Addr()
   495  	if err := conn.Close(); err != nil {
   496  		return nil, errors.Trace(err)
   497  	}
   498  
   499  	// Using the address we connected to above, perform the request.
   500  	// A success response will include a macaroon cookie that we can
   501  	// use to log in with.
   502  	urlString := fmt.Sprintf("https://%s/register", apiAddr)
   503  	httpReq, err := http.NewRequest("POST", urlString, r)
   504  	if err != nil {
   505  		return nil, errors.Trace(err)
   506  	}
   507  	httpReq.Header.Set("Content-Type", "application/json")
   508  	httpClient := utils.GetNonValidatingHTTPClient()
   509  	httpClient.Jar = apiContext.Jar
   510  	httpResp, err := httpClient.Do(httpReq)
   511  	if err != nil {
   512  		return nil, errors.Trace(err)
   513  	}
   514  	defer httpResp.Body.Close()
   515  
   516  	if httpResp.StatusCode != http.StatusOK {
   517  		var resp params.ErrorResult
   518  		if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
   519  			return nil, errors.Trace(err)
   520  		}
   521  		return nil, resp.Error
   522  	}
   523  
   524  	var resp params.SecretKeyLoginResponse
   525  	if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
   526  		return nil, errors.Annotatef(err, "cannot decode login response")
   527  	}
   528  	return &resp, nil
   529  }
   530  
   531  func (c *registerCommand) promptNewPassword(stderr io.Writer, stdin io.Reader) (string, error) {
   532  	password, err := c.readPassword("Enter a new password: ", stderr, stdin)
   533  	if err != nil {
   534  		return "", errors.Annotatef(err, "cannot read password")
   535  	}
   536  	if password == "" {
   537  		return "", errors.NewNotValid(nil, "you must specify a non-empty password")
   538  	}
   539  	passwordConfirmation, err := c.readPassword("Confirm password: ", stderr, stdin)
   540  	if err != nil {
   541  		return "", errors.Trace(err)
   542  	}
   543  	if password != passwordConfirmation {
   544  		return "", errors.Errorf("passwords do not match")
   545  	}
   546  	return password, nil
   547  }
   548  
   549  func (c *registerCommand) promptControllerName(store jujuclient.ClientStore, suggestedName string, stderr io.Writer, stdin io.Reader) (string, error) {
   550  	if suggestedName != "" {
   551  		if _, err := store.ControllerByName(suggestedName); err == nil {
   552  			suggestedName = ""
   553  		}
   554  	}
   555  	for {
   556  		var setMsg string
   557  		setMsg = "Enter a name for this controller: "
   558  		if suggestedName != "" {
   559  			setMsg = fmt.Sprintf("Enter a name for this controller [%s]: ", suggestedName)
   560  		}
   561  		fmt.Fprintf(stderr, setMsg)
   562  		name, err := c.readLine(stdin)
   563  		if err != nil {
   564  			return "", errors.Trace(err)
   565  		}
   566  		name = strings.TrimSpace(name)
   567  		if name == "" {
   568  			if suggestedName == "" {
   569  				fmt.Fprintln(stderr, "You must specify a non-empty controller name.")
   570  				continue
   571  			}
   572  			name = suggestedName
   573  		}
   574  		_, err = store.ControllerByName(name)
   575  		if err == nil {
   576  			fmt.Fprintf(stderr, "Controller %q already exists.\n", name)
   577  			continue
   578  		}
   579  		return name, nil
   580  	}
   581  }
   582  
   583  func (c *registerCommand) readPassword(prompt string, stderr io.Writer, stdin io.Reader) (string, error) {
   584  	fmt.Fprintf(stderr, "%s", prompt)
   585  	defer stderr.Write([]byte{'\n'})
   586  	if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) {
   587  		password, err := terminal.ReadPassword(int(f.Fd()))
   588  		if err != nil {
   589  			return "", errors.Trace(err)
   590  		}
   591  		return string(password), nil
   592  	}
   593  	return c.readLine(stdin)
   594  }
   595  
   596  func (c *registerCommand) readLine(stdin io.Reader) (string, error) {
   597  	// Read one byte at a time to avoid reading beyond the delimiter.
   598  	line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n')
   599  	if err != nil {
   600  		return "", errors.Trace(err)
   601  	}
   602  	return line[:len(line)-1], nil
   603  }
   604  
   605  type byteAtATimeReader struct {
   606  	io.Reader
   607  }
   608  
   609  func (r byteAtATimeReader) Read(out []byte) (int, error) {
   610  	return r.Reader.Read(out[:1])
   611  }