From 0f6d2ff3d990edcb88f79dd7aaf9bd7cff1d21c1 Mon Sep 17 00:00:00 2001 From: ChronosX88 Date: Tue, 6 Apr 2021 16:55:41 +0300 Subject: [PATCH] Implement user authorization (login) --- .gitignore | 1 + cmd/zr/main.go | 4 +- core/app_context.go | 34 +++---- core/auth_handler.go | 81 +++++++++++++++++ core/auth_manager.go | 48 ++++++++-- core/models/base_message.go | 2 +- core/models/protocol_error.go | 6 +- core/models/user.go | 10 ++ core/router.go | 13 +-- core/session.go | 7 +- ...nnection_handler.go => session_manager.go} | 13 ++- core/user_manager.go | 55 ++++++++++- core/utils.go | 50 ---------- core/utils/utils.go | 91 +++++++++++++++++++ core/websocket_server.go | 12 +-- go.mod | 4 +- go.sum | 11 +++ 17 files changed, 337 insertions(+), 105 deletions(-) create mode 100644 core/auth_handler.go create mode 100644 core/models/user.go rename core/{connection_handler.go => session_manager.go} (71%) delete mode 100644 core/utils.go create mode 100644 core/utils/utils.go diff --git a/.gitignore b/.gitignore index 5d8caaa..55437b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /zr +/.config.toml \ No newline at end of file diff --git a/cmd/zr/main.go b/cmd/zr/main.go index 083c5c3..3552f0e 100644 --- a/cmd/zr/main.go +++ b/cmd/zr/main.go @@ -16,8 +16,10 @@ func main() { var cfg core.Config var configPath string var generateConfig bool + + defer logger.Init("auth-server", true, false, ioutil.Discard).Close() // TODO Make ability to use file for log output flag.StringVar(&configPath, "config", "", "Path to config") - flag.BoolVar(&generateConfig, "gen_config", false, "Generate the config") + flag.BoolVar(&generateConfig, "gen-config", false, "Generate the config") flag.Parse() if generateConfig == true { sampleConfig := &core.Config{} diff --git a/core/app_context.go b/core/app_context.go index 3b9bfda..06c1af0 100644 --- a/core/app_context.go +++ b/core/app_context.go @@ -10,13 +10,13 @@ import ( ) type AppContext struct { - router *Router - authManager *AuthManager - connectionHandler *ConnectionHandler - websocketServer *WebsocketServer - cfg *Config - database *mongo.Database - userManager *UserManager + router *Router + authManager *AuthManager + sessionManager *SessionManager + websocketServer *WebsocketServer + cfg *Config + database *mongo.Database + userManager *UserManager } func NewAppContext(cfg *Config) *AppContext { @@ -32,19 +32,22 @@ func NewAppContext(cfg *Config) *AppContext { } appContext.router = router - authManager, err := NewAuthManager() + um, err := NewUserManager(appContext.database) + if err != nil { + logger.Fatalf("Unable to initialize user manager: %s", err.Error()) + } + appContext.userManager = um + + authManager, err := NewAuthManager(um, cfg.ServerID, router) if err != nil { logger.Fatalf("Unable to initialize authentication manager: %s", err.Error()) } appContext.authManager = authManager - um := NewUserManager(appContext.database) - appContext.userManager = um + sessionManager := NewSessionManager(router) + appContext.sessionManager = sessionManager - connHandler := NewConnectionHandler(router) - appContext.connectionHandler = connHandler - - wss := NewWebsocketServer(cfg, connHandler) + wss := NewWebsocketServer(cfg, sessionManager) appContext.websocketServer = wss return appContext @@ -72,6 +75,5 @@ func (ac *AppContext) connectToDatabase() { } func (ac *AppContext) Run() error { - // TODO - return nil + return ac.websocketServer.Run() } diff --git a/core/auth_handler.go b/core/auth_handler.go new file mode 100644 index 0000000..eadcdea --- /dev/null +++ b/core/auth_handler.go @@ -0,0 +1,81 @@ +package core + +import ( + "github.com/cadmium-im/zirconium-go/core/models" + "github.com/cadmium-im/zirconium-go/core/models/auth" + "github.com/cadmium-im/zirconium-go/core/utils" + "github.com/fatih/structs" + "github.com/google/logger" + "github.com/mitchellh/mapstructure" + "go.mongodb.org/mongo-driver/mongo" +) + +type AuthHandler struct { + serverID string + authManager *AuthManager +} + +func NewAuthHandler(am *AuthManager, serverID string) *AuthHandler { + return &AuthHandler{ + authManager: am, + serverID: serverID, + } +} + +func (ah *AuthHandler) HandleMessage(s *Session, message models.BaseMessage) { + var authRequest auth.AuthRequest + err := mapstructure.Decode(message.Payload, &authRequest) + if err != nil { + logger.Errorf(err.Error()) + return + } + + switch authRequest.Type { + case "urn:cadmium:auth:simple": + { + token, claims, err := ah.authManager.HandleSimpleAuth(authRequest.Fields["username"].(string), authRequest.Fields["password"].(string)) + if err != nil { + if mongo.ErrNoDocuments == err { + msg := utils.PrepareErrorMessage(message, "auth-failed", "invalid username", ah.serverID) + _ = s.Send(msg) + return + } + logger.Errorf(err.Error()) + msg := utils.PrepareMessageInternalServerError(message, err, ah.serverID) + _ = s.Send(msg) + return + } + ar := auth.AuthResponse{ + Token: token, + DeviceID: claims.DeviceID, + } + payload := structs.Map(ar) + msg := models.NewBaseMessage(message.ID, message.MessageType, ah.serverID, nil, true, payload) + _ = s.Send(msg) + s.Claims = claims + return + } + case "urn:cadmium:auth:token": + { + claims, err := ah.authManager.ValidateToken(authRequest.Fields["token"].(string)) + if err != nil { + logger.Errorf(err.Error()) + msg := utils.PrepareMessageInternalServerError(message, err, ah.serverID) + _ = s.Send(msg) + return + } + s.Claims = claims + msg := models.NewBaseMessage(message.ID, message.MessageType, ah.serverID, nil, true, nil) + _ = s.Send(msg) + return + } + } +} + +func (ah *AuthHandler) IsAuthorizationRequired() bool { + return false +} + +func (ah *AuthHandler) HandlingType() string { + return "urn:cadmium:auth" +} diff --git a/core/auth_manager.go b/core/auth_manager.go index 54e576b..8ccd2ee 100644 --- a/core/auth_manager.go +++ b/core/auth_manager.go @@ -3,6 +3,8 @@ package core import ( "encoding/base64" "fmt" + "github.com/alexedwards/argon2id" + "github.com/cadmium-im/zirconium-go/core/utils" "time" "github.com/cadmium-im/zirconium-go/core/models" @@ -16,7 +18,9 @@ const ( ) type AuthManager struct { - signingKey string // For now it is random bytes string represented in Base64 + serverID string + userManager *UserManager + signingKey string // For now it is random bytes string represented in Base64 } type JWTCustomClaims struct { @@ -25,17 +29,22 @@ type JWTCustomClaims struct { jwt.StandardClaims } -func NewAuthManager() (*AuthManager, error) { - am := &AuthManager{} - bytes, err := GenRandomBytes(SigningKeyBytesAmount) +func NewAuthManager(um *UserManager, serverID string, r *Router) (*AuthManager, error) { + am := &AuthManager{ + userManager: um, + serverID: serverID, + } + bytes, err := utils.GenRandomBytes(SigningKeyBytesAmount) if err != nil { return nil, err } am.signingKey = base64.RawStdEncoding.EncodeToString(bytes) + ah := NewAuthHandler(am, serverID) + r.RegisterC2SHandler(ah) return am, nil } -func (am *AuthManager) CreateNewToken(entityID *models.EntityID, deviceID string, tokenExpireTimeDuration time.Duration) (string, error) { +func (am *AuthManager) CreateNewToken(entityID *models.EntityID, deviceID string, tokenExpireTimeDuration time.Duration) (*JWTCustomClaims, string, error) { timeNow := time.Now() expiringTime := timeNow.Add(tokenExpireTimeDuration) @@ -59,9 +68,9 @@ func (am *AuthManager) CreateNewToken(entityID *models.EntityID, deviceID string token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(am.signingKey) if err != nil { - return "", err + return nil, "", err } - return tokenString, nil + return &claims, tokenString, nil } func (am *AuthManager) ValidateToken(tokenString string) (*JWTCustomClaims, error) { @@ -84,3 +93,28 @@ func (am *AuthManager) ValidateToken(tokenString string) (*JWTCustomClaims, erro } return nil, err } + +func (am *AuthManager) HandleSimpleAuth(username string, pass string) (string, *JWTCustomClaims, error) { + user, err := am.userManager.GetByUsername(username) + if err != nil { + return "", nil, err + } + match, err := argon2id.ComparePasswordAndHash(pass, user.PasswordHash) + if err != nil { + return "", nil, err + } + if !match { + return "", nil, fmt.Errorf("incorrect password") + } + + eid, err := models.NewEntityID("@", user.UUID, am.serverID) + if err != nil { + return "", nil, err + } + claims, token, err := am.CreateNewToken(eid, "ABCDEF", TokenExpireTimeDuration) + if err != nil { + return "", nil, err + } + + return token, claims, nil +} diff --git a/core/models/base_message.go b/core/models/base_message.go index b430f02..c9fad8d 100644 --- a/core/models/base_message.go +++ b/core/models/base_message.go @@ -5,7 +5,7 @@ type BaseMessage struct { ID string `json:"id"` MessageType string `json:"type"` From string `json:"from"` - To []string `json:"to"` + To []string `json:"to,omitempty"` Ok bool `json:"ok"` Payload map[string]interface{} `json:"payload"` } diff --git a/core/models/protocol_error.go b/core/models/protocol_error.go index 9f13a4d..7872daa 100644 --- a/core/models/protocol_error.go +++ b/core/models/protocol_error.go @@ -1,7 +1,7 @@ package models type ProtocolError struct { - ErrCode string `json:"code"` - ErrText string `json:"text"` - ErrPayload map[string]interface{} `json:"payload"` + ErrCode string `structs:"code"` + ErrText string `structs:"text"` + ErrPayload map[string]interface{} `structs:"payload,omitempty"` } diff --git a/core/models/user.go b/core/models/user.go new file mode 100644 index 0000000..df2f629 --- /dev/null +++ b/core/models/user.go @@ -0,0 +1,10 @@ +package models + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type User struct { + ID primitive.ObjectID `json:"-" bson:"_id"` + UUID string `json:"uuid"` + Username string `json:"username"` + PasswordHash string `json:"-" bson:"passwordHash"` +} diff --git a/core/router.go b/core/router.go index 021603b..fc34638 100644 --- a/core/router.go +++ b/core/router.go @@ -2,13 +2,14 @@ package core import ( "github.com/cadmium-im/zirconium-go/core/models" + "github.com/cadmium-im/zirconium-go/core/utils" + "github.com/fatih/structs" "github.com/google/logger" ) type Router struct { - appContext *AppContext - handlers map[string][]C2SMessageHandler - connections []*Session + appContext *AppContext + handlers map[string][]C2SMessageHandler } type C2SMessageHandler interface { @@ -30,10 +31,10 @@ func (r *Router) RouteMessage(origin *Session, message models.BaseMessage) { if handlers != nil { for _, v := range handlers { if v.IsAuthorizationRequired() { - if len(origin.entityID) == 0 { + if origin.Claims == nil { logger.Warningf("Connection %s isn't authorized", origin.connID) - msg := PrepareMessageUnauthorized(message, r.appContext.cfg.ServerDomains[0]) // fixme: domain + msg := utils.PrepareMessageUnauthorized(message, r.appContext.cfg.ServerDomains[0]) // fixme: domain _ = origin.Send(msg) } } @@ -45,7 +46,7 @@ func (r *Router) RouteMessage(origin *Session, message models.BaseMessage) { ErrText: "Server doesn't implement message type " + message.MessageType, ErrPayload: make(map[string]interface{}), } - errMsg := models.NewBaseMessage(message.ID, message.MessageType, r.appContext.cfg.ServerID, []string{message.From}, false, StructToMap(protocolError)) + errMsg := models.NewBaseMessage(message.ID, message.MessageType, r.appContext.cfg.ServerID, []string{message.From}, false, structs.Map(protocolError)) logger.Infof("Drop message with type %s because server hasn't proper handlers", message.MessageType) _ = origin.Send(errMsg) } diff --git a/core/session.go b/core/session.go index 1062955..0063189 100644 --- a/core/session.go +++ b/core/session.go @@ -6,10 +6,9 @@ import ( ) type Session struct { - wsConn *websocket.Conn - connID string - entityID []*models.EntityID - deviceID *string + wsConn *websocket.Conn + connID string + Claims *JWTCustomClaims } func (s *Session) Send(message models.BaseMessage) error { diff --git a/core/connection_handler.go b/core/session_manager.go similarity index 71% rename from core/connection_handler.go rename to core/session_manager.go index 04a6bd8..15a109f 100644 --- a/core/connection_handler.go +++ b/core/session_manager.go @@ -7,24 +7,23 @@ import ( "github.com/gorilla/websocket" ) -type ConnectionHandler struct { +type SessionManager struct { router *Router connections map[string]*Session } -func NewConnectionHandler(r *Router) *ConnectionHandler { - return &ConnectionHandler{ +func NewSessionManager(r *Router) *SessionManager { + return &SessionManager{ router: r, connections: make(map[string]*Session), } } -func (ch *ConnectionHandler) HandleNewConnection(wsocket *websocket.Conn) { - randomUUID, _ := uuid.NewRandom() - uuidStr := randomUUID.String() +func (ch *SessionManager) HandleNewConnection(wsocket *websocket.Conn) { + randomUUID := uuid.New().String() o := &Session{ wsConn: wsocket, - connID: uuidStr, + connID: randomUUID, } ch.connections[o.connID] = o go func() { diff --git a/core/user_manager.go b/core/user_manager.go index 6e38154..4627cc3 100644 --- a/core/user_manager.go +++ b/core/user_manager.go @@ -1,6 +1,15 @@ package core -import "go.mongodb.org/mongo-driver/mongo" +import ( + "context" + "github.com/cadmium-im/zirconium-go/core/models" + "github.com/cadmium-im/zirconium-go/core/utils" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) const ( UsersCollectionName = "users" @@ -10,12 +19,52 @@ type UserManager struct { usersCol *mongo.Collection } -func NewUserManager(db *mongo.Database) *UserManager { +func NewUserManager(db *mongo.Database) (*UserManager, error) { col := db.Collection(UsersCollectionName) um := &UserManager{ usersCol: col, } - return um + err := um.initMongo() + if err != nil { + return nil, err + } + + return um, nil +} + +func (um *UserManager) initMongo() error { + usernameIndex := mongo.IndexModel{ + Keys: bson.M{ + "username": 1, + }, Options: options.Index().SetUnique(true), + } + + exists, err := utils.IsCollectionExists(context.Background(), um.usersCol.Database(), um.usersCol.Name()) + if err != nil { + return err + } + if exists { + _, err := um.usersCol.Indexes().DropAll(context.Background()) + if err != nil { + return err + } + } + + _, err = um.usersCol.Indexes().CreateMany(context.Background(), []mongo.IndexModel{usernameIndex}) + return err +} + +func (um *UserManager) SaveUser(user *models.User) error { + user.ID = primitive.NewObjectID() + user.UUID = uuid.New().String() + _, err := um.usersCol.InsertOne(context.Background(), user) + return err +} + +func (um *UserManager) GetByUsername(username string) (*models.User, error) { + var user models.User + err := um.usersCol.FindOne(context.Background(), bson.D{{"username", username}}).Decode(&user) + return &user, err } diff --git a/core/utils.go b/core/utils.go deleted file mode 100644 index 57c27af..0000000 --- a/core/utils.go +++ /dev/null @@ -1,50 +0,0 @@ -package core - -import ( - "crypto/rand" - "reflect" - - "github.com/cadmium-im/zirconium-go/core/models" -) - -func GenRandomBytes(size int) (blk []byte, err error) { - blk = make([]byte, size) - _, err = rand.Read(blk) - return -} - -func StructToMap(item interface{}) map[string]interface{} { - res := map[string]interface{}{} - if item == nil { - return res - } - v := reflect.TypeOf(item) - reflectValue := reflect.ValueOf(item) - reflectValue = reflect.Indirect(reflectValue) - - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - for i := 0; i < v.NumField(); i++ { - tag := v.Field(i).Tag.Get("json") - field := reflectValue.Field(i).Interface() - if tag != "" && tag != "-" { - if v.Field(i).Type.Kind() == reflect.Struct { - res[tag] = StructToMap(field) - } else { - res[tag] = field - } - } - } - return res -} - -func PrepareMessageUnauthorized(msg models.BaseMessage, serverDomain string) models.BaseMessage { - protocolError := models.ProtocolError{ - ErrCode: "unauthorized", - ErrText: "Unauthorized access", - ErrPayload: make(map[string]interface{}), - } - errMsg := models.NewBaseMessage(msg.ID, msg.MessageType, serverDomain, []string{msg.From}, false, StructToMap(protocolError)) - return errMsg -} diff --git a/core/utils/utils.go b/core/utils/utils.go new file mode 100644 index 0000000..4f894f8 --- /dev/null +++ b/core/utils/utils.go @@ -0,0 +1,91 @@ +package utils + +import ( + "context" + "crypto/rand" + "fmt" + "github.com/alexedwards/argon2id" + "github.com/cadmium-im/zirconium-go/core/models" + "github.com/fatih/structs" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +var ( + ErrPasswordIsEmpty = fmt.Errorf("the password is empty") + ErrPasswordIsTooShort = fmt.Errorf("the password is too short") +) + +func GenRandomBytes(size int) (blk []byte, err error) { + blk = make([]byte, size) + _, err = rand.Read(blk) + return +} + +func PrepareMessageUnauthorized(msg models.BaseMessage, serverDomain string) models.BaseMessage { + protocolError := models.ProtocolError{ + ErrCode: "unauthorized", + ErrText: "Unauthorized access", + ErrPayload: make(map[string]interface{}), + } + errMsg := models.NewBaseMessage(msg.ID, msg.MessageType, serverDomain, []string{msg.From}, false, structs.Map(protocolError)) + return errMsg +} + +func PrepareMessageInternalServerError(msg models.BaseMessage, err error, serverID string) models.BaseMessage { + protocolError := models.ProtocolError{ + ErrCode: "internal-server-error", + ErrText: err.Error(), + ErrPayload: nil, + } + var to []string + if msg.From != "" { + to = append(to, msg.From) + } + errMsg := models.NewBaseMessage(msg.ID, msg.MessageType, serverID, to, false, structs.Map(protocolError)) + return errMsg +} + +func PrepareErrorMessage(msg models.BaseMessage, errorType string, errorText string, serverID string) models.BaseMessage { + protocolError := models.ProtocolError{ + ErrCode: errorType, + ErrText: errorText, + ErrPayload: nil, + } + var to []string + if msg.From != "" { + to = append(to, msg.From) + } + errMsg := models.NewBaseMessage(msg.ID, msg.MessageType, serverID, to, false, structs.Map(protocolError)) + return errMsg +} + +func HashPassword(password string) (string, error) { + return argon2id.CreateHash(password, argon2id.DefaultParams) +} + +func ValidatePassword(password string) error { + if password == "" { + return ErrPasswordIsEmpty + } + if len(password) < 4 { + return ErrPasswordIsTooShort + } + return nil +} + +func IsCollectionExists(ctx context.Context, db *mongo.Database, collectionName string) (bool, error) { + isExists := false + names, err := db.ListCollectionNames(ctx, bson.D{{"name", collectionName}}) + if err != nil { + return false, err + } + + for _, name := range names { + if name == collectionName { + isExists = true + break + } + } + return isExists, nil +} diff --git a/core/websocket_server.go b/core/websocket_server.go index 6136e40..8af48d3 100644 --- a/core/websocket_server.go +++ b/core/websocket_server.go @@ -15,15 +15,15 @@ var wsUpgrader = websocket.Upgrader{ } type WebsocketServer struct { - r *mux.Router - connHandler *ConnectionHandler - cfg *Config + r *mux.Router + sessionManager *SessionManager + cfg *Config } -func NewWebsocketServer(cfg *Config, connHandler *ConnectionHandler) *WebsocketServer { +func NewWebsocketServer(cfg *Config, sessionManager *SessionManager) *WebsocketServer { wss := &WebsocketServer{} - wss.connHandler = connHandler + wss.sessionManager = sessionManager wss.cfg = cfg r := mux.NewRouter() wss.r = r @@ -36,7 +36,7 @@ func NewWebsocketServer(cfg *Config, connHandler *ConnectionHandler) *WebsocketS logger.Errorf(err.Error()) return } - wss.connHandler.HandleNewConnection(ws) + wss.sessionManager.HandleNewConnection(ws) }) return wss diff --git a/go.mod b/go.mod index a671728..11a0eb6 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,14 @@ module github.com/cadmium-im/zirconium-go go 1.13 require ( - github.com/BurntSushi/toml v0.3.1 + github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/fatih/structs v1.1.0 // indirect github.com/google/logger v1.0.1 github.com/google/uuid v1.1.1 github.com/gorilla/mux v1.7.3 github.com/gorilla/websocket v1.4.1 + github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/pelletier/go-toml v1.7.0 go.mongodb.org/mongo-driver v1.5.1 ) diff --git a/go.sum b/go.sum index ed32168..ab8be42 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77 h1:X6U+/fhTYeDYS3sN4xHcoORJhhar+zSgrNeraapuRK4= +github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77/go.mod h1:Kmn5t2Rb93Q4NTprN4+CCgARGvigKMJyxP0WckpTUp0= github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -7,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -65,6 +69,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= @@ -105,6 +111,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= @@ -122,6 +130,9 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=