--- /dev/null
+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
+}
--- /dev/null
+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)
+ }
+ }
+}
--- /dev/null
+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
+}
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"
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
mux.Handle("/", &coreHandler)
var handler http.Handler = mux
+ handler = AuthHandler(handler, accessTokens)
handler = RedirectHandler(handler)
secureheader.DefaultConfig.PermitClearLoopback = true
sw.AddReactor("BLOCKCHAIN", bcReactor)
- rpcInit(bcReactor, config)
+ rpcInit(bcReactor, config, accessTokens)
// Optionally, start the pex reactor
var addrBook *p2p.AddrBook
if config.P2P.PexReactor {