Skip to content

Commit

Permalink
Add auth support for HTTP channels
Browse files Browse the repository at this point in the history
  • Loading branch information
rojer committed Sep 19, 2021
1 parent 941bef0 commit 964f5f1
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 64 deletions.
5 changes: 3 additions & 2 deletions cli/dev/dev_conn_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
6 changes: 6 additions & 0 deletions cli/devutil/devutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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),
Expand Down
7 changes: 4 additions & 3 deletions cli/rpccreds/rpc_creds.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,22 @@ 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)
}
}

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")
}
}
1 change: 1 addition & 0 deletions common/mgrpc/codec/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Codec interface {
type Options struct {
AzureDM AzureDMCodecOptions
GCP GCPCodecOptions
HTTPOut OutboundHTTPCodecOptions
MQTT MQTTCodecOptions
Serial SerialCodecOptions
UDP UDPCodecOptions
Expand Down
141 changes: 126 additions & 15 deletions common/mgrpc/codec/http_out.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -67,44 +87,101 @@ 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
if err := json.NewDecoder(resp.Body).Decode(&rfs); err != nil {
// 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
}

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():
Expand Down Expand Up @@ -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
}
51 changes: 7 additions & 44 deletions common/mgrpc/mgrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

0 comments on commit 964f5f1

Please sign in to comment.