From 964f5f112956f30e9e0d5713f38cf114b64f594c Mon Sep 17 00:00:00 2001 From: "Deomid \"rojer\" Ryabkov" Date: Sun, 19 Sep 2021 23:49:35 +0100 Subject: [PATCH] Add auth support for HTTP channels --- cli/dev/dev_conn_impl.go | 5 +- cli/devutil/devutil.go | 6 ++ cli/rpccreds/rpc_creds.go | 7 +- common/mgrpc/codec/codec.go | 1 + common/mgrpc/codec/http_out.go | 141 +++++++++++++++++++++++++++++---- common/mgrpc/mgrpc.go | 51 ++---------- 6 files changed, 147 insertions(+), 64 deletions(-) diff --git a/cli/dev/dev_conn_impl.go b/cli/dev/dev_conn_impl.go index e3b567f4..dec05185 100644 --- a/cli/dev/dev_conn_impl.go +++ b/cli/dev/dev_conn_impl.go @@ -23,12 +23,13 @@ import ( "time" "github.com/juju/errors" + flag "github.com/spf13/pflag" + glog "k8s.io/klog/v2" + "github.com/mongoose-os/mos/cli/rpccreds" "github.com/mongoose-os/mos/common/mgrpc" "github.com/mongoose-os/mos/common/mgrpc/codec" "github.com/mongoose-os/mos/common/mgrpc/frame" - flag "github.com/spf13/pflag" - glog "k8s.io/klog/v2" ) const ( diff --git a/cli/devutil/devutil.go b/cli/devutil/devutil.go index 6cc4b128..c13c4e13 100644 --- a/cli/devutil/devutil.go +++ b/cli/devutil/devutil.go @@ -24,9 +24,12 @@ import ( "time" "github.com/juju/errors" + "github.com/mongoose-os/mos/common/mgrpc/codec" + "github.com/mongoose-os/mos/cli/dev" "github.com/mongoose-os/mos/cli/flags" + "github.com/mongoose-os/mos/cli/rpccreds" "github.com/mongoose-os/mos/cli/watson" ) @@ -62,6 +65,9 @@ func createDevConnWithJunkHandler(ctx context.Context, junkHandler func(junk []b GCP: codec.GCPCodecOptions{ CreateTopic: *flags.GCPRPCCreateTopic, }, + HTTPOut: codec.OutboundHTTPCodecOptions{ + GetCredsCallback: rpccreds.GetRPCCreds, + }, MQTT: codec.MQTTCodecOptions{}, Serial: codec.SerialCodecOptions{ BaudRate: uint(*flags.BaudRate), diff --git a/cli/rpccreds/rpc_creds.go b/cli/rpccreds/rpc_creds.go index 3cdd0e97..f8d3b72c 100644 --- a/cli/rpccreds/rpc_creds.go +++ b/cli/rpccreds/rpc_creds.go @@ -35,7 +35,6 @@ func GetRPCCreds() (username, passwd string, err error) { if err != nil { return "", "", errors.Annotatef(err, "reading RPC creds file %s", filename) } - return getRPCCredsFromString(strings.TrimSpace(string(data))) } else { return getRPCCredsFromString(*rpcCreds) @@ -43,13 +42,15 @@ func GetRPCCreds() (username, passwd string, err error) { } func getRPCCredsFromString(s string) (username, passwd string, err error) { + if len(s) == 0 { + return "", "", errors.New("credentials required but none provided (use --rpc-creds)") + } parts := strings.Split(s, ":") if len(parts) == 2 { return parts[0], parts[1], nil } else { // TODO(dfrank): handle the case with nothing or only username provided, // and prompt the user for the missing parts. - - return "", "", errors.Errorf("Failed to get username and password: wrong RPC creds spec") + return "", "", errors.Errorf("wrong RPC creds spec format") } } diff --git a/common/mgrpc/codec/codec.go b/common/mgrpc/codec/codec.go index cf876e15..b28b6965 100644 --- a/common/mgrpc/codec/codec.go +++ b/common/mgrpc/codec/codec.go @@ -49,6 +49,7 @@ type Codec interface { type Options struct { AzureDM AzureDMCodecOptions GCP GCPCodecOptions + HTTPOut OutboundHTTPCodecOptions MQTT MQTTCodecOptions Serial SerialCodecOptions UDP UDPCodecOptions diff --git a/common/mgrpc/codec/http_out.go b/common/mgrpc/codec/http_out.go index 89d284eb..cc566589 100644 --- a/common/mgrpc/codec/http_out.go +++ b/common/mgrpc/codec/http_out.go @@ -19,20 +19,34 @@ package codec import ( "bytes" "context" + "crypto/md5" + "crypto/rand" + "crypto/sha256" "crypto/tls" + "encoding/hex" "encoding/json" "fmt" "io" + "math/big" "net/http" + "regexp" + "strings" "sync" + "time" "github.com/juju/errors" "github.com/mongoose-os/mos/common/mgrpc/frame" glog "k8s.io/klog/v2" ) +type OutboundHTTPCodecOptions struct { + GetCredsCallback func() (username, passwd string, err error) +} + type outboundHttpCodec struct { - sync.Mutex + mu sync.Mutex + tlsConfig *tls.Config + opts OutboundHTTPCodecOptions closeNotifier chan struct{} closeOnce sync.Once url string @@ -43,14 +57,20 @@ type outboundHttpCodec struct { // OutboundHTTP sends outbound frames in HTTP POST requests and // returns replies with Recv. -func OutboundHTTP(url string, tlsConfig *tls.Config) Codec { - r := &outboundHttpCodec{ +func OutboundHTTP(url string, tlsConfig *tls.Config, opts OutboundHTTPCodecOptions) Codec { + c := &outboundHttpCodec{ + tlsConfig: tlsConfig, + opts: opts, closeNotifier: make(chan struct{}), url: url, - client: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}, } - r.cond = sync.NewCond(r) - return r + c.cond = sync.NewCond(&c.mu) + c.createClient() + return c +} + +func (c *outboundHttpCodec) createClient() { + c.client = &http.Client{Transport: &http.Transport{TLSClientConfig: c.tlsConfig}} } func (c *outboundHttpCodec) String() string { @@ -67,14 +87,71 @@ func (c *outboundHttpCodec) Send(ctx context.Context, f *frame.Frame) error { if err != nil { return errors.Trace(err) } + return c.sendHTTPRequest(ctx, "", b) +} + +func (c *outboundHttpCodec) sendHTTPRequest(ctx context.Context, authHeader string, b []byte) error { glog.V(2).Infof("Sending to %q over HTTP POST: %q", c.url, string(b)) - // TODO(imax): use http.Client to set the timeout. - resp, err := c.client.Post(c.url, "application/json", bytes.NewReader(b)) + if d, ok := ctx.Deadline(); ok { + c.client.Timeout = time.Until(d) + } + req, err := http.NewRequest("POST", c.url, bytes.NewReader(b)) + if err != nil { + return errors.Annotatef(err, "failed to create request") + } + req.Header.Add("Content-Type", "application/json") + if authHeader != "" { + glog.V(2).Infof("Authorization: %s", authHeader) + req.Header.Add("Authorization", authHeader) + } + resp, err := c.client.Do(req) if err != nil { return errors.Trace(err) } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + switch resp.StatusCode { + case http.StatusOK: + case http.StatusUnauthorized: + if authHeader != "" { + return errors.New("authentication failed") + } + ah := resp.Header.Get("WWW-Authenticate") + if len(ah) == 0 { + return errors.New("no www-authentiocate header") + } + authMethod := regexp.MustCompile(`^(\S+)`).FindString(ah) + if strings.ToLower(authMethod) != "digest" { + return fmt.Errorf("invalid auth method %q", authMethod) + } + pp := make(map[string]string) + for _, m := range regexp.MustCompile(`(\w+)="([^"]*?)"|(\w+)=([^\s"]+)`).FindAllStringSubmatch(ah, -1) { + pp[strings.ToLower(m[1])] = m[2] + pp[strings.ToLower(m[3])] = m[4] + } + glog.Infof("%+v", pp) + if c.opts.GetCredsCallback == nil { + return errors.New("authorization required but no credentials callback provided") + } + username, passwd, err := c.opts.GetCredsCallback() + if err != nil { + return errors.Annotatef(err, "error getting credentials") + } + authAlgo := pp["algorithm"] + if authAlgo == "" { + authAlgo = "MD5" + } + nonce := pp["nonce"] + cnonce, authResp, err := MkDigestResp("POST", req.URL.Path, username, pp["realm"], passwd, pp["algorithm"], nonce, "00000001", pp["qop"]) + authHeader = fmt.Sprintf( + `%s username="%s", realm="%s", uri="%s", algorithm=%s, nonce="%s", nc=%08x, cnonce="%d", qop=%s, response="%s"`, + authMethod, username, pp["realm"], req.URL.Path, authAlgo, nonce, 1, cnonce, pp["qop"], authResp, + ) + if pp["opaque"] != "" { + authHeader = fmt.Sprintf(`%s, opaque="%s"`, authHeader, pp["opaque"]) + } + c.createClient() + return c.sendHTTPRequest(ctx, authHeader, b) + default: return fmt.Errorf("server returned an error: %v", resp) } var rfs *frame.Frame @@ -82,9 +159,9 @@ func (c *outboundHttpCodec) Send(ctx context.Context, f *frame.Frame) error { // Return it from Recv? return errors.Trace(err) } - c.Lock() + c.mu.Lock() c.queue = append(c.queue, rfs) - c.Unlock() + c.mu.Unlock() c.cond.Signal() return nil } @@ -92,19 +169,19 @@ func (c *outboundHttpCodec) Send(ctx context.Context, f *frame.Frame) error { func (c *outboundHttpCodec) Recv(ctx context.Context) (*frame.Frame, error) { // Check if there's anything left in the queue. var r *frame.Frame - c.Lock() + c.mu.Lock() if len(c.queue) > 0 { r, c.queue = c.queue[0], c.queue[1:] } - c.Unlock() + c.mu.Unlock() if r != nil { return r, nil } // Wait for stuff to arrive. ch := make(chan *frame.Frame, 1) go func(ctx context.Context) { - c.Lock() - defer c.Unlock() + c.mu.Lock() + defer c.mu.Unlock() for len(c.queue) == 0 { select { case <-ctx.Done(): @@ -144,3 +221,37 @@ func (c *outboundHttpCodec) Info() ConnectionInfo { func (c *outboundHttpCodec) SetOptions(opts *Options) error { return errors.NotImplementedf("SetOptions") } + +func MkDigestResp(method, uri, username, realm, passwd, algorithm, nonce, nc, qop string) (int, string, error) { + var hashFunc func(data []byte) []byte + switch algorithm { + case "": + fallthrough + case "MD5": + hashFunc = func(data []byte) []byte { + s := md5.Sum(data) + return s[:] + } + case "SHA-256": + hashFunc = func(data []byte) []byte { + s := sha256.Sum256(data) + return s[:] + } + default: + return 0, "", fmt.Errorf("unknown digest algorithm %q", algorithm) + } + + cnonceBig, err := rand.Int(rand.Reader, big.NewInt(0xffffffff)) + if err != nil { + return 0, "", errors.Annotatef(err, "generating cnonce") + } + cnonce := int(cnonceBig.Int64()) + + ha1 := hex.EncodeToString(hashFunc([]byte(fmt.Sprintf("%s:%s:%s", username, realm, passwd)))) + + ha2 := hex.EncodeToString(hashFunc([]byte(fmt.Sprintf("%s:%s", method, uri)))) + + resp := hex.EncodeToString(hashFunc([]byte(fmt.Sprintf("%s:%s:%s:%d:%s:%s", ha1, nonce, nc, cnonce, qop, ha2)))) + + return cnonce, resp, nil +} diff --git a/common/mgrpc/mgrpc.go b/common/mgrpc/mgrpc.go index 354c61f0..4be32576 100644 --- a/common/mgrpc/mgrpc.go +++ b/common/mgrpc/mgrpc.go @@ -18,16 +18,12 @@ package mgrpc import ( "context" - "crypto/md5" - "crypto/rand" - "crypto/sha256" "crypto/tls" - "encoding/hex" "encoding/json" "fmt" "io" - "math/big" "net" + "strconv" "sync" "time" @@ -80,7 +76,6 @@ type req struct { type authErrorMsg struct { AuthType string `json:"auth_type"` Nonce int `json:"nonce"` - NC int `json:"nc"` Realm string `json:"realm"` Algorithm string `json:"algorithm,omitempty"` Opaque string `json:"opaque,omitempty"` @@ -242,7 +237,10 @@ func (r *mgRPCImpl) connect(ctx context.Context, opts ...ConnectOption) error { switch r.opts.proto { case tHTTP_POST: - r.codec = codec.OutboundHTTP(r.opts.connectAddress, r.opts.tlsConfig) + r.codec = codec.OutboundHTTP( + r.opts.connectAddress, + r.opts.tlsConfig, + r.opts.codecOptions.HTTPOut) case tWebSocket: r.codec = codec.NewReconnectWrapperCodec( r.opts.connectAddress, @@ -419,18 +417,10 @@ func (r *mgRPCImpl) Call( return nil, errors.Trace(err) } - // Generate cnonce - cnonceBig, err := rand.Int(rand.Reader, big.NewInt(0xffffffff)) - if err != nil { - return nil, errors.Annotatef(err, "generating cnonce") - } - - cnonce := int(cnonceBig.Int64()) - // Compute resp - resp, err := mkDigestResp( + cnonce, resp, err := codec.MkDigestResp( "dummy_method", "dummy_uri", username, authMsg.Realm, passwd, - authMsg.Algorithm, authMsg.Nonce, authMsg.NC, cnonce, "auth", + authMsg.Algorithm, strconv.Itoa(authMsg.Nonce), "1", "auth", ) if err != nil { return nil, errors.Trace(err) @@ -486,30 +476,3 @@ func (r *mgRPCImpl) IsConnected() bool { func (r *mgRPCImpl) SetCodecOptions(opts *codec.Options) error { return r.codec.SetOptions(opts) } - -func mkDigestResp(method, uri, username, realm, passwd, algorithm string, nonce, nc, cnonce int, qop string) (string, error) { - - hashFunc := func(data []byte) []byte { - s := md5.Sum(data) - return s[:] - } - switch algorithm { - case "": - case "MD5": - case "SHA-256": - hashFunc = func(data []byte) []byte { - s := sha256.Sum256(data) - return s[:] - } - default: - return "", fmt.Errorf("unknown digest algorithm %q", algorithm) - } - - ha1 := hex.EncodeToString(hashFunc([]byte(fmt.Sprintf("%s:%s:%s", username, realm, passwd)))) - - ha2 := hex.EncodeToString(hashFunc([]byte(fmt.Sprintf("%s:%s", method, uri)))) - - resp := hex.EncodeToString(hashFunc([]byte(fmt.Sprintf("%s:%d:%d:%d:%s:%s", ha1, nonce, nc, cnonce, qop, ha2)))) - - return resp, nil -}