OSDN Git Service

Add rpc token authenticate function (#135)
authoryahtoo <yahtoo.ma@gmail.com>
Mon, 27 Nov 2017 08:45:24 +0000 (16:45 +0800)
committerPaladz <yzhu101@uottawa.ca>
Mon, 27 Nov 2017 08:45:24 +0000 (16:45 +0800)
net/http/authn/authn.go [new file with mode: 0644]
net/http/authn/authn_test.go [new file with mode: 0644]
net/http/authn/context.go [new file with mode: 0644]
node/node.go

diff --git a/net/http/authn/authn.go b/net/http/authn/authn.go
new file mode 100644 (file)
index 0000000..2bda775
--- /dev/null
@@ -0,0 +1,157 @@
+package authn
+
+import (
+       "context"
+       "crypto/x509"
+       "encoding/hex"
+       "net"
+       "net/http"
+       "strings"
+       "sync"
+       "time"
+
+       "github.com/bytom/blockchain/accesstoken"
+       "github.com/bytom/errors"
+)
+
+const tokenExpiry = time.Minute * 5
+
+var loopbackOn = true
+
+var (
+       //ErrInvalidToken is returned when authenticate is called with invalide token.
+       ErrInvalidToken = errors.New("invalid token")
+       //ErrNoToken is returned when authenticate is called with no token.
+       ErrNoToken = errors.New("no token")
+)
+
+//API describe the token authenticate.
+type API struct {
+       tokens             *accesstoken.CredentialStore
+       crosscoreRPCPrefix string
+       rootCAs            *x509.CertPool
+
+       tokenMu  sync.Mutex // protects the following
+       tokenMap map[string]tokenResult
+}
+
+type tokenResult struct {
+       valid      bool
+       lastLookup time.Time
+}
+
+//NewAPI create a token authenticate object.
+func NewAPI(tokens *accesstoken.CredentialStore) *API {
+       return &API{
+               tokens:   tokens,
+               tokenMap: make(map[string]tokenResult),
+       }
+}
+
+// Authenticate returns the request, with added tokens and/or localhost
+// flags in the context, as appropriate.
+func (a *API) Authenticate(req *http.Request) (*http.Request, error) {
+       ctx := req.Context()
+
+       token, err := a.tokenAuthn(req)
+       if err == nil && token != "" {
+               // if this request was successfully authenticated with a token, pass the token along
+               ctx = newContextWithToken(ctx, token)
+       }
+       local := a.localhostAuthn(req)
+       if local {
+               ctx = newContextWithLocalhost(ctx)
+       }
+       // Temporary workaround. Dashboard is always ok.
+       // See loopbackOn comment above.
+       if strings.HasPrefix(req.URL.Path, "/dashboard/") || req.URL.Path == "/dashboard" {
+               return req.WithContext(ctx), nil
+       }
+       if loopbackOn && local {
+               return req.WithContext(ctx), nil
+       }
+
+       return req.WithContext(ctx), err
+}
+
+// checks the request for a valid client cert list.
+// If found, it is added to the request's context.
+// Note that an *invalid* client cert is treated the
+// same as no client cert -- it is omitted from the
+// returned context, but the connection may proceed.
+func certAuthn(req *http.Request, rootCAs *x509.CertPool) context.Context {
+       if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
+               certs := req.TLS.PeerCertificates
+
+               // Same logic as serverHandshakeState.processCertsFromClient
+               // in $GOROOT/src/crypto/tls/handshake_server.go.
+               opts := x509.VerifyOptions{
+                       Roots:         rootCAs,
+                       CurrentTime:   time.Now(),
+                       Intermediates: x509.NewCertPool(),
+                       KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+               }
+               for _, cert := range certs[1:] {
+                       opts.Intermediates.AddCert(cert)
+               }
+
+               if _, err := certs[0].Verify(opts); err != nil {
+                       // crypto/tls treats this as an error:
+                       // errors.New("tls: failed to verify client's certificate: " + err.Error())
+                       // For us, it is ok; we want to treat it the same as if there
+                       // were no client cert presented.
+                       return req.Context()
+               }
+
+               return context.WithValue(req.Context(), x509CertsKey, certs)
+       }
+       return req.Context()
+}
+
+// returns true if this request is coming from a loopback address
+func (a *API) localhostAuthn(req *http.Request) bool {
+       h, _, err := net.SplitHostPort(req.RemoteAddr)
+       if err != nil {
+               return false
+       }
+       if !net.ParseIP(h).IsLoopback() {
+               return false
+       }
+       return true
+}
+
+func (a *API) tokenAuthn(req *http.Request) (string, error) {
+       user, pw, ok := req.BasicAuth()
+       if !ok {
+               return "", ErrNoToken
+       }
+       return user, a.cachedTokenAuthnCheck(req.Context(), user, pw)
+}
+
+func (a *API) tokenAuthnCheck(ctx context.Context, user, pw string) (bool, error) {
+       pwBytes, err := hex.DecodeString(pw)
+       if err != nil {
+               return false, nil
+       }
+       return a.tokens.Check(ctx, user, pwBytes)
+}
+
+func (a *API) cachedTokenAuthnCheck(ctx context.Context, user, pw string) error {
+       a.tokenMu.Lock()
+       res, ok := a.tokenMap[user+pw]
+       a.tokenMu.Unlock()
+       if !ok || time.Now().After(res.lastLookup.Add(tokenExpiry)) {
+               valid, err := a.tokenAuthnCheck(ctx, user, pw)
+               if err != nil {
+                       return errors.Wrap(err)
+               }
+               res = tokenResult{valid: valid, lastLookup: time.Now()}
+               a.tokenMu.Lock()
+               a.tokenMap[user+pw] = res
+               a.tokenMu.Unlock()
+       }
+       if !res.valid {
+               return ErrInvalidToken
+       }
+       return nil
+}
diff --git a/net/http/authn/authn_test.go b/net/http/authn/authn_test.go
new file mode 100644 (file)
index 0000000..e0e7d72
--- /dev/null
@@ -0,0 +1,56 @@
+package authn
+
+import (
+       "context"
+       "net/http"
+       "os"
+       "strings"
+       "testing"
+
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/blockchain/accesstoken"
+       "github.com/bytom/errors"
+)
+
+func TestAuthenticate(t *testing.T) {
+       ctx := context.Background()
+
+       var token *string
+       tokenDB := dbm.NewDB("testdb", "leveldb", "temp")
+       defer os.RemoveAll("temp")
+       accessTokens := accesstoken.NewStore(tokenDB)
+       token, err := accessTokens.Create(ctx, "alice", "test")
+       if err != nil {
+               t.Errorf("create token error")
+       }
+
+       cases := []struct {
+               id, tok string
+               want    error
+       }{
+               {"alice", *token, nil},
+               {"alice", "alice:abcsdsdfassdfsefsfsfesfesfefsefa", ErrInvalidToken},
+       }
+
+       api := NewAPI(accessTokens)
+
+       for _, c := range cases {
+               var username, password string
+               toks := strings.SplitN(c.tok, ":", 2)
+               if len(toks) > 0 {
+                       username = toks[0]
+               }
+               if len(toks) > 1 {
+                       password = toks[1]
+               }
+
+               req, _ := http.NewRequest("GET", "/", nil)
+               req.SetBasicAuth(username, password)
+
+               _, err := api.Authenticate(req)
+               if errors.Root(err) != c.want {
+                       t.Errorf("Authenticate(%s) error = %s want %s", c.id, err, c.want)
+               }
+       }
+}
diff --git a/net/http/authn/context.go b/net/http/authn/context.go
new file mode 100644 (file)
index 0000000..e2b1369
--- /dev/null
@@ -0,0 +1,49 @@
+package authn
+
+import (
+       "context"
+       "crypto/x509"
+)
+
+type key int
+
+const (
+       tokenKey key = iota
+       localhostKey
+       x509CertsKey
+)
+
+// X509Certs returns the cert stored in the context, if it exists.
+func X509Certs(ctx context.Context) []*x509.Certificate {
+       c, _ := ctx.Value(x509CertsKey).([]*x509.Certificate)
+       return c
+}
+
+// newContextWithToken sets the token in a new context and returns the context.
+func newContextWithToken(ctx context.Context, token string) context.Context {
+       return context.WithValue(ctx, tokenKey, token)
+}
+
+// Token returns the token stored in the context, if there is one.
+func Token(ctx context.Context) string {
+       t, ok := ctx.Value(tokenKey).(string)
+       if !ok {
+               return ""
+       }
+       return t
+}
+
+// newContextWithLocalhost sets the localhost flag to `true` in a new context
+// and returns that context.
+func newContextWithLocalhost(ctx context.Context) context.Context {
+       return context.WithValue(ctx, localhostKey, true)
+}
+
+// Localhost returns true if the localhost flag has been set.
+func Localhost(ctx context.Context) bool {
+       l, ok := ctx.Value(localhostKey).(bool)
+       if ok && l {
+               return true
+       }
+       return false
+}
index de4333d..f5546fa 100755 (executable)
@@ -28,6 +28,7 @@ import (
        cfg "github.com/bytom/config"
        "github.com/bytom/env"
        "github.com/bytom/errors"
+       "github.com/bytom/net/http/authn"
        "github.com/bytom/p2p"
        "github.com/bytom/protocol"
        "github.com/bytom/types"
@@ -86,7 +87,23 @@ func (wh *waitHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
        wh.h.ServeHTTP(w, req)
 }
 
-func rpcInit(h *bc.BlockchainReactor, config *cfg.Config) {
+func AuthHandler(handler http.Handler, accessTokens *accesstoken.CredentialStore) http.Handler {
+
+       authenticator := authn.NewAPI(accessTokens)
+
+       return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+               // TODO(tessr): check that this path exists; return early if this path isn't legit
+               req, err := authenticator.Authenticate(req)
+               if err != nil {
+                       log.WithField("error", errors.Wrap(err, "Serve")).Error("Authenticate fail")
+
+                       return
+               }
+               handler.ServeHTTP(rw, req)
+       })
+}
+
+func rpcInit(h *bc.BlockchainReactor, config *cfg.Config, accessTokens *accesstoken.CredentialStore) {
        // The waitHandler accepts incoming requests, but blocks until its underlying
        // handler is set, when the second phase is complete.
        var coreHandler waitHandler
@@ -95,6 +112,7 @@ func rpcInit(h *bc.BlockchainReactor, config *cfg.Config) {
        mux.Handle("/", &coreHandler)
 
        var handler http.Handler = mux
+       handler = AuthHandler(handler, accessTokens)
        handler = RedirectHandler(handler)
 
        secureheader.DefaultConfig.PermitClearLoopback = true
@@ -215,7 +233,7 @@ func NewNode(config *cfg.Config) *Node {
 
        sw.AddReactor("BLOCKCHAIN", bcReactor)
 
-       rpcInit(bcReactor, config)
+       rpcInit(bcReactor, config, accessTokens)
        // Optionally, start the pex reactor
        var addrBook *p2p.AddrBook
        if config.P2P.PexReactor {