123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- // mgo - MongoDB driver for Go
- //
- // Copyright (c) 2010-2012 - Gustavo Niemeyer <gustavo@niemeyer.net>
- //
- // All rights reserved.
- //
- // Redistribution and use in source and binary forms, with or without
- // modification, are permitted provided that the following conditions are met:
- //
- // 1. Redistributions of source code must retain the above copyright notice, this
- // list of conditions and the following disclaimer.
- // 2. Redistributions in binary form must reproduce the above copyright notice,
- // this list of conditions and the following disclaimer in the documentation
- // and/or other materials provided with the distribution.
- //
- // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- package mgo
- import (
- "crypto/md5"
- "crypto/sha1"
- "encoding/hex"
- "errors"
- "fmt"
- "sync"
- "gopkg.in/mgo.v2/bson"
- "gopkg.in/mgo.v2/internal/scram"
- )
- type authCmd struct {
- Authenticate int
- Nonce string
- User string
- Key string
- }
- type startSaslCmd struct {
- StartSASL int `bson:"startSasl"`
- }
- type authResult struct {
- ErrMsg string
- Ok bool
- }
- type getNonceCmd struct {
- GetNonce int
- }
- type getNonceResult struct {
- Nonce string
- Err string "$err"
- Code int
- }
- type logoutCmd struct {
- Logout int
- }
- type saslCmd struct {
- Start int `bson:"saslStart,omitempty"`
- Continue int `bson:"saslContinue,omitempty"`
- ConversationId int `bson:"conversationId,omitempty"`
- Mechanism string `bson:"mechanism,omitempty"`
- Payload []byte
- }
- type saslResult struct {
- Ok bool `bson:"ok"`
- NotOk bool `bson:"code"` // Server <= 2.3.2 returns ok=1 & code>0 on errors (WTF?)
- Done bool
- ConversationId int `bson:"conversationId"`
- Payload []byte
- ErrMsg string
- }
- type saslStepper interface {
- Step(serverData []byte) (clientData []byte, done bool, err error)
- Close()
- }
- func (socket *mongoSocket) getNonce() (nonce string, err error) {
- socket.Lock()
- for socket.cachedNonce == "" && socket.dead == nil {
- debugf("Socket %p to %s: waiting for nonce", socket, socket.addr)
- socket.gotNonce.Wait()
- }
- if socket.cachedNonce == "mongos" {
- socket.Unlock()
- return "", errors.New("Can't authenticate with mongos; see http://j.mp/mongos-auth")
- }
- debugf("Socket %p to %s: got nonce", socket, socket.addr)
- nonce, err = socket.cachedNonce, socket.dead
- socket.cachedNonce = ""
- socket.Unlock()
- if err != nil {
- nonce = ""
- }
- return
- }
- func (socket *mongoSocket) resetNonce() {
- debugf("Socket %p to %s: requesting a new nonce", socket, socket.addr)
- op := &queryOp{}
- op.query = &getNonceCmd{GetNonce: 1}
- op.collection = "admin.$cmd"
- op.limit = -1
- op.replyFunc = func(err error, reply *replyOp, docNum int, docData []byte) {
- if err != nil {
- socket.kill(errors.New("getNonce: "+err.Error()), true)
- return
- }
- result := &getNonceResult{}
- err = bson.Unmarshal(docData, &result)
- if err != nil {
- socket.kill(errors.New("Failed to unmarshal nonce: "+err.Error()), true)
- return
- }
- debugf("Socket %p to %s: nonce unmarshalled: %#v", socket, socket.addr, result)
- if result.Code == 13390 {
- // mongos doesn't yet support auth (see http://j.mp/mongos-auth)
- result.Nonce = "mongos"
- } else if result.Nonce == "" {
- var msg string
- if result.Err != "" {
- msg = fmt.Sprintf("Got an empty nonce: %s (%d)", result.Err, result.Code)
- } else {
- msg = "Got an empty nonce"
- }
- socket.kill(errors.New(msg), true)
- return
- }
- socket.Lock()
- if socket.cachedNonce != "" {
- socket.Unlock()
- panic("resetNonce: nonce already cached")
- }
- socket.cachedNonce = result.Nonce
- socket.gotNonce.Signal()
- socket.Unlock()
- }
- err := socket.Query(op)
- if err != nil {
- socket.kill(errors.New("resetNonce: "+err.Error()), true)
- }
- }
- func (socket *mongoSocket) Login(cred Credential) error {
- socket.Lock()
- if cred.Mechanism == "" && socket.serverInfo.MaxWireVersion >= 3 {
- cred.Mechanism = "SCRAM-SHA-1"
- }
- for _, sockCred := range socket.creds {
- if sockCred == cred {
- debugf("Socket %p to %s: login: db=%q user=%q (already logged in)", socket, socket.addr, cred.Source, cred.Username)
- socket.Unlock()
- return nil
- }
- }
- if socket.dropLogout(cred) {
- debugf("Socket %p to %s: login: db=%q user=%q (cached)", socket, socket.addr, cred.Source, cred.Username)
- socket.creds = append(socket.creds, cred)
- socket.Unlock()
- return nil
- }
- socket.Unlock()
- debugf("Socket %p to %s: login: db=%q user=%q", socket, socket.addr, cred.Source, cred.Username)
- var err error
- switch cred.Mechanism {
- case "", "MONGODB-CR", "MONGO-CR": // Name changed to MONGODB-CR in SERVER-8501.
- err = socket.loginClassic(cred)
- case "PLAIN":
- err = socket.loginPlain(cred)
- case "MONGODB-X509":
- err = socket.loginX509(cred)
- default:
- // Try SASL for everything else, if it is available.
- err = socket.loginSASL(cred)
- }
- if err != nil {
- debugf("Socket %p to %s: login error: %s", socket, socket.addr, err)
- } else {
- debugf("Socket %p to %s: login successful", socket, socket.addr)
- }
- return err
- }
- func (socket *mongoSocket) loginClassic(cred Credential) error {
- // Note that this only works properly because this function is
- // synchronous, which means the nonce won't get reset while we're
- // using it and any other login requests will block waiting for a
- // new nonce provided in the defer call below.
- nonce, err := socket.getNonce()
- if err != nil {
- return err
- }
- defer socket.resetNonce()
- psum := md5.New()
- psum.Write([]byte(cred.Username + ":mongo:" + cred.Password))
- ksum := md5.New()
- ksum.Write([]byte(nonce + cred.Username))
- ksum.Write([]byte(hex.EncodeToString(psum.Sum(nil))))
- key := hex.EncodeToString(ksum.Sum(nil))
- cmd := authCmd{Authenticate: 1, User: cred.Username, Nonce: nonce, Key: key}
- res := authResult{}
- return socket.loginRun(cred.Source, &cmd, &res, func() error {
- if !res.Ok {
- return errors.New(res.ErrMsg)
- }
- socket.Lock()
- socket.dropAuth(cred.Source)
- socket.creds = append(socket.creds, cred)
- socket.Unlock()
- return nil
- })
- }
- type authX509Cmd struct {
- Authenticate int
- User string
- Mechanism string
- }
- func (socket *mongoSocket) loginX509(cred Credential) error {
- cmd := authX509Cmd{Authenticate: 1, User: cred.Username, Mechanism: "MONGODB-X509"}
- res := authResult{}
- return socket.loginRun(cred.Source, &cmd, &res, func() error {
- if !res.Ok {
- return errors.New(res.ErrMsg)
- }
- socket.Lock()
- socket.dropAuth(cred.Source)
- socket.creds = append(socket.creds, cred)
- socket.Unlock()
- return nil
- })
- }
- func (socket *mongoSocket) loginPlain(cred Credential) error {
- cmd := saslCmd{Start: 1, Mechanism: "PLAIN", Payload: []byte("\x00" + cred.Username + "\x00" + cred.Password)}
- res := authResult{}
- return socket.loginRun(cred.Source, &cmd, &res, func() error {
- if !res.Ok {
- return errors.New(res.ErrMsg)
- }
- socket.Lock()
- socket.dropAuth(cred.Source)
- socket.creds = append(socket.creds, cred)
- socket.Unlock()
- return nil
- })
- }
- func (socket *mongoSocket) loginSASL(cred Credential) error {
- var sasl saslStepper
- var err error
- if cred.Mechanism == "SCRAM-SHA-1" {
- // SCRAM is handled without external libraries.
- sasl = saslNewScram(cred)
- } else if len(cred.ServiceHost) > 0 {
- sasl, err = saslNew(cred, cred.ServiceHost)
- } else {
- sasl, err = saslNew(cred, socket.Server().Addr)
- }
- if err != nil {
- return err
- }
- defer sasl.Close()
- // The goal of this logic is to carry a locked socket until the
- // local SASL step confirms the auth is valid; the socket needs to be
- // locked so that concurrent action doesn't leave the socket in an
- // auth state that doesn't reflect the operations that took place.
- // As a simple case, imagine inverting login=>logout to logout=>login.
- //
- // The logic below works because the lock func isn't called concurrently.
- locked := false
- lock := func(b bool) {
- if locked != b {
- locked = b
- if b {
- socket.Lock()
- } else {
- socket.Unlock()
- }
- }
- }
- lock(true)
- defer lock(false)
- start := 1
- cmd := saslCmd{}
- res := saslResult{}
- for {
- payload, done, err := sasl.Step(res.Payload)
- if err != nil {
- return err
- }
- if done && res.Done {
- socket.dropAuth(cred.Source)
- socket.creds = append(socket.creds, cred)
- break
- }
- lock(false)
- cmd = saslCmd{
- Start: start,
- Continue: 1 - start,
- ConversationId: res.ConversationId,
- Mechanism: cred.Mechanism,
- Payload: payload,
- }
- start = 0
- err = socket.loginRun(cred.Source, &cmd, &res, func() error {
- // See the comment on lock for why this is necessary.
- lock(true)
- if !res.Ok || res.NotOk {
- return fmt.Errorf("server returned error on SASL authentication step: %s", res.ErrMsg)
- }
- return nil
- })
- if err != nil {
- return err
- }
- if done && res.Done {
- socket.dropAuth(cred.Source)
- socket.creds = append(socket.creds, cred)
- break
- }
- }
- return nil
- }
- func saslNewScram(cred Credential) *saslScram {
- credsum := md5.New()
- credsum.Write([]byte(cred.Username + ":mongo:" + cred.Password))
- client := scram.NewClient(sha1.New, cred.Username, hex.EncodeToString(credsum.Sum(nil)))
- return &saslScram{cred: cred, client: client}
- }
- type saslScram struct {
- cred Credential
- client *scram.Client
- }
- func (s *saslScram) Close() {}
- func (s *saslScram) Step(serverData []byte) (clientData []byte, done bool, err error) {
- more := s.client.Step(serverData)
- return s.client.Out(), !more, s.client.Err()
- }
- func (socket *mongoSocket) loginRun(db string, query, result interface{}, f func() error) error {
- var mutex sync.Mutex
- var replyErr error
- mutex.Lock()
- op := queryOp{}
- op.query = query
- op.collection = db + ".$cmd"
- op.limit = -1
- op.replyFunc = func(err error, reply *replyOp, docNum int, docData []byte) {
- defer mutex.Unlock()
- if err != nil {
- replyErr = err
- return
- }
- err = bson.Unmarshal(docData, result)
- if err != nil {
- replyErr = err
- } else {
- // Must handle this within the read loop for the socket, so
- // that concurrent login requests are properly ordered.
- replyErr = f()
- }
- }
- err := socket.Query(&op)
- if err != nil {
- return err
- }
- mutex.Lock() // Wait.
- return replyErr
- }
- func (socket *mongoSocket) Logout(db string) {
- socket.Lock()
- cred, found := socket.dropAuth(db)
- if found {
- debugf("Socket %p to %s: logout: db=%q (flagged)", socket, socket.addr, db)
- socket.logout = append(socket.logout, cred)
- }
- socket.Unlock()
- }
- func (socket *mongoSocket) LogoutAll() {
- socket.Lock()
- if l := len(socket.creds); l > 0 {
- debugf("Socket %p to %s: logout all (flagged %d)", socket, socket.addr, l)
- socket.logout = append(socket.logout, socket.creds...)
- socket.creds = socket.creds[0:0]
- }
- socket.Unlock()
- }
- func (socket *mongoSocket) flushLogout() (ops []interface{}) {
- socket.Lock()
- if l := len(socket.logout); l > 0 {
- debugf("Socket %p to %s: logout all (flushing %d)", socket, socket.addr, l)
- for i := 0; i != l; i++ {
- op := queryOp{}
- op.query = &logoutCmd{1}
- op.collection = socket.logout[i].Source + ".$cmd"
- op.limit = -1
- ops = append(ops, &op)
- }
- socket.logout = socket.logout[0:0]
- }
- socket.Unlock()
- return
- }
- func (socket *mongoSocket) dropAuth(db string) (cred Credential, found bool) {
- for i, sockCred := range socket.creds {
- if sockCred.Source == db {
- copy(socket.creds[i:], socket.creds[i+1:])
- socket.creds = socket.creds[:len(socket.creds)-1]
- return sockCred, true
- }
- }
- return cred, false
- }
- func (socket *mongoSocket) dropLogout(cred Credential) (found bool) {
- for i, sockCred := range socket.logout {
- if sockCred == cred {
- copy(socket.logout[i:], socket.logout[i+1:])
- socket.logout = socket.logout[:len(socket.logout)-1]
- return true
- }
- }
- return false
- }
|