Implement user authorization (login)

This commit is contained in:
ChronosX88 2021-04-06 16:55:41 +03:00
parent 123610d33b
commit 0f6d2ff3d9
Signed by: ChronosXYZ
GPG Key ID: 085A69A82C8C511A
17 changed files with 337 additions and 105 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/zr
/.config.toml

View File

@ -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{}

View File

@ -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()
}

81
core/auth_handler.go Normal file
View File

@ -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"
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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"`
}

10
core/models/user.go Normal file
View File

@ -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"`
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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() {

View File

@ -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
}

View File

@ -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
}

91
core/utils/utils.go Normal file
View File

@ -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
}

View File

@ -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

4
go.mod
View File

@ -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
)

11
go.sum
View File

@ -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=