diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go index 3b983696d..ba989ff2f 100644 --- a/cmd/lncli/main.go +++ b/cmd/lncli/main.go @@ -24,6 +24,7 @@ import ( "golang.org/x/term" "google.golang.org/grpc" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" ) const ( @@ -109,6 +110,12 @@ func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn { // Create a dial options array. opts := []grpc.DialOption{ grpc.WithTransportCredentials(creds), + grpc.WithUnaryInterceptor( + addMetadataUnaryInterceptor(profile.Metadata), + ), + grpc.WithStreamInterceptor( + addMetaDataStreamInterceptor(profile.Metadata), + ), } // Only process macaroon credentials if --no-macaroons isn't set and @@ -141,16 +148,17 @@ func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn { } macConstraints := []macaroons.Constraint{ - // We add a time-based constraint to prevent replay of the - // macaroon. It's good for 60 seconds by default to make up for - // any discrepancy between client and server clocks, but leaking - // the macaroon before it becomes invalid makes it possible for - // an attacker to reuse the macaroon. In addition, the validity - // time of the macaroon is extended by the time the server clock - // is behind the client clock, or shortened by the time the + // We add a time-based constraint to prevent replay of + // the macaroon. It's good for 60 seconds by default to + // make up for any discrepancy between client and server + // clocks, but leaking the macaroon before it becomes + // invalid makes it possible for an attacker to reuse + // the macaroon. In addition, the validity time of the + // macaroon is extended by the time the server clock is + // behind the client clock, or shortened by the time the // server clock is ahead of the client clock (or invalid - // altogether if, in the latter case, this time is more than 60 - // seconds). + // altogether if, in the latter case, this time is more + // than 60 seconds). // TODO(aakselrod): add better anti-replay protection. macaroons.TimeoutConstraint(profile.Macaroons.Timeout), @@ -180,7 +188,9 @@ func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn { // to connect to the grpc server. if ctx.GlobalIsSet("socksproxy") { socksProxy := ctx.GlobalString("socksproxy") - torDialer := func(_ context.Context, addr string) (net.Conn, error) { + torDialer := func(_ context.Context, addr string) (net.Conn, + error) { + return tor.Dial( addr, socksProxy, false, false, tor.DefaultConnTimeout, @@ -204,6 +214,49 @@ func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn { return conn } +// addMetadataUnaryInterceptor returns a grpc client side interceptor that +// appends any key-value metadata strings to the outgoing context of a grpc +// unary call. +func addMetadataUnaryInterceptor( + md map[string]string) grpc.UnaryClientInterceptor { + + return func(ctx context.Context, method string, req, reply interface{}, + cc *grpc.ClientConn, invoker grpc.UnaryInvoker, + opts ...grpc.CallOption) error { + + outCtx := contextWithMetadata(ctx, md) + return invoker(outCtx, method, req, reply, cc, opts...) + } +} + +// addMetaDataStreamInterceptor returns a grpc client side interceptor that +// appends any key-value metadata strings to the outgoing context of a grpc +// stream call. +func addMetaDataStreamInterceptor( + md map[string]string) grpc.StreamClientInterceptor { + + return func(ctx context.Context, desc *grpc.StreamDesc, + cc *grpc.ClientConn, method string, streamer grpc.Streamer, + opts ...grpc.CallOption) (grpc.ClientStream, error) { + + outCtx := contextWithMetadata(ctx, md) + return streamer(outCtx, desc, cc, method, opts...) + } +} + +// contextWithMetaData appends the given metadata key-value pairs to the given +// context. +func contextWithMetadata(ctx context.Context, + md map[string]string) context.Context { + + kvPairs := make([]string, 0, 2*len(md)) + for k, v := range md { + kvPairs = append(kvPairs, k, v) + } + + return metadata.AppendToOutgoingContext(ctx, kvPairs...) +} + // extractPathArgs parses the TLS certificate and macaroon paths from the // command. func extractPathArgs(ctx *cli.Context) (string, string, error) { @@ -349,6 +402,14 @@ func main() { "macaroon jar instead of the default one. " + "Can only be used if profiles are defined.", }, + cli.StringSliceFlag{ + Name: "metadata", + Usage: "This flag can be used to specify a key-value " + + "pair that should be appended to the " + + "outgoing context before the request is sent " + + "to lnd. This flag may be specified multiple " + + "times. The format is: \"key:value\".", + }, } app.Commands = []cli.Command{ createCommand, diff --git a/cmd/lncli/profile.go b/cmd/lncli/profile.go index 2f2819df6..5e611af88 100644 --- a/cmd/lncli/profile.go +++ b/cmd/lncli/profile.go @@ -24,14 +24,15 @@ var ( // profileEntry is a struct that represents all settings for one specific // profile. type profileEntry struct { - Name string `json:"name"` - RPCServer string `json:"rpcserver"` - LndDir string `json:"lnddir"` - Chain string `json:"chain"` - Network string `json:"network"` - NoMacaroons bool `json:"no-macaroons,omitempty"` // nolint:tagliatelle - TLSCert string `json:"tlscert"` - Macaroons *macaroonJar `json:"macaroons"` + Name string `json:"name"` + RPCServer string `json:"rpcserver"` + LndDir string `json:"lnddir"` + Chain string `json:"chain"` + Network string `json:"network"` + NoMacaroons bool `json:"no-macaroons,omitempty"` // nolint:tagliatelle + TLSCert string `json:"tlscert"` + Macaroons *macaroonJar `json:"macaroons"` + Metadata map[string]string `json:"metadata,omitempty"` } // cert returns the profile's TLS certificate as a x509 certificate pool. @@ -128,17 +129,32 @@ func profileFromContext(ctx *cli.Context, store, skipMacaroons bool) ( var err error tlsCert, err = ioutil.ReadFile(tlsCertPath) if err != nil { - return nil, fmt.Errorf("could not load TLS cert file: %v", err) + return nil, fmt.Errorf("could not load TLS cert "+ + "file: %v", err) } } + metadata := make(map[string]string) + for _, m := range ctx.GlobalStringSlice("metadata") { + pair := strings.Split(m, ":") + if len(pair) != 2 { + return nil, fmt.Errorf("invalid format for metadata " + + "flag; expected \"key:value\"") + } + + metadata[pair[0]] = pair[1] + } + entry := &profileEntry{ - RPCServer: ctx.GlobalString("rpcserver"), - LndDir: lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir")), + RPCServer: ctx.GlobalString("rpcserver"), + LndDir: lncfg.CleanAndExpandPath( + ctx.GlobalString("lnddir"), + ), Chain: ctx.GlobalString("chain"), Network: ctx.GlobalString("network"), NoMacaroons: ctx.GlobalBool("no-macaroons"), TLSCert: string(tlsCert), + Metadata: metadata, } // If we aren't using macaroons in general (flag --no-macaroons) or