升级流程引擎配置文件版本

This commit is contained in:
hoteas
2022-10-19 21:32:34 +08:00
parent cdba8af129
commit 78337c14ec
501 changed files with 157534 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsonx // import "go.mongodb.org/mongo-driver/x/bsonx"
import (
"bytes"
"errors"
"fmt"
"strconv"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// ErrNilArray indicates that an operation was attempted on a nil *Array.
var ErrNilArray = errors.New("array is nil")
// Arr represents an array in BSON.
type Arr []Val
// String implements the fmt.Stringer interface.
func (a Arr) String() string {
var buf bytes.Buffer
buf.Write([]byte("bson.Array["))
for idx, val := range a {
if idx > 0 {
buf.Write([]byte(", "))
}
fmt.Fprintf(&buf, "%s", val)
}
buf.WriteByte(']')
return buf.String()
}
// MarshalBSONValue implements the bsoncodec.ValueMarshaler interface.
func (a Arr) MarshalBSONValue() (bsontype.Type, []byte, error) {
if a == nil {
// TODO: Should we do this?
return bsontype.Null, nil, nil
}
idx, dst := bsoncore.ReserveLength(nil)
for idx, value := range a {
t, data, _ := value.MarshalBSONValue() // marshalBSONValue never returns an error.
dst = append(dst, byte(t))
dst = append(dst, strconv.Itoa(idx)...)
dst = append(dst, 0x00)
dst = append(dst, data...)
}
dst = append(dst, 0x00)
dst = bsoncore.UpdateLength(dst, idx, int32(len(dst[idx:])))
return bsontype.Array, dst, nil
}
// UnmarshalBSONValue implements the bsoncodec.ValueUnmarshaler interface.
func (a *Arr) UnmarshalBSONValue(t bsontype.Type, data []byte) error {
if a == nil {
return ErrNilArray
}
*a = (*a)[:0]
elements, err := bsoncore.Document(data).Elements()
if err != nil {
return err
}
for _, elem := range elements {
var val Val
rawval := elem.Value()
err = val.UnmarshalBSONValue(rawval.Type, rawval.Data)
if err != nil {
return err
}
*a = append(*a, val)
}
return nil
}
// Equal compares this document to another, returning true if they are equal.
func (a Arr) Equal(a2 Arr) bool {
if len(a) != len(a2) {
return false
}
for idx := range a {
if !a[idx].Equal(a2[idx]) {
return false
}
}
return true
}
func (Arr) idoc() {}
+164
View File
@@ -0,0 +1,164 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsoncore
import (
"bytes"
"fmt"
"io"
"strconv"
)
// NewArrayLengthError creates and returns an error for when the length of an array exceeds the
// bytes available.
func NewArrayLengthError(length, rem int) error {
return lengthError("array", length, rem)
}
// Array is a raw bytes representation of a BSON array.
type Array []byte
// NewArrayFromReader reads an array from r. This function will only validate the length is
// correct and that the array ends with a null byte.
func NewArrayFromReader(r io.Reader) (Array, error) {
return newBufferFromReader(r)
}
// Index searches for and retrieves the value at the given index. This method will panic if
// the array is invalid or if the index is out of bounds.
func (a Array) Index(index uint) Value {
value, err := a.IndexErr(index)
if err != nil {
panic(err)
}
return value
}
// IndexErr searches for and retrieves the value at the given index.
func (a Array) IndexErr(index uint) (Value, error) {
elem, err := indexErr(a, index)
if err != nil {
return Value{}, err
}
return elem.Value(), err
}
// DebugString outputs a human readable version of Array. It will attempt to stringify the
// valid components of the array even if the entire array is not valid.
func (a Array) DebugString() string {
if len(a) < 5 {
return "<malformed>"
}
var buf bytes.Buffer
buf.WriteString("Array")
length, rem, _ := ReadLength(a) // We know we have enough bytes to read the length
buf.WriteByte('(')
buf.WriteString(strconv.Itoa(int(length)))
length -= 4
buf.WriteString(")[")
var elem Element
var ok bool
for length > 1 {
elem, rem, ok = ReadElement(rem)
length -= int32(len(elem))
if !ok {
buf.WriteString(fmt.Sprintf("<malformed (%d)>", length))
break
}
fmt.Fprintf(&buf, "%s", elem.Value().DebugString())
if length != 1 {
buf.WriteByte(',')
}
}
buf.WriteByte(']')
return buf.String()
}
// String outputs an ExtendedJSON version of Array. If the Array is not valid, this method
// returns an empty string.
func (a Array) String() string {
if len(a) < 5 {
return ""
}
var buf bytes.Buffer
buf.WriteByte('[')
length, rem, _ := ReadLength(a) // We know we have enough bytes to read the length
length -= 4
var elem Element
var ok bool
for length > 1 {
elem, rem, ok = ReadElement(rem)
length -= int32(len(elem))
if !ok {
return ""
}
fmt.Fprintf(&buf, "%s", elem.Value().String())
if length > 1 {
buf.WriteByte(',')
}
}
if length != 1 { // Missing final null byte or inaccurate length
return ""
}
buf.WriteByte(']')
return buf.String()
}
// Values returns this array as a slice of values. The returned slice will contain valid values.
// If the array is not valid, the values up to the invalid point will be returned along with an
// error.
func (a Array) Values() ([]Value, error) {
return values(a)
}
// Validate validates the array and ensures the elements contained within are valid.
func (a Array) Validate() error {
length, rem, ok := ReadLength(a)
if !ok {
return NewInsufficientBytesError(a, rem)
}
if int(length) > len(a) {
return NewArrayLengthError(int(length), len(a))
}
if a[length-1] != 0x00 {
return ErrMissingNull
}
length -= 4
var elem Element
var keyNum int64
for length > 1 {
elem, rem, ok = ReadElement(rem)
length -= int32(len(elem))
if !ok {
return NewInsufficientBytesError(a, rem)
}
// validate element
err := elem.Validate()
if err != nil {
return err
}
// validate keys increase numerically
if fmt.Sprint(keyNum) != elem.Key() {
return fmt.Errorf("array key %q is out of order or invalid", elem.Key())
}
keyNum++
}
if len(rem) < 1 || rem[0] != 0x00 {
return ErrMissingNull
}
return nil
}
@@ -0,0 +1,201 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsoncore
import (
"strconv"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// ArrayBuilder builds a bson array
type ArrayBuilder struct {
arr []byte
indexes []int32
keys []int
}
// NewArrayBuilder creates a new ArrayBuilder
func NewArrayBuilder() *ArrayBuilder {
return (&ArrayBuilder{}).startArray()
}
// startArray reserves the array's length and sets the index to where the length begins
func (a *ArrayBuilder) startArray() *ArrayBuilder {
var index int32
index, a.arr = AppendArrayStart(a.arr)
a.indexes = append(a.indexes, index)
a.keys = append(a.keys, 0)
return a
}
// Build updates the length of the array and index to the beginning of the documents length
// bytes, then returns the array (bson bytes)
func (a *ArrayBuilder) Build() Array {
lastIndex := len(a.indexes) - 1
lastKey := len(a.keys) - 1
a.arr, _ = AppendArrayEnd(a.arr, a.indexes[lastIndex])
a.indexes = a.indexes[:lastIndex]
a.keys = a.keys[:lastKey]
return a.arr
}
// incrementKey() increments the value keys and returns the key to be used to a.appendArray* functions
func (a *ArrayBuilder) incrementKey() string {
idx := len(a.keys) - 1
key := strconv.Itoa(a.keys[idx])
a.keys[idx]++
return key
}
// AppendInt32 will append i32 to ArrayBuilder.arr
func (a *ArrayBuilder) AppendInt32(i32 int32) *ArrayBuilder {
a.arr = AppendInt32Element(a.arr, a.incrementKey(), i32)
return a
}
// AppendDocument will append doc to ArrayBuilder.arr
func (a *ArrayBuilder) AppendDocument(doc []byte) *ArrayBuilder {
a.arr = AppendDocumentElement(a.arr, a.incrementKey(), doc)
return a
}
// AppendArray will append arr to ArrayBuilder.arr
func (a *ArrayBuilder) AppendArray(arr []byte) *ArrayBuilder {
a.arr = AppendArrayElement(a.arr, a.incrementKey(), arr)
return a
}
// AppendDouble will append f to ArrayBuilder.doc
func (a *ArrayBuilder) AppendDouble(f float64) *ArrayBuilder {
a.arr = AppendDoubleElement(a.arr, a.incrementKey(), f)
return a
}
// AppendString will append str to ArrayBuilder.doc
func (a *ArrayBuilder) AppendString(str string) *ArrayBuilder {
a.arr = AppendStringElement(a.arr, a.incrementKey(), str)
return a
}
// AppendObjectID will append oid to ArrayBuilder.doc
func (a *ArrayBuilder) AppendObjectID(oid primitive.ObjectID) *ArrayBuilder {
a.arr = AppendObjectIDElement(a.arr, a.incrementKey(), oid)
return a
}
// AppendBinary will append a BSON binary element using subtype, and
// b to a.arr
func (a *ArrayBuilder) AppendBinary(subtype byte, b []byte) *ArrayBuilder {
a.arr = AppendBinaryElement(a.arr, a.incrementKey(), subtype, b)
return a
}
// AppendUndefined will append a BSON undefined element using key to a.arr
func (a *ArrayBuilder) AppendUndefined() *ArrayBuilder {
a.arr = AppendUndefinedElement(a.arr, a.incrementKey())
return a
}
// AppendBoolean will append a boolean element using b to a.arr
func (a *ArrayBuilder) AppendBoolean(b bool) *ArrayBuilder {
a.arr = AppendBooleanElement(a.arr, a.incrementKey(), b)
return a
}
// AppendDateTime will append datetime element dt to a.arr
func (a *ArrayBuilder) AppendDateTime(dt int64) *ArrayBuilder {
a.arr = AppendDateTimeElement(a.arr, a.incrementKey(), dt)
return a
}
// AppendNull will append a null element to a.arr
func (a *ArrayBuilder) AppendNull() *ArrayBuilder {
a.arr = AppendNullElement(a.arr, a.incrementKey())
return a
}
// AppendRegex will append pattern and options to a.arr
func (a *ArrayBuilder) AppendRegex(pattern, options string) *ArrayBuilder {
a.arr = AppendRegexElement(a.arr, a.incrementKey(), pattern, options)
return a
}
// AppendDBPointer will append ns and oid to a.arr
func (a *ArrayBuilder) AppendDBPointer(ns string, oid primitive.ObjectID) *ArrayBuilder {
a.arr = AppendDBPointerElement(a.arr, a.incrementKey(), ns, oid)
return a
}
// AppendJavaScript will append js to a.arr
func (a *ArrayBuilder) AppendJavaScript(js string) *ArrayBuilder {
a.arr = AppendJavaScriptElement(a.arr, a.incrementKey(), js)
return a
}
// AppendSymbol will append symbol to a.arr
func (a *ArrayBuilder) AppendSymbol(symbol string) *ArrayBuilder {
a.arr = AppendSymbolElement(a.arr, a.incrementKey(), symbol)
return a
}
// AppendCodeWithScope will append code and scope to a.arr
func (a *ArrayBuilder) AppendCodeWithScope(code string, scope Document) *ArrayBuilder {
a.arr = AppendCodeWithScopeElement(a.arr, a.incrementKey(), code, scope)
return a
}
// AppendTimestamp will append t and i to a.arr
func (a *ArrayBuilder) AppendTimestamp(t, i uint32) *ArrayBuilder {
a.arr = AppendTimestampElement(a.arr, a.incrementKey(), t, i)
return a
}
// AppendInt64 will append i64 to a.arr
func (a *ArrayBuilder) AppendInt64(i64 int64) *ArrayBuilder {
a.arr = AppendInt64Element(a.arr, a.incrementKey(), i64)
return a
}
// AppendDecimal128 will append d128 to a.arr
func (a *ArrayBuilder) AppendDecimal128(d128 primitive.Decimal128) *ArrayBuilder {
a.arr = AppendDecimal128Element(a.arr, a.incrementKey(), d128)
return a
}
// AppendMaxKey will append a max key element to a.arr
func (a *ArrayBuilder) AppendMaxKey() *ArrayBuilder {
a.arr = AppendMaxKeyElement(a.arr, a.incrementKey())
return a
}
// AppendMinKey will append a min key element to a.arr
func (a *ArrayBuilder) AppendMinKey() *ArrayBuilder {
a.arr = AppendMinKeyElement(a.arr, a.incrementKey())
return a
}
// AppendValue appends a BSON value to the array.
func (a *ArrayBuilder) AppendValue(val Value) *ArrayBuilder {
a.arr = AppendValueElement(a.arr, a.incrementKey(), val)
return a
}
// StartArray starts building an inline Array. After this document is completed,
// the user must call a.FinishArray
func (a *ArrayBuilder) StartArray() *ArrayBuilder {
a.arr = AppendHeader(a.arr, bsontype.Array, a.incrementKey())
a.startArray()
return a
}
// FinishArray builds the most recent array created
func (a *ArrayBuilder) FinishArray() *ArrayBuilder {
a.arr = a.Build()
return a
}
@@ -0,0 +1,189 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsoncore
import (
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// DocumentBuilder builds a bson document
type DocumentBuilder struct {
doc []byte
indexes []int32
}
// startDocument reserves the document's length and set the index to where the length begins
func (db *DocumentBuilder) startDocument() *DocumentBuilder {
var index int32
index, db.doc = AppendDocumentStart(db.doc)
db.indexes = append(db.indexes, index)
return db
}
// NewDocumentBuilder creates a new DocumentBuilder
func NewDocumentBuilder() *DocumentBuilder {
return (&DocumentBuilder{}).startDocument()
}
// Build updates the length of the document and index to the beginning of the documents length
// bytes, then returns the document (bson bytes)
func (db *DocumentBuilder) Build() Document {
last := len(db.indexes) - 1
db.doc, _ = AppendDocumentEnd(db.doc, db.indexes[last])
db.indexes = db.indexes[:last]
return db.doc
}
// AppendInt32 will append an int32 element using key and i32 to DocumentBuilder.doc
func (db *DocumentBuilder) AppendInt32(key string, i32 int32) *DocumentBuilder {
db.doc = AppendInt32Element(db.doc, key, i32)
return db
}
// AppendDocument will append a bson embedded document element using key
// and doc to DocumentBuilder.doc
func (db *DocumentBuilder) AppendDocument(key string, doc []byte) *DocumentBuilder {
db.doc = AppendDocumentElement(db.doc, key, doc)
return db
}
// AppendArray will append a bson array using key and arr to DocumentBuilder.doc
func (db *DocumentBuilder) AppendArray(key string, arr []byte) *DocumentBuilder {
db.doc = AppendHeader(db.doc, bsontype.Array, key)
db.doc = AppendArray(db.doc, arr)
return db
}
// AppendDouble will append a double element using key and f to DocumentBuilder.doc
func (db *DocumentBuilder) AppendDouble(key string, f float64) *DocumentBuilder {
db.doc = AppendDoubleElement(db.doc, key, f)
return db
}
// AppendString will append str to DocumentBuilder.doc with the given key
func (db *DocumentBuilder) AppendString(key string, str string) *DocumentBuilder {
db.doc = AppendStringElement(db.doc, key, str)
return db
}
// AppendObjectID will append oid to DocumentBuilder.doc with the given key
func (db *DocumentBuilder) AppendObjectID(key string, oid primitive.ObjectID) *DocumentBuilder {
db.doc = AppendObjectIDElement(db.doc, key, oid)
return db
}
// AppendBinary will append a BSON binary element using key, subtype, and
// b to db.doc
func (db *DocumentBuilder) AppendBinary(key string, subtype byte, b []byte) *DocumentBuilder {
db.doc = AppendBinaryElement(db.doc, key, subtype, b)
return db
}
// AppendUndefined will append a BSON undefined element using key to db.doc
func (db *DocumentBuilder) AppendUndefined(key string) *DocumentBuilder {
db.doc = AppendUndefinedElement(db.doc, key)
return db
}
// AppendBoolean will append a boolean element using key and b to db.doc
func (db *DocumentBuilder) AppendBoolean(key string, b bool) *DocumentBuilder {
db.doc = AppendBooleanElement(db.doc, key, b)
return db
}
// AppendDateTime will append a datetime element using key and dt to db.doc
func (db *DocumentBuilder) AppendDateTime(key string, dt int64) *DocumentBuilder {
db.doc = AppendDateTimeElement(db.doc, key, dt)
return db
}
// AppendNull will append a null element using key to db.doc
func (db *DocumentBuilder) AppendNull(key string) *DocumentBuilder {
db.doc = AppendNullElement(db.doc, key)
return db
}
// AppendRegex will append pattern and options using key to db.doc
func (db *DocumentBuilder) AppendRegex(key, pattern, options string) *DocumentBuilder {
db.doc = AppendRegexElement(db.doc, key, pattern, options)
return db
}
// AppendDBPointer will append ns and oid to using key to db.doc
func (db *DocumentBuilder) AppendDBPointer(key string, ns string, oid primitive.ObjectID) *DocumentBuilder {
db.doc = AppendDBPointerElement(db.doc, key, ns, oid)
return db
}
// AppendJavaScript will append js using the provided key to db.doc
func (db *DocumentBuilder) AppendJavaScript(key, js string) *DocumentBuilder {
db.doc = AppendJavaScriptElement(db.doc, key, js)
return db
}
// AppendSymbol will append a BSON symbol element using key and symbol db.doc
func (db *DocumentBuilder) AppendSymbol(key, symbol string) *DocumentBuilder {
db.doc = AppendSymbolElement(db.doc, key, symbol)
return db
}
// AppendCodeWithScope will append code and scope using key to db.doc
func (db *DocumentBuilder) AppendCodeWithScope(key string, code string, scope Document) *DocumentBuilder {
db.doc = AppendCodeWithScopeElement(db.doc, key, code, scope)
return db
}
// AppendTimestamp will append t and i to db.doc using provided key
func (db *DocumentBuilder) AppendTimestamp(key string, t, i uint32) *DocumentBuilder {
db.doc = AppendTimestampElement(db.doc, key, t, i)
return db
}
// AppendInt64 will append i64 to dst using key to db.doc
func (db *DocumentBuilder) AppendInt64(key string, i64 int64) *DocumentBuilder {
db.doc = AppendInt64Element(db.doc, key, i64)
return db
}
// AppendDecimal128 will append d128 to db.doc using provided key
func (db *DocumentBuilder) AppendDecimal128(key string, d128 primitive.Decimal128) *DocumentBuilder {
db.doc = AppendDecimal128Element(db.doc, key, d128)
return db
}
// AppendMaxKey will append a max key element using key to db.doc
func (db *DocumentBuilder) AppendMaxKey(key string) *DocumentBuilder {
db.doc = AppendMaxKeyElement(db.doc, key)
return db
}
// AppendMinKey will append a min key element using key to db.doc
func (db *DocumentBuilder) AppendMinKey(key string) *DocumentBuilder {
db.doc = AppendMinKeyElement(db.doc, key)
return db
}
// AppendValue will append a BSON element with the provided key and value to the document.
func (db *DocumentBuilder) AppendValue(key string, val Value) *DocumentBuilder {
db.doc = AppendValueElement(db.doc, key, val)
return db
}
// StartDocument starts building an inline document element with the provided key
// After this document is completed, the user must call finishDocument
func (db *DocumentBuilder) StartDocument(key string) *DocumentBuilder {
db.doc = AppendHeader(db.doc, bsontype.EmbeddedDocument, key)
db = db.startDocument()
return db
}
// FinishDocument builds the most recent document created
func (db *DocumentBuilder) FinishDocument() *DocumentBuilder {
db.doc = db.Build()
return db
}
+862
View File
@@ -0,0 +1,862 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
// Package bsoncore contains functions that can be used to encode and decode BSON
// elements and values to or from a slice of bytes. These functions are aimed at
// allowing low level manipulation of BSON and can be used to build a higher
// level BSON library.
//
// The Read* functions within this package return the values of the element and
// a boolean indicating if the values are valid. A boolean was used instead of
// an error because any error that would be returned would be the same: not
// enough bytes. This library attempts to do no validation, it will only return
// false if there are not enough bytes for an item to be read. For example, the
// ReadDocument function checks the length, if that length is larger than the
// number of bytes available, it will return false, if there are enough bytes, it
// will return those bytes and true. It is the consumers responsibility to
// validate those bytes.
//
// The Append* functions within this package will append the type value to the
// given dst slice. If the slice has enough capacity, it will not grow the
// slice. The Append*Element functions within this package operate in the same
// way, but additionally append the BSON type and the key before the value.
package bsoncore // import "go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
import (
"bytes"
"fmt"
"math"
"strconv"
"strings"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
// EmptyDocumentLength is the length of a document that has been started/ended but has no elements.
EmptyDocumentLength = 5
// nullTerminator is a string version of the 0 byte that is appended at the end of cstrings.
nullTerminator = string(byte(0))
invalidKeyPanicMsg = "BSON element keys cannot contain null bytes"
invalidRegexPanicMsg = "BSON regex values cannot contain null bytes"
)
// AppendType will append t to dst and return the extended buffer.
func AppendType(dst []byte, t bsontype.Type) []byte { return append(dst, byte(t)) }
// AppendKey will append key to dst and return the extended buffer.
func AppendKey(dst []byte, key string) []byte { return append(dst, key+nullTerminator...) }
// AppendHeader will append Type t and key to dst and return the extended
// buffer.
func AppendHeader(dst []byte, t bsontype.Type, key string) []byte {
if !isValidCString(key) {
panic(invalidKeyPanicMsg)
}
dst = AppendType(dst, t)
dst = append(dst, key...)
return append(dst, 0x00)
// return append(AppendType(dst, t), key+string(0x00)...)
}
// TODO(skriptble): All of the Read* functions should return src resliced to start just after what was read.
// ReadType will return the first byte of the provided []byte as a type. If
// there is no available byte, false is returned.
func ReadType(src []byte) (bsontype.Type, []byte, bool) {
if len(src) < 1 {
return 0, src, false
}
return bsontype.Type(src[0]), src[1:], true
}
// ReadKey will read a key from src. The 0x00 byte will not be present
// in the returned string. If there are not enough bytes available, false is
// returned.
func ReadKey(src []byte) (string, []byte, bool) { return readcstring(src) }
// ReadKeyBytes will read a key from src as bytes. The 0x00 byte will
// not be present in the returned string. If there are not enough bytes
// available, false is returned.
func ReadKeyBytes(src []byte) ([]byte, []byte, bool) { return readcstringbytes(src) }
// ReadHeader will read a type byte and a key from src. If both of these
// values cannot be read, false is returned.
func ReadHeader(src []byte) (t bsontype.Type, key string, rem []byte, ok bool) {
t, rem, ok = ReadType(src)
if !ok {
return 0, "", src, false
}
key, rem, ok = ReadKey(rem)
if !ok {
return 0, "", src, false
}
return t, key, rem, true
}
// ReadHeaderBytes will read a type and a key from src and the remainder of the bytes
// are returned as rem. If either the type or key cannot be red, ok will be false.
func ReadHeaderBytes(src []byte) (header []byte, rem []byte, ok bool) {
if len(src) < 1 {
return nil, src, false
}
idx := bytes.IndexByte(src[1:], 0x00)
if idx == -1 {
return nil, src, false
}
return src[:idx], src[idx+1:], true
}
// ReadElement reads the next full element from src. It returns the element, the remaining bytes in
// the slice, and a boolean indicating if the read was successful.
func ReadElement(src []byte) (Element, []byte, bool) {
if len(src) < 1 {
return nil, src, false
}
t := bsontype.Type(src[0])
idx := bytes.IndexByte(src[1:], 0x00)
if idx == -1 {
return nil, src, false
}
length, ok := valueLength(src[idx+2:], t) // We add 2 here because we called IndexByte with src[1:]
if !ok {
return nil, src, false
}
elemLength := 1 + idx + 1 + int(length)
if elemLength > len(src) {
return nil, src, false
}
if elemLength < 0 {
return nil, src, false
}
return src[:elemLength], src[elemLength:], true
}
// AppendValueElement appends value to dst as an element using key as the element's key.
func AppendValueElement(dst []byte, key string, value Value) []byte {
dst = AppendHeader(dst, value.Type, key)
dst = append(dst, value.Data...)
return dst
}
// ReadValue reads the next value as the provided types and returns a Value, the remaining bytes,
// and a boolean indicating if the read was successful.
func ReadValue(src []byte, t bsontype.Type) (Value, []byte, bool) {
data, rem, ok := readValue(src, t)
if !ok {
return Value{}, src, false
}
return Value{Type: t, Data: data}, rem, true
}
// AppendDouble will append f to dst and return the extended buffer.
func AppendDouble(dst []byte, f float64) []byte {
return appendu64(dst, math.Float64bits(f))
}
// AppendDoubleElement will append a BSON double element using key and f to dst
// and return the extended buffer.
func AppendDoubleElement(dst []byte, key string, f float64) []byte {
return AppendDouble(AppendHeader(dst, bsontype.Double, key), f)
}
// ReadDouble will read a float64 from src. If there are not enough bytes it
// will return false.
func ReadDouble(src []byte) (float64, []byte, bool) {
bits, src, ok := readu64(src)
if !ok {
return 0, src, false
}
return math.Float64frombits(bits), src, true
}
// AppendString will append s to dst and return the extended buffer.
func AppendString(dst []byte, s string) []byte {
return appendstring(dst, s)
}
// AppendStringElement will append a BSON string element using key and val to dst
// and return the extended buffer.
func AppendStringElement(dst []byte, key, val string) []byte {
return AppendString(AppendHeader(dst, bsontype.String, key), val)
}
// ReadString will read a string from src. If there are not enough bytes it
// will return false.
func ReadString(src []byte) (string, []byte, bool) {
return readstring(src)
}
// AppendDocumentStart reserves a document's length and returns the index where the length begins.
// This index can later be used to write the length of the document.
func AppendDocumentStart(dst []byte) (index int32, b []byte) {
// TODO(skriptble): We really need AppendDocumentStart and AppendDocumentEnd. AppendDocumentStart would handle calling
// TODO ReserveLength and providing the index of the start of the document. AppendDocumentEnd would handle taking that
// TODO start index, adding the null byte, calculating the length, and filling in the length at the start of the
// TODO document.
return ReserveLength(dst)
}
// AppendDocumentStartInline functions the same as AppendDocumentStart but takes a pointer to the
// index int32 which allows this function to be used inline.
func AppendDocumentStartInline(dst []byte, index *int32) []byte {
idx, doc := AppendDocumentStart(dst)
*index = idx
return doc
}
// AppendDocumentElementStart writes a document element header and then reserves the length bytes.
func AppendDocumentElementStart(dst []byte, key string) (index int32, b []byte) {
return AppendDocumentStart(AppendHeader(dst, bsontype.EmbeddedDocument, key))
}
// AppendDocumentEnd writes the null byte for a document and updates the length of the document.
// The index should be the beginning of the document's length bytes.
func AppendDocumentEnd(dst []byte, index int32) ([]byte, error) {
if int(index) > len(dst)-4 {
return dst, fmt.Errorf("not enough bytes available after index to write length")
}
dst = append(dst, 0x00)
dst = UpdateLength(dst, index, int32(len(dst[index:])))
return dst, nil
}
// AppendDocument will append doc to dst and return the extended buffer.
func AppendDocument(dst []byte, doc []byte) []byte { return append(dst, doc...) }
// AppendDocumentElement will append a BSON embedded document element using key
// and doc to dst and return the extended buffer.
func AppendDocumentElement(dst []byte, key string, doc []byte) []byte {
return AppendDocument(AppendHeader(dst, bsontype.EmbeddedDocument, key), doc)
}
// BuildDocument will create a document with the given slice of elements and will append
// it to dst and return the extended buffer.
func BuildDocument(dst []byte, elems ...[]byte) []byte {
idx, dst := ReserveLength(dst)
for _, elem := range elems {
dst = append(dst, elem...)
}
dst = append(dst, 0x00)
dst = UpdateLength(dst, idx, int32(len(dst[idx:])))
return dst
}
// BuildDocumentValue creates an Embedded Document value from the given elements.
func BuildDocumentValue(elems ...[]byte) Value {
return Value{Type: bsontype.EmbeddedDocument, Data: BuildDocument(nil, elems...)}
}
// BuildDocumentElement will append a BSON embedded document elemnt using key and the provided
// elements and return the extended buffer.
func BuildDocumentElement(dst []byte, key string, elems ...[]byte) []byte {
return BuildDocument(AppendHeader(dst, bsontype.EmbeddedDocument, key), elems...)
}
// BuildDocumentFromElements is an alaias for the BuildDocument function.
var BuildDocumentFromElements = BuildDocument
// ReadDocument will read a document from src. If there are not enough bytes it
// will return false.
func ReadDocument(src []byte) (doc Document, rem []byte, ok bool) { return readLengthBytes(src) }
// AppendArrayStart appends the length bytes to an array and then returns the index of the start
// of those length bytes.
func AppendArrayStart(dst []byte) (index int32, b []byte) { return ReserveLength(dst) }
// AppendArrayElementStart appends an array element header and then the length bytes for an array,
// returning the index where the length starts.
func AppendArrayElementStart(dst []byte, key string) (index int32, b []byte) {
return AppendArrayStart(AppendHeader(dst, bsontype.Array, key))
}
// AppendArrayEnd appends the null byte to an array and calculates the length, inserting that
// calculated length starting at index.
func AppendArrayEnd(dst []byte, index int32) ([]byte, error) { return AppendDocumentEnd(dst, index) }
// AppendArray will append arr to dst and return the extended buffer.
func AppendArray(dst []byte, arr []byte) []byte { return append(dst, arr...) }
// AppendArrayElement will append a BSON array element using key and arr to dst
// and return the extended buffer.
func AppendArrayElement(dst []byte, key string, arr []byte) []byte {
return AppendArray(AppendHeader(dst, bsontype.Array, key), arr)
}
// BuildArray will append a BSON array to dst built from values.
func BuildArray(dst []byte, values ...Value) []byte {
idx, dst := ReserveLength(dst)
for pos, val := range values {
dst = AppendValueElement(dst, strconv.Itoa(pos), val)
}
dst = append(dst, 0x00)
dst = UpdateLength(dst, idx, int32(len(dst[idx:])))
return dst
}
// BuildArrayElement will create an array element using the provided values.
func BuildArrayElement(dst []byte, key string, values ...Value) []byte {
return BuildArray(AppendHeader(dst, bsontype.Array, key), values...)
}
// ReadArray will read an array from src. If there are not enough bytes it
// will return false.
func ReadArray(src []byte) (arr Array, rem []byte, ok bool) { return readLengthBytes(src) }
// AppendBinary will append subtype and b to dst and return the extended buffer.
func AppendBinary(dst []byte, subtype byte, b []byte) []byte {
if subtype == 0x02 {
return appendBinarySubtype2(dst, subtype, b)
}
dst = append(appendLength(dst, int32(len(b))), subtype)
return append(dst, b...)
}
// AppendBinaryElement will append a BSON binary element using key, subtype, and
// b to dst and return the extended buffer.
func AppendBinaryElement(dst []byte, key string, subtype byte, b []byte) []byte {
return AppendBinary(AppendHeader(dst, bsontype.Binary, key), subtype, b)
}
// ReadBinary will read a subtype and bin from src. If there are not enough bytes it
// will return false.
func ReadBinary(src []byte) (subtype byte, bin []byte, rem []byte, ok bool) {
length, rem, ok := ReadLength(src)
if !ok {
return 0x00, nil, src, false
}
if len(rem) < 1 { // subtype
return 0x00, nil, src, false
}
subtype, rem = rem[0], rem[1:]
if len(rem) < int(length) {
return 0x00, nil, src, false
}
if subtype == 0x02 {
length, rem, ok = ReadLength(rem)
if !ok || len(rem) < int(length) {
return 0x00, nil, src, false
}
}
return subtype, rem[:length], rem[length:], true
}
// AppendUndefinedElement will append a BSON undefined element using key to dst
// and return the extended buffer.
func AppendUndefinedElement(dst []byte, key string) []byte {
return AppendHeader(dst, bsontype.Undefined, key)
}
// AppendObjectID will append oid to dst and return the extended buffer.
func AppendObjectID(dst []byte, oid primitive.ObjectID) []byte { return append(dst, oid[:]...) }
// AppendObjectIDElement will append a BSON ObjectID element using key and oid to dst
// and return the extended buffer.
func AppendObjectIDElement(dst []byte, key string, oid primitive.ObjectID) []byte {
return AppendObjectID(AppendHeader(dst, bsontype.ObjectID, key), oid)
}
// ReadObjectID will read an ObjectID from src. If there are not enough bytes it
// will return false.
func ReadObjectID(src []byte) (primitive.ObjectID, []byte, bool) {
if len(src) < 12 {
return primitive.ObjectID{}, src, false
}
var oid primitive.ObjectID
copy(oid[:], src[0:12])
return oid, src[12:], true
}
// AppendBoolean will append b to dst and return the extended buffer.
func AppendBoolean(dst []byte, b bool) []byte {
if b {
return append(dst, 0x01)
}
return append(dst, 0x00)
}
// AppendBooleanElement will append a BSON boolean element using key and b to dst
// and return the extended buffer.
func AppendBooleanElement(dst []byte, key string, b bool) []byte {
return AppendBoolean(AppendHeader(dst, bsontype.Boolean, key), b)
}
// ReadBoolean will read a bool from src. If there are not enough bytes it
// will return false.
func ReadBoolean(src []byte) (bool, []byte, bool) {
if len(src) < 1 {
return false, src, false
}
return src[0] == 0x01, src[1:], true
}
// AppendDateTime will append dt to dst and return the extended buffer.
func AppendDateTime(dst []byte, dt int64) []byte { return appendi64(dst, dt) }
// AppendDateTimeElement will append a BSON datetime element using key and dt to dst
// and return the extended buffer.
func AppendDateTimeElement(dst []byte, key string, dt int64) []byte {
return AppendDateTime(AppendHeader(dst, bsontype.DateTime, key), dt)
}
// ReadDateTime will read an int64 datetime from src. If there are not enough bytes it
// will return false.
func ReadDateTime(src []byte) (int64, []byte, bool) { return readi64(src) }
// AppendTime will append time as a BSON DateTime to dst and return the extended buffer.
func AppendTime(dst []byte, t time.Time) []byte {
return AppendDateTime(dst, t.Unix()*1000+int64(t.Nanosecond()/1e6))
}
// AppendTimeElement will append a BSON datetime element using key and dt to dst
// and return the extended buffer.
func AppendTimeElement(dst []byte, key string, t time.Time) []byte {
return AppendTime(AppendHeader(dst, bsontype.DateTime, key), t)
}
// ReadTime will read an time.Time datetime from src. If there are not enough bytes it
// will return false.
func ReadTime(src []byte) (time.Time, []byte, bool) {
dt, rem, ok := readi64(src)
return time.Unix(dt/1e3, dt%1e3*1e6), rem, ok
}
// AppendNullElement will append a BSON null element using key to dst
// and return the extended buffer.
func AppendNullElement(dst []byte, key string) []byte { return AppendHeader(dst, bsontype.Null, key) }
// AppendRegex will append pattern and options to dst and return the extended buffer.
func AppendRegex(dst []byte, pattern, options string) []byte {
if !isValidCString(pattern) || !isValidCString(options) {
panic(invalidRegexPanicMsg)
}
return append(dst, pattern+nullTerminator+options+nullTerminator...)
}
// AppendRegexElement will append a BSON regex element using key, pattern, and
// options to dst and return the extended buffer.
func AppendRegexElement(dst []byte, key, pattern, options string) []byte {
return AppendRegex(AppendHeader(dst, bsontype.Regex, key), pattern, options)
}
// ReadRegex will read a pattern and options from src. If there are not enough bytes it
// will return false.
func ReadRegex(src []byte) (pattern, options string, rem []byte, ok bool) {
pattern, rem, ok = readcstring(src)
if !ok {
return "", "", src, false
}
options, rem, ok = readcstring(rem)
if !ok {
return "", "", src, false
}
return pattern, options, rem, true
}
// AppendDBPointer will append ns and oid to dst and return the extended buffer.
func AppendDBPointer(dst []byte, ns string, oid primitive.ObjectID) []byte {
return append(appendstring(dst, ns), oid[:]...)
}
// AppendDBPointerElement will append a BSON DBPointer element using key, ns,
// and oid to dst and return the extended buffer.
func AppendDBPointerElement(dst []byte, key, ns string, oid primitive.ObjectID) []byte {
return AppendDBPointer(AppendHeader(dst, bsontype.DBPointer, key), ns, oid)
}
// ReadDBPointer will read a ns and oid from src. If there are not enough bytes it
// will return false.
func ReadDBPointer(src []byte) (ns string, oid primitive.ObjectID, rem []byte, ok bool) {
ns, rem, ok = readstring(src)
if !ok {
return "", primitive.ObjectID{}, src, false
}
oid, rem, ok = ReadObjectID(rem)
if !ok {
return "", primitive.ObjectID{}, src, false
}
return ns, oid, rem, true
}
// AppendJavaScript will append js to dst and return the extended buffer.
func AppendJavaScript(dst []byte, js string) []byte { return appendstring(dst, js) }
// AppendJavaScriptElement will append a BSON JavaScript element using key and
// js to dst and return the extended buffer.
func AppendJavaScriptElement(dst []byte, key, js string) []byte {
return AppendJavaScript(AppendHeader(dst, bsontype.JavaScript, key), js)
}
// ReadJavaScript will read a js string from src. If there are not enough bytes it
// will return false.
func ReadJavaScript(src []byte) (js string, rem []byte, ok bool) { return readstring(src) }
// AppendSymbol will append symbol to dst and return the extended buffer.
func AppendSymbol(dst []byte, symbol string) []byte { return appendstring(dst, symbol) }
// AppendSymbolElement will append a BSON symbol element using key and symbol to dst
// and return the extended buffer.
func AppendSymbolElement(dst []byte, key, symbol string) []byte {
return AppendSymbol(AppendHeader(dst, bsontype.Symbol, key), symbol)
}
// ReadSymbol will read a symbol string from src. If there are not enough bytes it
// will return false.
func ReadSymbol(src []byte) (symbol string, rem []byte, ok bool) { return readstring(src) }
// AppendCodeWithScope will append code and scope to dst and return the extended buffer.
func AppendCodeWithScope(dst []byte, code string, scope []byte) []byte {
length := int32(4 + 4 + len(code) + 1 + len(scope)) // length of cws, length of code, code, 0x00, scope
dst = appendLength(dst, length)
return append(appendstring(dst, code), scope...)
}
// AppendCodeWithScopeElement will append a BSON code with scope element using
// key, code, and scope to dst
// and return the extended buffer.
func AppendCodeWithScopeElement(dst []byte, key, code string, scope []byte) []byte {
return AppendCodeWithScope(AppendHeader(dst, bsontype.CodeWithScope, key), code, scope)
}
// ReadCodeWithScope will read code and scope from src. If there are not enough bytes it
// will return false.
func ReadCodeWithScope(src []byte) (code string, scope []byte, rem []byte, ok bool) {
length, rem, ok := ReadLength(src)
if !ok || len(src) < int(length) {
return "", nil, src, false
}
code, rem, ok = readstring(rem)
if !ok {
return "", nil, src, false
}
scope, rem, ok = ReadDocument(rem)
if !ok {
return "", nil, src, false
}
return code, scope, rem, true
}
// AppendInt32 will append i32 to dst and return the extended buffer.
func AppendInt32(dst []byte, i32 int32) []byte { return appendi32(dst, i32) }
// AppendInt32Element will append a BSON int32 element using key and i32 to dst
// and return the extended buffer.
func AppendInt32Element(dst []byte, key string, i32 int32) []byte {
return AppendInt32(AppendHeader(dst, bsontype.Int32, key), i32)
}
// ReadInt32 will read an int32 from src. If there are not enough bytes it
// will return false.
func ReadInt32(src []byte) (int32, []byte, bool) { return readi32(src) }
// AppendTimestamp will append t and i to dst and return the extended buffer.
func AppendTimestamp(dst []byte, t, i uint32) []byte {
return appendu32(appendu32(dst, i), t) // i is the lower 4 bytes, t is the higher 4 bytes
}
// AppendTimestampElement will append a BSON timestamp element using key, t, and
// i to dst and return the extended buffer.
func AppendTimestampElement(dst []byte, key string, t, i uint32) []byte {
return AppendTimestamp(AppendHeader(dst, bsontype.Timestamp, key), t, i)
}
// ReadTimestamp will read t and i from src. If there are not enough bytes it
// will return false.
func ReadTimestamp(src []byte) (t, i uint32, rem []byte, ok bool) {
i, rem, ok = readu32(src)
if !ok {
return 0, 0, src, false
}
t, rem, ok = readu32(rem)
if !ok {
return 0, 0, src, false
}
return t, i, rem, true
}
// AppendInt64 will append i64 to dst and return the extended buffer.
func AppendInt64(dst []byte, i64 int64) []byte { return appendi64(dst, i64) }
// AppendInt64Element will append a BSON int64 element using key and i64 to dst
// and return the extended buffer.
func AppendInt64Element(dst []byte, key string, i64 int64) []byte {
return AppendInt64(AppendHeader(dst, bsontype.Int64, key), i64)
}
// ReadInt64 will read an int64 from src. If there are not enough bytes it
// will return false.
func ReadInt64(src []byte) (int64, []byte, bool) { return readi64(src) }
// AppendDecimal128 will append d128 to dst and return the extended buffer.
func AppendDecimal128(dst []byte, d128 primitive.Decimal128) []byte {
high, low := d128.GetBytes()
return appendu64(appendu64(dst, low), high)
}
// AppendDecimal128Element will append a BSON primitive.28 element using key and
// d128 to dst and return the extended buffer.
func AppendDecimal128Element(dst []byte, key string, d128 primitive.Decimal128) []byte {
return AppendDecimal128(AppendHeader(dst, bsontype.Decimal128, key), d128)
}
// ReadDecimal128 will read a primitive.Decimal128 from src. If there are not enough bytes it
// will return false.
func ReadDecimal128(src []byte) (primitive.Decimal128, []byte, bool) {
l, rem, ok := readu64(src)
if !ok {
return primitive.Decimal128{}, src, false
}
h, rem, ok := readu64(rem)
if !ok {
return primitive.Decimal128{}, src, false
}
return primitive.NewDecimal128(h, l), rem, true
}
// AppendMaxKeyElement will append a BSON max key element using key to dst
// and return the extended buffer.
func AppendMaxKeyElement(dst []byte, key string) []byte {
return AppendHeader(dst, bsontype.MaxKey, key)
}
// AppendMinKeyElement will append a BSON min key element using key to dst
// and return the extended buffer.
func AppendMinKeyElement(dst []byte, key string) []byte {
return AppendHeader(dst, bsontype.MinKey, key)
}
// EqualValue will return true if the two values are equal.
func EqualValue(t1, t2 bsontype.Type, v1, v2 []byte) bool {
if t1 != t2 {
return false
}
v1, _, ok := readValue(v1, t1)
if !ok {
return false
}
v2, _, ok = readValue(v2, t2)
if !ok {
return false
}
return bytes.Equal(v1, v2)
}
// valueLength will determine the length of the next value contained in src as if it
// is type t. The returned bool will be false if there are not enough bytes in src for
// a value of type t.
func valueLength(src []byte, t bsontype.Type) (int32, bool) {
var length int32
ok := true
switch t {
case bsontype.Array, bsontype.EmbeddedDocument, bsontype.CodeWithScope:
length, _, ok = ReadLength(src)
case bsontype.Binary:
length, _, ok = ReadLength(src)
length += 4 + 1 // binary length + subtype byte
case bsontype.Boolean:
length = 1
case bsontype.DBPointer:
length, _, ok = ReadLength(src)
length += 4 + 12 // string length + ObjectID length
case bsontype.DateTime, bsontype.Double, bsontype.Int64, bsontype.Timestamp:
length = 8
case bsontype.Decimal128:
length = 16
case bsontype.Int32:
length = 4
case bsontype.JavaScript, bsontype.String, bsontype.Symbol:
length, _, ok = ReadLength(src)
length += 4
case bsontype.MaxKey, bsontype.MinKey, bsontype.Null, bsontype.Undefined:
length = 0
case bsontype.ObjectID:
length = 12
case bsontype.Regex:
regex := bytes.IndexByte(src, 0x00)
if regex < 0 {
ok = false
break
}
pattern := bytes.IndexByte(src[regex+1:], 0x00)
if pattern < 0 {
ok = false
break
}
length = int32(int64(regex) + 1 + int64(pattern) + 1)
default:
ok = false
}
return length, ok
}
func readValue(src []byte, t bsontype.Type) ([]byte, []byte, bool) {
length, ok := valueLength(src, t)
if !ok || int(length) > len(src) {
return nil, src, false
}
return src[:length], src[length:], true
}
// ReserveLength reserves the space required for length and returns the index where to write the length
// and the []byte with reserved space.
func ReserveLength(dst []byte) (int32, []byte) {
index := len(dst)
return int32(index), append(dst, 0x00, 0x00, 0x00, 0x00)
}
// UpdateLength updates the length at index with length and returns the []byte.
func UpdateLength(dst []byte, index, length int32) []byte {
dst[index] = byte(length)
dst[index+1] = byte(length >> 8)
dst[index+2] = byte(length >> 16)
dst[index+3] = byte(length >> 24)
return dst
}
func appendLength(dst []byte, l int32) []byte { return appendi32(dst, l) }
func appendi32(dst []byte, i32 int32) []byte {
return append(dst, byte(i32), byte(i32>>8), byte(i32>>16), byte(i32>>24))
}
// ReadLength reads an int32 length from src and returns the length and the remaining bytes. If
// there aren't enough bytes to read a valid length, src is returned unomdified and the returned
// bool will be false.
func ReadLength(src []byte) (int32, []byte, bool) {
ln, src, ok := readi32(src)
if ln < 0 {
return ln, src, false
}
return ln, src, ok
}
func readi32(src []byte) (int32, []byte, bool) {
if len(src) < 4 {
return 0, src, false
}
return (int32(src[0]) | int32(src[1])<<8 | int32(src[2])<<16 | int32(src[3])<<24), src[4:], true
}
func appendi64(dst []byte, i64 int64) []byte {
return append(dst,
byte(i64), byte(i64>>8), byte(i64>>16), byte(i64>>24),
byte(i64>>32), byte(i64>>40), byte(i64>>48), byte(i64>>56),
)
}
func readi64(src []byte) (int64, []byte, bool) {
if len(src) < 8 {
return 0, src, false
}
i64 := (int64(src[0]) | int64(src[1])<<8 | int64(src[2])<<16 | int64(src[3])<<24 |
int64(src[4])<<32 | int64(src[5])<<40 | int64(src[6])<<48 | int64(src[7])<<56)
return i64, src[8:], true
}
func appendu32(dst []byte, u32 uint32) []byte {
return append(dst, byte(u32), byte(u32>>8), byte(u32>>16), byte(u32>>24))
}
func readu32(src []byte) (uint32, []byte, bool) {
if len(src) < 4 {
return 0, src, false
}
return (uint32(src[0]) | uint32(src[1])<<8 | uint32(src[2])<<16 | uint32(src[3])<<24), src[4:], true
}
func appendu64(dst []byte, u64 uint64) []byte {
return append(dst,
byte(u64), byte(u64>>8), byte(u64>>16), byte(u64>>24),
byte(u64>>32), byte(u64>>40), byte(u64>>48), byte(u64>>56),
)
}
func readu64(src []byte) (uint64, []byte, bool) {
if len(src) < 8 {
return 0, src, false
}
u64 := (uint64(src[0]) | uint64(src[1])<<8 | uint64(src[2])<<16 | uint64(src[3])<<24 |
uint64(src[4])<<32 | uint64(src[5])<<40 | uint64(src[6])<<48 | uint64(src[7])<<56)
return u64, src[8:], true
}
// keep in sync with readcstringbytes
func readcstring(src []byte) (string, []byte, bool) {
idx := bytes.IndexByte(src, 0x00)
if idx < 0 {
return "", src, false
}
return string(src[:idx]), src[idx+1:], true
}
// keep in sync with readcstring
func readcstringbytes(src []byte) ([]byte, []byte, bool) {
idx := bytes.IndexByte(src, 0x00)
if idx < 0 {
return nil, src, false
}
return src[:idx], src[idx+1:], true
}
func appendstring(dst []byte, s string) []byte {
l := int32(len(s) + 1)
dst = appendLength(dst, l)
dst = append(dst, s...)
return append(dst, 0x00)
}
func readstring(src []byte) (string, []byte, bool) {
l, rem, ok := ReadLength(src)
if !ok {
return "", src, false
}
if len(src[4:]) < int(l) || l == 0 {
return "", src, false
}
return string(rem[:l-1]), rem[l:], true
}
// readLengthBytes attempts to read a length and that number of bytes. This
// function requires that the length include the four bytes for itself.
func readLengthBytes(src []byte) ([]byte, []byte, bool) {
l, _, ok := ReadLength(src)
if !ok {
return nil, src, false
}
if len(src) < int(l) {
return nil, src, false
}
return src[:l], src[l:], true
}
func appendBinarySubtype2(dst []byte, subtype byte, b []byte) []byte {
dst = appendLength(dst, int32(len(b)+4)) // The bytes we'll encode need to be 4 larger for the length bytes
dst = append(dst, subtype)
dst = appendLength(dst, int32(len(b)))
return append(dst, b...)
}
func isValidCString(cs string) bool {
return !strings.ContainsRune(cs, '\x00')
}
+386
View File
@@ -0,0 +1,386 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsoncore
import (
"bytes"
"errors"
"fmt"
"io"
"strconv"
"go.mongodb.org/mongo-driver/bson/bsontype"
)
// ValidationError is an error type returned when attempting to validate a document or array.
type ValidationError string
func (ve ValidationError) Error() string { return string(ve) }
// NewDocumentLengthError creates and returns an error for when the length of a document exceeds the
// bytes available.
func NewDocumentLengthError(length, rem int) error {
return lengthError("document", length, rem)
}
func lengthError(bufferType string, length, rem int) error {
return ValidationError(fmt.Sprintf("%v length exceeds available bytes. length=%d remainingBytes=%d",
bufferType, length, rem))
}
// InsufficientBytesError indicates that there were not enough bytes to read the next component.
type InsufficientBytesError struct {
Source []byte
Remaining []byte
}
// NewInsufficientBytesError creates a new InsufficientBytesError with the given Document and
// remaining bytes.
func NewInsufficientBytesError(src, rem []byte) InsufficientBytesError {
return InsufficientBytesError{Source: src, Remaining: rem}
}
// Error implements the error interface.
func (ibe InsufficientBytesError) Error() string {
return "too few bytes to read next component"
}
// Equal checks that err2 also is an ErrTooSmall.
func (ibe InsufficientBytesError) Equal(err2 error) bool {
switch err2.(type) {
case InsufficientBytesError:
return true
default:
return false
}
}
// InvalidDepthTraversalError is returned when attempting a recursive Lookup when one component of
// the path is neither an embedded document nor an array.
type InvalidDepthTraversalError struct {
Key string
Type bsontype.Type
}
func (idte InvalidDepthTraversalError) Error() string {
return fmt.Sprintf(
"attempt to traverse into %s, but it's type is %s, not %s nor %s",
idte.Key, idte.Type, bsontype.EmbeddedDocument, bsontype.Array,
)
}
// ErrMissingNull is returned when a document or array's last byte is not null.
const ErrMissingNull ValidationError = "document or array end is missing null byte"
// ErrInvalidLength indicates that a length in a binary representation of a BSON document or array
// is invalid.
const ErrInvalidLength ValidationError = "document or array length is invalid"
// ErrNilReader indicates that an operation was attempted on a nil io.Reader.
var ErrNilReader = errors.New("nil reader")
// ErrEmptyKey indicates that no key was provided to a Lookup method.
var ErrEmptyKey = errors.New("empty key provided")
// ErrElementNotFound indicates that an Element matching a certain condition does not exist.
var ErrElementNotFound = errors.New("element not found")
// ErrOutOfBounds indicates that an index provided to access something was invalid.
var ErrOutOfBounds = errors.New("out of bounds")
// Document is a raw bytes representation of a BSON document.
type Document []byte
// NewDocumentFromReader reads a document from r. This function will only validate the length is
// correct and that the document ends with a null byte.
func NewDocumentFromReader(r io.Reader) (Document, error) {
return newBufferFromReader(r)
}
func newBufferFromReader(r io.Reader) ([]byte, error) {
if r == nil {
return nil, ErrNilReader
}
var lengthBytes [4]byte
// ReadFull guarantees that we will have read at least len(lengthBytes) if err == nil
_, err := io.ReadFull(r, lengthBytes[:])
if err != nil {
return nil, err
}
length, _, _ := readi32(lengthBytes[:]) // ignore ok since we always have enough bytes to read a length
if length < 0 {
return nil, ErrInvalidLength
}
buffer := make([]byte, length)
copy(buffer, lengthBytes[:])
_, err = io.ReadFull(r, buffer[4:])
if err != nil {
return nil, err
}
if buffer[length-1] != 0x00 {
return nil, ErrMissingNull
}
return buffer, nil
}
// Lookup searches the document, potentially recursively, for the given key. If there are multiple
// keys provided, this method will recurse down, as long as the top and intermediate nodes are
// either documents or arrays. If an error occurs or if the value doesn't exist, an empty Value is
// returned.
func (d Document) Lookup(key ...string) Value {
val, _ := d.LookupErr(key...)
return val
}
// LookupErr is the same as Lookup, except it returns an error in addition to an empty Value.
func (d Document) LookupErr(key ...string) (Value, error) {
if len(key) < 1 {
return Value{}, ErrEmptyKey
}
length, rem, ok := ReadLength(d)
if !ok {
return Value{}, NewInsufficientBytesError(d, rem)
}
length -= 4
var elem Element
for length > 1 {
elem, rem, ok = ReadElement(rem)
length -= int32(len(elem))
if !ok {
return Value{}, NewInsufficientBytesError(d, rem)
}
// We use `KeyBytes` rather than `Key` to avoid a needless string alloc.
if string(elem.KeyBytes()) != key[0] {
continue
}
if len(key) > 1 {
tt := bsontype.Type(elem[0])
switch tt {
case bsontype.EmbeddedDocument:
val, err := elem.Value().Document().LookupErr(key[1:]...)
if err != nil {
return Value{}, err
}
return val, nil
case bsontype.Array:
// Convert to Document to continue Lookup recursion.
val, err := Document(elem.Value().Array()).LookupErr(key[1:]...)
if err != nil {
return Value{}, err
}
return val, nil
default:
return Value{}, InvalidDepthTraversalError{Key: elem.Key(), Type: tt}
}
}
return elem.ValueErr()
}
return Value{}, ErrElementNotFound
}
// Index searches for and retrieves the element at the given index. This method will panic if
// the document is invalid or if the index is out of bounds.
func (d Document) Index(index uint) Element {
elem, err := d.IndexErr(index)
if err != nil {
panic(err)
}
return elem
}
// IndexErr searches for and retrieves the element at the given index.
func (d Document) IndexErr(index uint) (Element, error) {
return indexErr(d, index)
}
func indexErr(b []byte, index uint) (Element, error) {
length, rem, ok := ReadLength(b)
if !ok {
return nil, NewInsufficientBytesError(b, rem)
}
length -= 4
var current uint
var elem Element
for length > 1 {
elem, rem, ok = ReadElement(rem)
length -= int32(len(elem))
if !ok {
return nil, NewInsufficientBytesError(b, rem)
}
if current != index {
current++
continue
}
return elem, nil
}
return nil, ErrOutOfBounds
}
// DebugString outputs a human readable version of Document. It will attempt to stringify the
// valid components of the document even if the entire document is not valid.
func (d Document) DebugString() string {
if len(d) < 5 {
return "<malformed>"
}
var buf bytes.Buffer
buf.WriteString("Document")
length, rem, _ := ReadLength(d) // We know we have enough bytes to read the length
buf.WriteByte('(')
buf.WriteString(strconv.Itoa(int(length)))
length -= 4
buf.WriteString("){")
var elem Element
var ok bool
for length > 1 {
elem, rem, ok = ReadElement(rem)
length -= int32(len(elem))
if !ok {
buf.WriteString(fmt.Sprintf("<malformed (%d)>", length))
break
}
fmt.Fprintf(&buf, "%s ", elem.DebugString())
}
buf.WriteByte('}')
return buf.String()
}
// String outputs an ExtendedJSON version of Document. If the document is not valid, this method
// returns an empty string.
func (d Document) String() string {
if len(d) < 5 {
return ""
}
var buf bytes.Buffer
buf.WriteByte('{')
length, rem, _ := ReadLength(d) // We know we have enough bytes to read the length
length -= 4
var elem Element
var ok bool
first := true
for length > 1 {
if !first {
buf.WriteByte(',')
}
elem, rem, ok = ReadElement(rem)
length -= int32(len(elem))
if !ok {
return ""
}
fmt.Fprintf(&buf, "%s", elem.String())
first = false
}
buf.WriteByte('}')
return buf.String()
}
// Elements returns this document as a slice of elements. The returned slice will contain valid
// elements. If the document is not valid, the elements up to the invalid point will be returned
// along with an error.
func (d Document) Elements() ([]Element, error) {
length, rem, ok := ReadLength(d)
if !ok {
return nil, NewInsufficientBytesError(d, rem)
}
length -= 4
var elem Element
var elems []Element
for length > 1 {
elem, rem, ok = ReadElement(rem)
length -= int32(len(elem))
if !ok {
return elems, NewInsufficientBytesError(d, rem)
}
if err := elem.Validate(); err != nil {
return elems, err
}
elems = append(elems, elem)
}
return elems, nil
}
// Values returns this document as a slice of values. The returned slice will contain valid values.
// If the document is not valid, the values up to the invalid point will be returned along with an
// error.
func (d Document) Values() ([]Value, error) {
return values(d)
}
func values(b []byte) ([]Value, error) {
length, rem, ok := ReadLength(b)
if !ok {
return nil, NewInsufficientBytesError(b, rem)
}
length -= 4
var elem Element
var vals []Value
for length > 1 {
elem, rem, ok = ReadElement(rem)
length -= int32(len(elem))
if !ok {
return vals, NewInsufficientBytesError(b, rem)
}
if err := elem.Value().Validate(); err != nil {
return vals, err
}
vals = append(vals, elem.Value())
}
return vals, nil
}
// Validate validates the document and ensures the elements contained within are valid.
func (d Document) Validate() error {
length, rem, ok := ReadLength(d)
if !ok {
return NewInsufficientBytesError(d, rem)
}
if int(length) > len(d) {
return NewDocumentLengthError(int(length), len(d))
}
if d[length-1] != 0x00 {
return ErrMissingNull
}
length -= 4
var elem Element
for length > 1 {
elem, rem, ok = ReadElement(rem)
length -= int32(len(elem))
if !ok {
return NewInsufficientBytesError(d, rem)
}
err := elem.Validate()
if err != nil {
return err
}
}
if len(rem) < 1 || rem[0] != 0x00 {
return ErrMissingNull
}
return nil
}
@@ -0,0 +1,189 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsoncore
import (
"errors"
"io"
"go.mongodb.org/mongo-driver/bson/bsontype"
)
// DocumentSequenceStyle is used to represent how a document sequence is laid out in a slice of
// bytes.
type DocumentSequenceStyle uint32
// These constants are the valid styles for a DocumentSequence.
const (
_ DocumentSequenceStyle = iota
SequenceStyle
ArrayStyle
)
// DocumentSequence represents a sequence of documents. The Style field indicates how the documents
// are laid out inside of the Data field.
type DocumentSequence struct {
Style DocumentSequenceStyle
Data []byte
Pos int
}
// ErrCorruptedDocument is returned when a full document couldn't be read from the sequence.
var ErrCorruptedDocument = errors.New("invalid DocumentSequence: corrupted document")
// ErrNonDocument is returned when a DocumentSequence contains a non-document BSON value.
var ErrNonDocument = errors.New("invalid DocumentSequence: a non-document value was found in sequence")
// ErrInvalidDocumentSequenceStyle is returned when an unknown DocumentSequenceStyle is set on a
// DocumentSequence.
var ErrInvalidDocumentSequenceStyle = errors.New("invalid DocumentSequenceStyle")
// DocumentCount returns the number of documents in the sequence.
func (ds *DocumentSequence) DocumentCount() int {
if ds == nil {
return 0
}
switch ds.Style {
case SequenceStyle:
var count int
var ok bool
rem := ds.Data
for len(rem) > 0 {
_, rem, ok = ReadDocument(rem)
if !ok {
return 0
}
count++
}
return count
case ArrayStyle:
_, rem, ok := ReadLength(ds.Data)
if !ok {
return 0
}
var count int
for len(rem) > 1 {
_, rem, ok = ReadElement(rem)
if !ok {
return 0
}
count++
}
return count
default:
return 0
}
}
// Empty returns true if the sequence is empty. It always returns true for unknown sequence styles.
func (ds *DocumentSequence) Empty() bool {
if ds == nil {
return true
}
switch ds.Style {
case SequenceStyle:
return len(ds.Data) == 0
case ArrayStyle:
return len(ds.Data) <= 5
default:
return true
}
}
//ResetIterator resets the iteration point for the Next method to the beginning of the document
//sequence.
func (ds *DocumentSequence) ResetIterator() {
if ds == nil {
return
}
ds.Pos = 0
}
// Documents returns a slice of the documents. If nil either the Data field is also nil or could not
// be properly read.
func (ds *DocumentSequence) Documents() ([]Document, error) {
if ds == nil {
return nil, nil
}
switch ds.Style {
case SequenceStyle:
rem := ds.Data
var docs []Document
var doc Document
var ok bool
for {
doc, rem, ok = ReadDocument(rem)
if !ok {
if len(rem) == 0 {
break
}
return nil, ErrCorruptedDocument
}
docs = append(docs, doc)
}
return docs, nil
case ArrayStyle:
if len(ds.Data) == 0 {
return nil, nil
}
vals, err := Document(ds.Data).Values()
if err != nil {
return nil, ErrCorruptedDocument
}
docs := make([]Document, 0, len(vals))
for _, v := range vals {
if v.Type != bsontype.EmbeddedDocument {
return nil, ErrNonDocument
}
docs = append(docs, v.Data)
}
return docs, nil
default:
return nil, ErrInvalidDocumentSequenceStyle
}
}
// Next retrieves the next document from this sequence and returns it. This method will return
// io.EOF when it has reached the end of the sequence.
func (ds *DocumentSequence) Next() (Document, error) {
if ds == nil || ds.Pos >= len(ds.Data) {
return nil, io.EOF
}
switch ds.Style {
case SequenceStyle:
doc, _, ok := ReadDocument(ds.Data[ds.Pos:])
if !ok {
return nil, ErrCorruptedDocument
}
ds.Pos += len(doc)
return doc, nil
case ArrayStyle:
if ds.Pos < 4 {
if len(ds.Data) < 4 {
return nil, ErrCorruptedDocument
}
ds.Pos = 4 // Skip the length of the document
}
if len(ds.Data[ds.Pos:]) == 1 && ds.Data[ds.Pos] == 0x00 {
return nil, io.EOF // At the end of the document
}
elem, _, ok := ReadElement(ds.Data[ds.Pos:])
if !ok {
return nil, ErrCorruptedDocument
}
ds.Pos += len(elem)
val := elem.Value()
if val.Type != bsontype.EmbeddedDocument {
return nil, ErrNonDocument
}
return val.Data, nil
default:
return nil, ErrInvalidDocumentSequenceStyle
}
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsoncore
import (
"bytes"
"fmt"
"go.mongodb.org/mongo-driver/bson/bsontype"
)
// MalformedElementError represents a class of errors that RawElement methods return.
type MalformedElementError string
func (mee MalformedElementError) Error() string { return string(mee) }
// ErrElementMissingKey is returned when a RawElement is missing a key.
const ErrElementMissingKey MalformedElementError = "element is missing key"
// ErrElementMissingType is returned when a RawElement is missing a type.
const ErrElementMissingType MalformedElementError = "element is missing type"
// Element is a raw bytes representation of a BSON element.
type Element []byte
// Key returns the key for this element. If the element is not valid, this method returns an empty
// string. If knowing if the element is valid is important, use KeyErr.
func (e Element) Key() string {
key, _ := e.KeyErr()
return key
}
// KeyBytes returns the key for this element as a []byte. If the element is not valid, this method
// returns an empty string. If knowing if the element is valid is important, use KeyErr. This method
// will not include the null byte at the end of the key in the slice of bytes.
func (e Element) KeyBytes() []byte {
key, _ := e.KeyBytesErr()
return key
}
// KeyErr returns the key for this element, returning an error if the element is not valid.
func (e Element) KeyErr() (string, error) {
key, err := e.KeyBytesErr()
return string(key), err
}
// KeyBytesErr returns the key for this element as a []byte, returning an error if the element is
// not valid.
func (e Element) KeyBytesErr() ([]byte, error) {
if len(e) <= 0 {
return nil, ErrElementMissingType
}
idx := bytes.IndexByte(e[1:], 0x00)
if idx == -1 {
return nil, ErrElementMissingKey
}
return e[1 : idx+1], nil
}
// Validate ensures the element is a valid BSON element.
func (e Element) Validate() error {
if len(e) < 1 {
return ErrElementMissingType
}
idx := bytes.IndexByte(e[1:], 0x00)
if idx == -1 {
return ErrElementMissingKey
}
return Value{Type: bsontype.Type(e[0]), Data: e[idx+2:]}.Validate()
}
// CompareKey will compare this element's key to key. This method makes it easy to compare keys
// without needing to allocate a string. The key may be null terminated. If a valid key cannot be
// read this method will return false.
func (e Element) CompareKey(key []byte) bool {
if len(e) < 2 {
return false
}
idx := bytes.IndexByte(e[1:], 0x00)
if idx == -1 {
return false
}
if index := bytes.IndexByte(key, 0x00); index > -1 {
key = key[:index]
}
return bytes.Equal(e[1:idx+1], key)
}
// Value returns the value of this element. If the element is not valid, this method returns an
// empty Value. If knowing if the element is valid is important, use ValueErr.
func (e Element) Value() Value {
val, _ := e.ValueErr()
return val
}
// ValueErr returns the value for this element, returning an error if the element is not valid.
func (e Element) ValueErr() (Value, error) {
if len(e) <= 0 {
return Value{}, ErrElementMissingType
}
idx := bytes.IndexByte(e[1:], 0x00)
if idx == -1 {
return Value{}, ErrElementMissingKey
}
val, rem, exists := ReadValue(e[idx+2:], bsontype.Type(e[0]))
if !exists {
return Value{}, NewInsufficientBytesError(e, rem)
}
return val, nil
}
// String implements the fmt.String interface. The output will be in extended JSON format.
func (e Element) String() string {
if len(e) <= 0 {
return ""
}
t := bsontype.Type(e[0])
idx := bytes.IndexByte(e[1:], 0x00)
if idx == -1 {
return ""
}
key, valBytes := []byte(e[1:idx+1]), []byte(e[idx+2:])
val, _, valid := ReadValue(valBytes, t)
if !valid {
return ""
}
return fmt.Sprintf(`"%s": %v`, key, val)
}
// DebugString outputs a human readable version of RawElement. It will attempt to stringify the
// valid components of the element even if the entire element is not valid.
func (e Element) DebugString() string {
if len(e) <= 0 {
return "<malformed>"
}
t := bsontype.Type(e[0])
idx := bytes.IndexByte(e[1:], 0x00)
if idx == -1 {
return fmt.Sprintf(`bson.Element{[%s]<malformed>}`, t)
}
key, valBytes := []byte(e[1:idx+1]), []byte(e[idx+2:])
val, _, valid := ReadValue(valBytes, t)
if !valid {
return fmt.Sprintf(`bson.Element{[%s]"%s": <malformed>}`, t, key)
}
return fmt.Sprintf(`bson.Element{[%s]"%s": %v}`, t, key, val)
}
+223
View File
@@ -0,0 +1,223 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Based on github.com/golang/go by The Go Authors
// See THIRD-PARTY-NOTICES for original license terms.
package bsoncore
import "unicode/utf8"
// safeSet holds the value true if the ASCII character with the given array
// position can be represented inside a JSON string without any further
// escaping.
//
// All values are true except for the ASCII control characters (0-31), the
// double quote ("), and the backslash character ("\").
var safeSet = [utf8.RuneSelf]bool{
' ': true,
'!': true,
'"': false,
'#': true,
'$': true,
'%': true,
'&': true,
'\'': true,
'(': true,
')': true,
'*': true,
'+': true,
',': true,
'-': true,
'.': true,
'/': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
':': true,
';': true,
'<': true,
'=': true,
'>': true,
'?': true,
'@': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'V': true,
'W': true,
'X': true,
'Y': true,
'Z': true,
'[': true,
'\\': false,
']': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'{': true,
'|': true,
'}': true,
'~': true,
'\u007f': true,
}
// htmlSafeSet holds the value true if the ASCII character with the given
// array position can be safely represented inside a JSON string, embedded
// inside of HTML <script> tags, without any additional escaping.
//
// All values are true except for the ASCII control characters (0-31), the
// double quote ("), the backslash character ("\"), HTML opening and closing
// tags ("<" and ">"), and the ampersand ("&").
var htmlSafeSet = [utf8.RuneSelf]bool{
' ': true,
'!': true,
'"': false,
'#': true,
'$': true,
'%': true,
'&': false,
'\'': true,
'(': true,
')': true,
'*': true,
'+': true,
',': true,
'-': true,
'.': true,
'/': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
':': true,
';': true,
'<': false,
'=': true,
'>': false,
'?': true,
'@': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'V': true,
'W': true,
'X': true,
'Y': true,
'Z': true,
'[': true,
'\\': false,
']': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'{': true,
'|': true,
'}': true,
'~': true,
'\u007f': true,
}
+980
View File
@@ -0,0 +1,980 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsoncore
import (
"bytes"
"encoding/base64"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
"unicode/utf8"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// ElementTypeError specifies that a method to obtain a BSON value an incorrect type was called on a bson.Value.
type ElementTypeError struct {
Method string
Type bsontype.Type
}
// Error implements the error interface.
func (ete ElementTypeError) Error() string {
return "Call of " + ete.Method + " on " + ete.Type.String() + " type"
}
// Value represents a BSON value with a type and raw bytes.
type Value struct {
Type bsontype.Type
Data []byte
}
// Validate ensures the value is a valid BSON value.
func (v Value) Validate() error {
_, _, valid := readValue(v.Data, v.Type)
if !valid {
return NewInsufficientBytesError(v.Data, v.Data)
}
return nil
}
// IsNumber returns true if the type of v is a numeric BSON type.
func (v Value) IsNumber() bool {
switch v.Type {
case bsontype.Double, bsontype.Int32, bsontype.Int64, bsontype.Decimal128:
return true
default:
return false
}
}
// AsInt32 returns a BSON number as an int32. If the BSON type is not a numeric one, this method
// will panic.
//
// TODO(skriptble): Add support for Decimal128.
func (v Value) AsInt32() int32 {
if !v.IsNumber() {
panic(ElementTypeError{"bsoncore.Value.AsInt32", v.Type})
}
var i32 int32
switch v.Type {
case bsontype.Double:
f64, _, ok := ReadDouble(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
i32 = int32(f64)
case bsontype.Int32:
var ok bool
i32, _, ok = ReadInt32(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
case bsontype.Int64:
i64, _, ok := ReadInt64(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
i32 = int32(i64)
case bsontype.Decimal128:
panic(ElementTypeError{"bsoncore.Value.AsInt32", v.Type})
}
return i32
}
// AsInt32OK functions the same as AsInt32 but returns a boolean instead of panicking. False
// indicates an error.
//
// TODO(skriptble): Add support for Decimal128.
func (v Value) AsInt32OK() (int32, bool) {
if !v.IsNumber() {
return 0, false
}
var i32 int32
switch v.Type {
case bsontype.Double:
f64, _, ok := ReadDouble(v.Data)
if !ok {
return 0, false
}
i32 = int32(f64)
case bsontype.Int32:
var ok bool
i32, _, ok = ReadInt32(v.Data)
if !ok {
return 0, false
}
case bsontype.Int64:
i64, _, ok := ReadInt64(v.Data)
if !ok {
return 0, false
}
i32 = int32(i64)
case bsontype.Decimal128:
return 0, false
}
return i32, true
}
// AsInt64 returns a BSON number as an int64. If the BSON type is not a numeric one, this method
// will panic.
//
// TODO(skriptble): Add support for Decimal128.
func (v Value) AsInt64() int64 {
if !v.IsNumber() {
panic(ElementTypeError{"bsoncore.Value.AsInt64", v.Type})
}
var i64 int64
switch v.Type {
case bsontype.Double:
f64, _, ok := ReadDouble(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
i64 = int64(f64)
case bsontype.Int32:
var ok bool
i32, _, ok := ReadInt32(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
i64 = int64(i32)
case bsontype.Int64:
var ok bool
i64, _, ok = ReadInt64(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
case bsontype.Decimal128:
panic(ElementTypeError{"bsoncore.Value.AsInt64", v.Type})
}
return i64
}
// AsInt64OK functions the same as AsInt64 but returns a boolean instead of panicking. False
// indicates an error.
//
// TODO(skriptble): Add support for Decimal128.
func (v Value) AsInt64OK() (int64, bool) {
if !v.IsNumber() {
return 0, false
}
var i64 int64
switch v.Type {
case bsontype.Double:
f64, _, ok := ReadDouble(v.Data)
if !ok {
return 0, false
}
i64 = int64(f64)
case bsontype.Int32:
var ok bool
i32, _, ok := ReadInt32(v.Data)
if !ok {
return 0, false
}
i64 = int64(i32)
case bsontype.Int64:
var ok bool
i64, _, ok = ReadInt64(v.Data)
if !ok {
return 0, false
}
case bsontype.Decimal128:
return 0, false
}
return i64, true
}
// AsFloat64 returns a BSON number as an float64. If the BSON type is not a numeric one, this method
// will panic.
//
// TODO(skriptble): Add support for Decimal128.
func (v Value) AsFloat64() float64 { return 0 }
// AsFloat64OK functions the same as AsFloat64 but returns a boolean instead of panicking. False
// indicates an error.
//
// TODO(skriptble): Add support for Decimal128.
func (v Value) AsFloat64OK() (float64, bool) { return 0, false }
// Add will add this value to another. This is currently only implemented for strings and numbers.
// If either value is a string, the other type is coerced into a string and added to the other.
//
// This method will alter v and will attempt to reuse the []byte of v. If the []byte is too small,
// it will be expanded.
func (v *Value) Add(v2 Value) error { return nil }
// Equal compaes v to v2 and returns true if they are equal.
func (v Value) Equal(v2 Value) bool {
if v.Type != v2.Type {
return false
}
return bytes.Equal(v.Data, v2.Data)
}
// String implements the fmt.String interface. This method will return values in extended JSON
// format. If the value is not valid, this returns an empty string
func (v Value) String() string {
switch v.Type {
case bsontype.Double:
f64, ok := v.DoubleOK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$numberDouble":"%s"}`, formatDouble(f64))
case bsontype.String:
str, ok := v.StringValueOK()
if !ok {
return ""
}
return escapeString(str)
case bsontype.EmbeddedDocument:
doc, ok := v.DocumentOK()
if !ok {
return ""
}
return doc.String()
case bsontype.Array:
arr, ok := v.ArrayOK()
if !ok {
return ""
}
return arr.String()
case bsontype.Binary:
subtype, data, ok := v.BinaryOK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$binary":{"base64":"%s","subType":"%02x"}}`, base64.StdEncoding.EncodeToString(data), subtype)
case bsontype.Undefined:
return `{"$undefined":true}`
case bsontype.ObjectID:
oid, ok := v.ObjectIDOK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$oid":"%s"}`, oid.Hex())
case bsontype.Boolean:
b, ok := v.BooleanOK()
if !ok {
return ""
}
return strconv.FormatBool(b)
case bsontype.DateTime:
dt, ok := v.DateTimeOK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$date":{"$numberLong":"%d"}}`, dt)
case bsontype.Null:
return "null"
case bsontype.Regex:
pattern, options, ok := v.RegexOK()
if !ok {
return ""
}
return fmt.Sprintf(
`{"$regularExpression":{"pattern":%s,"options":"%s"}}`,
escapeString(pattern), sortStringAlphebeticAscending(options),
)
case bsontype.DBPointer:
ns, pointer, ok := v.DBPointerOK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$dbPointer":{"$ref":%s,"$id":{"$oid":"%s"}}}`, escapeString(ns), pointer.Hex())
case bsontype.JavaScript:
js, ok := v.JavaScriptOK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$code":%s}`, escapeString(js))
case bsontype.Symbol:
symbol, ok := v.SymbolOK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$symbol":%s}`, escapeString(symbol))
case bsontype.CodeWithScope:
code, scope, ok := v.CodeWithScopeOK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$code":%s,"$scope":%s}`, code, scope)
case bsontype.Int32:
i32, ok := v.Int32OK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$numberInt":"%d"}`, i32)
case bsontype.Timestamp:
t, i, ok := v.TimestampOK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$timestamp":{"t":"%s","i":"%s"}}`, strconv.FormatUint(uint64(t), 10), strconv.FormatUint(uint64(i), 10))
case bsontype.Int64:
i64, ok := v.Int64OK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$numberLong":"%d"}`, i64)
case bsontype.Decimal128:
d128, ok := v.Decimal128OK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$numberDecimal":"%s"}`, d128.String())
case bsontype.MinKey:
return `{"$minKey":1}`
case bsontype.MaxKey:
return `{"$maxKey":1}`
default:
return ""
}
}
// DebugString outputs a human readable version of Document. It will attempt to stringify the
// valid components of the document even if the entire document is not valid.
func (v Value) DebugString() string {
switch v.Type {
case bsontype.String:
str, ok := v.StringValueOK()
if !ok {
return "<malformed>"
}
return escapeString(str)
case bsontype.EmbeddedDocument:
doc, ok := v.DocumentOK()
if !ok {
return "<malformed>"
}
return doc.DebugString()
case bsontype.Array:
arr, ok := v.ArrayOK()
if !ok {
return "<malformed>"
}
return arr.DebugString()
case bsontype.CodeWithScope:
code, scope, ok := v.CodeWithScopeOK()
if !ok {
return ""
}
return fmt.Sprintf(`{"$code":%s,"$scope":%s}`, code, scope.DebugString())
default:
str := v.String()
if str == "" {
return "<malformed>"
}
return str
}
}
// Double returns the float64 value for this element.
// It panics if e's BSON type is not bsontype.Double.
func (v Value) Double() float64 {
if v.Type != bsontype.Double {
panic(ElementTypeError{"bsoncore.Value.Double", v.Type})
}
f64, _, ok := ReadDouble(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return f64
}
// DoubleOK is the same as Double, but returns a boolean instead of panicking.
func (v Value) DoubleOK() (float64, bool) {
if v.Type != bsontype.Double {
return 0, false
}
f64, _, ok := ReadDouble(v.Data)
if !ok {
return 0, false
}
return f64, true
}
// StringValue returns the string balue for this element.
// It panics if e's BSON type is not bsontype.String.
//
// NOTE: This method is called StringValue to avoid a collision with the String method which
// implements the fmt.Stringer interface.
func (v Value) StringValue() string {
if v.Type != bsontype.String {
panic(ElementTypeError{"bsoncore.Value.StringValue", v.Type})
}
str, _, ok := ReadString(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return str
}
// StringValueOK is the same as StringValue, but returns a boolean instead of
// panicking.
func (v Value) StringValueOK() (string, bool) {
if v.Type != bsontype.String {
return "", false
}
str, _, ok := ReadString(v.Data)
if !ok {
return "", false
}
return str, true
}
// Document returns the BSON document the Value represents as a Document. It panics if the
// value is a BSON type other than document.
func (v Value) Document() Document {
if v.Type != bsontype.EmbeddedDocument {
panic(ElementTypeError{"bsoncore.Value.Document", v.Type})
}
doc, _, ok := ReadDocument(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return doc
}
// DocumentOK is the same as Document, except it returns a boolean
// instead of panicking.
func (v Value) DocumentOK() (Document, bool) {
if v.Type != bsontype.EmbeddedDocument {
return nil, false
}
doc, _, ok := ReadDocument(v.Data)
if !ok {
return nil, false
}
return doc, true
}
// Array returns the BSON array the Value represents as an Array. It panics if the
// value is a BSON type other than array.
func (v Value) Array() Array {
if v.Type != bsontype.Array {
panic(ElementTypeError{"bsoncore.Value.Array", v.Type})
}
arr, _, ok := ReadArray(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return arr
}
// ArrayOK is the same as Array, except it returns a boolean instead
// of panicking.
func (v Value) ArrayOK() (Array, bool) {
if v.Type != bsontype.Array {
return nil, false
}
arr, _, ok := ReadArray(v.Data)
if !ok {
return nil, false
}
return arr, true
}
// Binary returns the BSON binary value the Value represents. It panics if the value is a BSON type
// other than binary.
func (v Value) Binary() (subtype byte, data []byte) {
if v.Type != bsontype.Binary {
panic(ElementTypeError{"bsoncore.Value.Binary", v.Type})
}
subtype, data, _, ok := ReadBinary(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return subtype, data
}
// BinaryOK is the same as Binary, except it returns a boolean instead of
// panicking.
func (v Value) BinaryOK() (subtype byte, data []byte, ok bool) {
if v.Type != bsontype.Binary {
return 0x00, nil, false
}
subtype, data, _, ok = ReadBinary(v.Data)
if !ok {
return 0x00, nil, false
}
return subtype, data, true
}
// ObjectID returns the BSON objectid value the Value represents. It panics if the value is a BSON
// type other than objectid.
func (v Value) ObjectID() primitive.ObjectID {
if v.Type != bsontype.ObjectID {
panic(ElementTypeError{"bsoncore.Value.ObjectID", v.Type})
}
oid, _, ok := ReadObjectID(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return oid
}
// ObjectIDOK is the same as ObjectID, except it returns a boolean instead of
// panicking.
func (v Value) ObjectIDOK() (primitive.ObjectID, bool) {
if v.Type != bsontype.ObjectID {
return primitive.ObjectID{}, false
}
oid, _, ok := ReadObjectID(v.Data)
if !ok {
return primitive.ObjectID{}, false
}
return oid, true
}
// Boolean returns the boolean value the Value represents. It panics if the
// value is a BSON type other than boolean.
func (v Value) Boolean() bool {
if v.Type != bsontype.Boolean {
panic(ElementTypeError{"bsoncore.Value.Boolean", v.Type})
}
b, _, ok := ReadBoolean(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return b
}
// BooleanOK is the same as Boolean, except it returns a boolean instead of
// panicking.
func (v Value) BooleanOK() (bool, bool) {
if v.Type != bsontype.Boolean {
return false, false
}
b, _, ok := ReadBoolean(v.Data)
if !ok {
return false, false
}
return b, true
}
// DateTime returns the BSON datetime value the Value represents as a
// unix timestamp. It panics if the value is a BSON type other than datetime.
func (v Value) DateTime() int64 {
if v.Type != bsontype.DateTime {
panic(ElementTypeError{"bsoncore.Value.DateTime", v.Type})
}
dt, _, ok := ReadDateTime(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return dt
}
// DateTimeOK is the same as DateTime, except it returns a boolean instead of
// panicking.
func (v Value) DateTimeOK() (int64, bool) {
if v.Type != bsontype.DateTime {
return 0, false
}
dt, _, ok := ReadDateTime(v.Data)
if !ok {
return 0, false
}
return dt, true
}
// Time returns the BSON datetime value the Value represents. It panics if the value is a BSON
// type other than datetime.
func (v Value) Time() time.Time {
if v.Type != bsontype.DateTime {
panic(ElementTypeError{"bsoncore.Value.Time", v.Type})
}
dt, _, ok := ReadDateTime(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return time.Unix(dt/1000, dt%1000*1000000)
}
// TimeOK is the same as Time, except it returns a boolean instead of
// panicking.
func (v Value) TimeOK() (time.Time, bool) {
if v.Type != bsontype.DateTime {
return time.Time{}, false
}
dt, _, ok := ReadDateTime(v.Data)
if !ok {
return time.Time{}, false
}
return time.Unix(dt/1000, dt%1000*1000000), true
}
// Regex returns the BSON regex value the Value represents. It panics if the value is a BSON
// type other than regex.
func (v Value) Regex() (pattern, options string) {
if v.Type != bsontype.Regex {
panic(ElementTypeError{"bsoncore.Value.Regex", v.Type})
}
pattern, options, _, ok := ReadRegex(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return pattern, options
}
// RegexOK is the same as Regex, except it returns a boolean instead of
// panicking.
func (v Value) RegexOK() (pattern, options string, ok bool) {
if v.Type != bsontype.Regex {
return "", "", false
}
pattern, options, _, ok = ReadRegex(v.Data)
if !ok {
return "", "", false
}
return pattern, options, true
}
// DBPointer returns the BSON dbpointer value the Value represents. It panics if the value is a BSON
// type other than DBPointer.
func (v Value) DBPointer() (string, primitive.ObjectID) {
if v.Type != bsontype.DBPointer {
panic(ElementTypeError{"bsoncore.Value.DBPointer", v.Type})
}
ns, pointer, _, ok := ReadDBPointer(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return ns, pointer
}
// DBPointerOK is the same as DBPoitner, except that it returns a boolean
// instead of panicking.
func (v Value) DBPointerOK() (string, primitive.ObjectID, bool) {
if v.Type != bsontype.DBPointer {
return "", primitive.ObjectID{}, false
}
ns, pointer, _, ok := ReadDBPointer(v.Data)
if !ok {
return "", primitive.ObjectID{}, false
}
return ns, pointer, true
}
// JavaScript returns the BSON JavaScript code value the Value represents. It panics if the value is
// a BSON type other than JavaScript code.
func (v Value) JavaScript() string {
if v.Type != bsontype.JavaScript {
panic(ElementTypeError{"bsoncore.Value.JavaScript", v.Type})
}
js, _, ok := ReadJavaScript(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return js
}
// JavaScriptOK is the same as Javascript, excepti that it returns a boolean
// instead of panicking.
func (v Value) JavaScriptOK() (string, bool) {
if v.Type != bsontype.JavaScript {
return "", false
}
js, _, ok := ReadJavaScript(v.Data)
if !ok {
return "", false
}
return js, true
}
// Symbol returns the BSON symbol value the Value represents. It panics if the value is a BSON
// type other than symbol.
func (v Value) Symbol() string {
if v.Type != bsontype.Symbol {
panic(ElementTypeError{"bsoncore.Value.Symbol", v.Type})
}
symbol, _, ok := ReadSymbol(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return symbol
}
// SymbolOK is the same as Symbol, excepti that it returns a boolean
// instead of panicking.
func (v Value) SymbolOK() (string, bool) {
if v.Type != bsontype.Symbol {
return "", false
}
symbol, _, ok := ReadSymbol(v.Data)
if !ok {
return "", false
}
return symbol, true
}
// CodeWithScope returns the BSON JavaScript code with scope the Value represents.
// It panics if the value is a BSON type other than JavaScript code with scope.
func (v Value) CodeWithScope() (string, Document) {
if v.Type != bsontype.CodeWithScope {
panic(ElementTypeError{"bsoncore.Value.CodeWithScope", v.Type})
}
code, scope, _, ok := ReadCodeWithScope(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return code, scope
}
// CodeWithScopeOK is the same as CodeWithScope, except that it returns a boolean instead of
// panicking.
func (v Value) CodeWithScopeOK() (string, Document, bool) {
if v.Type != bsontype.CodeWithScope {
return "", nil, false
}
code, scope, _, ok := ReadCodeWithScope(v.Data)
if !ok {
return "", nil, false
}
return code, scope, true
}
// Int32 returns the int32 the Value represents. It panics if the value is a BSON type other than
// int32.
func (v Value) Int32() int32 {
if v.Type != bsontype.Int32 {
panic(ElementTypeError{"bsoncore.Value.Int32", v.Type})
}
i32, _, ok := ReadInt32(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return i32
}
// Int32OK is the same as Int32, except that it returns a boolean instead of
// panicking.
func (v Value) Int32OK() (int32, bool) {
if v.Type != bsontype.Int32 {
return 0, false
}
i32, _, ok := ReadInt32(v.Data)
if !ok {
return 0, false
}
return i32, true
}
// Timestamp returns the BSON timestamp value the Value represents. It panics if the value is a
// BSON type other than timestamp.
func (v Value) Timestamp() (t, i uint32) {
if v.Type != bsontype.Timestamp {
panic(ElementTypeError{"bsoncore.Value.Timestamp", v.Type})
}
t, i, _, ok := ReadTimestamp(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return t, i
}
// TimestampOK is the same as Timestamp, except that it returns a boolean
// instead of panicking.
func (v Value) TimestampOK() (t, i uint32, ok bool) {
if v.Type != bsontype.Timestamp {
return 0, 0, false
}
t, i, _, ok = ReadTimestamp(v.Data)
if !ok {
return 0, 0, false
}
return t, i, true
}
// Int64 returns the int64 the Value represents. It panics if the value is a BSON type other than
// int64.
func (v Value) Int64() int64 {
if v.Type != bsontype.Int64 {
panic(ElementTypeError{"bsoncore.Value.Int64", v.Type})
}
i64, _, ok := ReadInt64(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return i64
}
// Int64OK is the same as Int64, except that it returns a boolean instead of
// panicking.
func (v Value) Int64OK() (int64, bool) {
if v.Type != bsontype.Int64 {
return 0, false
}
i64, _, ok := ReadInt64(v.Data)
if !ok {
return 0, false
}
return i64, true
}
// Decimal128 returns the decimal the Value represents. It panics if the value is a BSON type other than
// decimal.
func (v Value) Decimal128() primitive.Decimal128 {
if v.Type != bsontype.Decimal128 {
panic(ElementTypeError{"bsoncore.Value.Decimal128", v.Type})
}
d128, _, ok := ReadDecimal128(v.Data)
if !ok {
panic(NewInsufficientBytesError(v.Data, v.Data))
}
return d128
}
// Decimal128OK is the same as Decimal128, except that it returns a boolean
// instead of panicking.
func (v Value) Decimal128OK() (primitive.Decimal128, bool) {
if v.Type != bsontype.Decimal128 {
return primitive.Decimal128{}, false
}
d128, _, ok := ReadDecimal128(v.Data)
if !ok {
return primitive.Decimal128{}, false
}
return d128, true
}
var hexChars = "0123456789abcdef"
func escapeString(s string) string {
escapeHTML := true
var buf bytes.Buffer
buf.WriteByte('"')
start := 0
for i := 0; i < len(s); {
if b := s[i]; b < utf8.RuneSelf {
if htmlSafeSet[b] || (!escapeHTML && safeSet[b]) {
i++
continue
}
if start < i {
buf.WriteString(s[start:i])
}
switch b {
case '\\', '"':
buf.WriteByte('\\')
buf.WriteByte(b)
case '\n':
buf.WriteByte('\\')
buf.WriteByte('n')
case '\r':
buf.WriteByte('\\')
buf.WriteByte('r')
case '\t':
buf.WriteByte('\\')
buf.WriteByte('t')
case '\b':
buf.WriteByte('\\')
buf.WriteByte('b')
case '\f':
buf.WriteByte('\\')
buf.WriteByte('f')
default:
// This encodes bytes < 0x20 except for \t, \n and \r.
// If escapeHTML is set, it also escapes <, >, and &
// because they can lead to security holes when
// user-controlled strings are rendered into JSON
// and served to some browsers.
buf.WriteString(`\u00`)
buf.WriteByte(hexChars[b>>4])
buf.WriteByte(hexChars[b&0xF])
}
i++
start = i
continue
}
c, size := utf8.DecodeRuneInString(s[i:])
if c == utf8.RuneError && size == 1 {
if start < i {
buf.WriteString(s[start:i])
}
buf.WriteString(`\ufffd`)
i += size
start = i
continue
}
// U+2028 is LINE SEPARATOR.
// U+2029 is PARAGRAPH SEPARATOR.
// They are both technically valid characters in JSON strings,
// but don't work in JSONP, which has to be evaluated as JavaScript,
// and can lead to security holes there. It is valid JSON to
// escape them, so we do so unconditionally.
// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
if c == '\u2028' || c == '\u2029' {
if start < i {
buf.WriteString(s[start:i])
}
buf.WriteString(`\u202`)
buf.WriteByte(hexChars[c&0xF])
i += size
start = i
continue
}
i += size
}
if start < len(s) {
buf.WriteString(s[start:])
}
buf.WriteByte('"')
return buf.String()
}
func formatDouble(f float64) string {
var s string
if math.IsInf(f, 1) {
s = "Infinity"
} else if math.IsInf(f, -1) {
s = "-Infinity"
} else if math.IsNaN(f) {
s = "NaN"
} else {
// Print exactly one decimalType place for integers; otherwise, print as many are necessary to
// perfectly represent it.
s = strconv.FormatFloat(f, 'G', -1, 64)
if !strings.ContainsRune(s, '.') {
s += ".0"
}
}
return s
}
type sortableString []rune
func (ss sortableString) Len() int {
return len(ss)
}
func (ss sortableString) Less(i, j int) bool {
return ss[i] < ss[j]
}
func (ss sortableString) Swap(i, j int) {
oldI := ss[i]
ss[i] = ss[j]
ss[j] = oldI
}
func sortStringAlphebeticAscending(s string) string {
ss := sortableString([]rune(s))
sort.Sort(ss)
return string([]rune(ss))
}
+166
View File
@@ -0,0 +1,166 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsonx
import (
"encoding/binary"
"math"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// IDoc is the interface implemented by Doc and MDoc. It allows either of these types to be provided
// to the Document function to create a Value.
type IDoc interface {
idoc()
}
// Double constructs a BSON double Value.
func Double(f64 float64) Val {
v := Val{t: bsontype.Double}
binary.LittleEndian.PutUint64(v.bootstrap[0:8], math.Float64bits(f64))
return v
}
// String constructs a BSON string Value.
func String(str string) Val { return Val{t: bsontype.String}.writestring(str) }
// Document constructs a Value from the given IDoc. If nil is provided, a BSON Null value will be
// returned.
func Document(doc IDoc) Val {
var v Val
switch tt := doc.(type) {
case Doc:
if tt == nil {
v.t = bsontype.Null
break
}
v.t = bsontype.EmbeddedDocument
v.primitive = tt
case MDoc:
if tt == nil {
v.t = bsontype.Null
break
}
v.t = bsontype.EmbeddedDocument
v.primitive = tt
default:
v.t = bsontype.Null
}
return v
}
// Array constructs a Value from arr. If arr is nil, a BSON Null value is returned.
func Array(arr Arr) Val {
if arr == nil {
return Val{t: bsontype.Null}
}
return Val{t: bsontype.Array, primitive: arr}
}
// Binary constructs a BSON binary Value.
func Binary(subtype byte, data []byte) Val {
return Val{t: bsontype.Binary, primitive: primitive.Binary{Subtype: subtype, Data: data}}
}
// Undefined constructs a BSON binary Value.
func Undefined() Val { return Val{t: bsontype.Undefined} }
// ObjectID constructs a BSON objectid Value.
func ObjectID(oid primitive.ObjectID) Val {
v := Val{t: bsontype.ObjectID}
copy(v.bootstrap[0:12], oid[:])
return v
}
// Boolean constructs a BSON boolean Value.
func Boolean(b bool) Val {
v := Val{t: bsontype.Boolean}
if b {
v.bootstrap[0] = 0x01
}
return v
}
// DateTime constructs a BSON datetime Value.
func DateTime(dt int64) Val { return Val{t: bsontype.DateTime}.writei64(dt) }
// Time constructs a BSON datetime Value.
func Time(t time.Time) Val {
return Val{t: bsontype.DateTime}.writei64(t.Unix()*1e3 + int64(t.Nanosecond()/1e6))
}
// Null constructs a BSON binary Value.
func Null() Val { return Val{t: bsontype.Null} }
// Regex constructs a BSON regex Value.
func Regex(pattern, options string) Val {
regex := primitive.Regex{Pattern: pattern, Options: options}
return Val{t: bsontype.Regex, primitive: regex}
}
// DBPointer constructs a BSON dbpointer Value.
func DBPointer(ns string, ptr primitive.ObjectID) Val {
dbptr := primitive.DBPointer{DB: ns, Pointer: ptr}
return Val{t: bsontype.DBPointer, primitive: dbptr}
}
// JavaScript constructs a BSON javascript Value.
func JavaScript(js string) Val {
return Val{t: bsontype.JavaScript}.writestring(js)
}
// Symbol constructs a BSON symbol Value.
func Symbol(symbol string) Val {
return Val{t: bsontype.Symbol}.writestring(symbol)
}
// CodeWithScope constructs a BSON code with scope Value.
func CodeWithScope(code string, scope IDoc) Val {
cws := primitive.CodeWithScope{Code: primitive.JavaScript(code), Scope: scope}
return Val{t: bsontype.CodeWithScope, primitive: cws}
}
// Int32 constructs a BSON int32 Value.
func Int32(i32 int32) Val {
v := Val{t: bsontype.Int32}
v.bootstrap[0] = byte(i32)
v.bootstrap[1] = byte(i32 >> 8)
v.bootstrap[2] = byte(i32 >> 16)
v.bootstrap[3] = byte(i32 >> 24)
return v
}
// Timestamp constructs a BSON timestamp Value.
func Timestamp(t, i uint32) Val {
v := Val{t: bsontype.Timestamp}
v.bootstrap[0] = byte(i)
v.bootstrap[1] = byte(i >> 8)
v.bootstrap[2] = byte(i >> 16)
v.bootstrap[3] = byte(i >> 24)
v.bootstrap[4] = byte(t)
v.bootstrap[5] = byte(t >> 8)
v.bootstrap[6] = byte(t >> 16)
v.bootstrap[7] = byte(t >> 24)
return v
}
// Int64 constructs a BSON int64 Value.
func Int64(i64 int64) Val { return Val{t: bsontype.Int64}.writei64(i64) }
// Decimal128 constructs a BSON decimal128 Value.
func Decimal128(d128 primitive.Decimal128) Val {
return Val{t: bsontype.Decimal128, primitive: d128}
}
// MinKey constructs a BSON minkey Value.
func MinKey() Val { return Val{t: bsontype.MinKey} }
// MaxKey constructs a BSON maxkey Value.
func MaxKey() Val { return Val{t: bsontype.MaxKey} }
+305
View File
@@ -0,0 +1,305 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsonx
import (
"bytes"
"errors"
"fmt"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// ErrNilDocument indicates that an operation was attempted on a nil *bson.Document.
var ErrNilDocument = errors.New("document is nil")
// KeyNotFound is an error type returned from the Lookup methods on Document. This type contains
// information about which key was not found and if it was actually not found or if a component of
// the key except the last was not a document nor array.
type KeyNotFound struct {
Key []string // The keys that were searched for.
Depth uint // Which key either was not found or was an incorrect type.
Type bsontype.Type // The type of the key that was found but was an incorrect type.
}
func (knf KeyNotFound) Error() string {
depth := knf.Depth
if depth >= uint(len(knf.Key)) {
depth = uint(len(knf.Key)) - 1
}
if len(knf.Key) == 0 {
return "no keys were provided for lookup"
}
if knf.Type != bsontype.Type(0) {
return fmt.Sprintf(`key "%s" was found but was not valid to traverse BSON type %s`, knf.Key[depth], knf.Type)
}
return fmt.Sprintf(`key "%s" was not found`, knf.Key[depth])
}
// Doc is a type safe, concise BSON document representation.
type Doc []Elem
// ReadDoc will create a Document using the provided slice of bytes. If the
// slice of bytes is not a valid BSON document, this method will return an error.
func ReadDoc(b []byte) (Doc, error) {
doc := make(Doc, 0)
err := doc.UnmarshalBSON(b)
if err != nil {
return nil, err
}
return doc, nil
}
// Copy makes a shallow copy of this document.
func (d Doc) Copy() Doc {
d2 := make(Doc, len(d))
copy(d2, d)
return d2
}
// Append adds an element to the end of the document, creating it from the key and value provided.
func (d Doc) Append(key string, val Val) Doc {
return append(d, Elem{Key: key, Value: val})
}
// Prepend adds an element to the beginning of the document, creating it from the key and value provided.
func (d Doc) Prepend(key string, val Val) Doc {
// TODO: should we just modify d itself instead of doing an alloc here?
return append(Doc{{Key: key, Value: val}}, d...)
}
// Set replaces an element of a document. If an element with a matching key is
// found, the element will be replaced with the one provided. If the document
// does not have an element with that key, the element is appended to the
// document instead.
func (d Doc) Set(key string, val Val) Doc {
idx := d.IndexOf(key)
if idx == -1 {
return append(d, Elem{Key: key, Value: val})
}
d[idx] = Elem{Key: key, Value: val}
return d
}
// IndexOf returns the index of the first element with a key of key, or -1 if no element with a key
// was found.
func (d Doc) IndexOf(key string) int {
for i, e := range d {
if e.Key == key {
return i
}
}
return -1
}
// Delete removes the element with key if it exists and returns the updated Doc.
func (d Doc) Delete(key string) Doc {
idx := d.IndexOf(key)
if idx == -1 {
return d
}
return append(d[:idx], d[idx+1:]...)
}
// Lookup searches the document and potentially subdocuments or arrays for the
// provided key. Each key provided to this method represents a layer of depth.
//
// This method will return an empty Value if they key does not exist. To know if they key actually
// exists, use LookupErr.
func (d Doc) Lookup(key ...string) Val {
val, _ := d.LookupErr(key...)
return val
}
// LookupErr searches the document and potentially subdocuments or arrays for the
// provided key. Each key provided to this method represents a layer of depth.
func (d Doc) LookupErr(key ...string) (Val, error) {
elem, err := d.LookupElementErr(key...)
return elem.Value, err
}
// LookupElement searches the document and potentially subdocuments or arrays for the
// provided key. Each key provided to this method represents a layer of depth.
//
// This method will return an empty Element if they key does not exist. To know if they key actually
// exists, use LookupElementErr.
func (d Doc) LookupElement(key ...string) Elem {
elem, _ := d.LookupElementErr(key...)
return elem
}
// LookupElementErr searches the document and potentially subdocuments for the
// provided key. Each key provided to this method represents a layer of depth.
func (d Doc) LookupElementErr(key ...string) (Elem, error) {
// KeyNotFound operates by being created where the error happens and then the depth is
// incremented by 1 as each function unwinds. Whenever this function returns, it also assigns
// the Key slice to the key slice it has. This ensures that the proper depth is identified and
// the proper keys.
if len(key) == 0 {
return Elem{}, KeyNotFound{Key: key}
}
var elem Elem
var err error
idx := d.IndexOf(key[0])
if idx == -1 {
return Elem{}, KeyNotFound{Key: key}
}
elem = d[idx]
if len(key) == 1 {
return elem, nil
}
switch elem.Value.Type() {
case bsontype.EmbeddedDocument:
switch tt := elem.Value.primitive.(type) {
case Doc:
elem, err = tt.LookupElementErr(key[1:]...)
case MDoc:
elem, err = tt.LookupElementErr(key[1:]...)
}
default:
return Elem{}, KeyNotFound{Type: elem.Value.Type()}
}
switch tt := err.(type) {
case KeyNotFound:
tt.Depth++
tt.Key = key
return Elem{}, tt
case nil:
return elem, nil
default:
return Elem{}, err // We can't actually hit this.
}
}
// MarshalBSONValue implements the bsoncodec.ValueMarshaler interface.
//
// This method will never return an error.
func (d Doc) MarshalBSONValue() (bsontype.Type, []byte, error) {
if d == nil {
// TODO: Should we do this?
return bsontype.Null, nil, nil
}
data, _ := d.MarshalBSON()
return bsontype.EmbeddedDocument, data, nil
}
// MarshalBSON implements the Marshaler interface.
//
// This method will never return an error.
func (d Doc) MarshalBSON() ([]byte, error) { return d.AppendMarshalBSON(nil) }
// AppendMarshalBSON marshals Doc to BSON bytes, appending to dst.
//
// This method will never return an error.
func (d Doc) AppendMarshalBSON(dst []byte) ([]byte, error) {
idx, dst := bsoncore.ReserveLength(dst)
for _, elem := range d {
t, data, _ := elem.Value.MarshalBSONValue() // Value.MarshalBSONValue never returns an error.
dst = append(dst, byte(t))
dst = append(dst, elem.Key...)
dst = append(dst, 0x00)
dst = append(dst, data...)
}
dst = append(dst, 0x00)
dst = bsoncore.UpdateLength(dst, idx, int32(len(dst[idx:])))
return dst, nil
}
// UnmarshalBSON implements the Unmarshaler interface.
func (d *Doc) UnmarshalBSON(b []byte) error {
if d == nil {
return ErrNilDocument
}
if err := bsoncore.Document(b).Validate(); err != nil {
return err
}
elems, err := bsoncore.Document(b).Elements()
if err != nil {
return err
}
var val Val
for _, elem := range elems {
rawv := elem.Value()
err = val.UnmarshalBSONValue(rawv.Type, rawv.Data)
if err != nil {
return err
}
*d = d.Append(elem.Key(), val)
}
return nil
}
// UnmarshalBSONValue implements the bson.ValueUnmarshaler interface.
func (d *Doc) UnmarshalBSONValue(t bsontype.Type, data []byte) error {
if t != bsontype.EmbeddedDocument {
return fmt.Errorf("cannot unmarshal %s into a bsonx.Doc", t)
}
return d.UnmarshalBSON(data)
}
// Equal compares this document to another, returning true if they are equal.
func (d Doc) Equal(id IDoc) bool {
switch tt := id.(type) {
case Doc:
d2 := tt
if len(d) != len(d2) {
return false
}
for idx := range d {
if !d[idx].Equal(d2[idx]) {
return false
}
}
case MDoc:
unique := make(map[string]struct{})
for _, elem := range d {
unique[elem.Key] = struct{}{}
val, ok := tt[elem.Key]
if !ok {
return false
}
if !val.Equal(elem.Value) {
return false
}
}
if len(unique) != len(tt) {
return false
}
case nil:
return d == nil
default:
return false
}
return true
}
// String implements the fmt.Stringer interface.
func (d Doc) String() string {
var buf bytes.Buffer
buf.Write([]byte("bson.Document{"))
for idx, elem := range d {
if idx > 0 {
buf.Write([]byte(", "))
}
fmt.Fprintf(&buf, "%v", elem)
}
buf.WriteByte('}')
return buf.String()
}
func (Doc) idoc() {}
+51
View File
@@ -0,0 +1,51 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsonx
import (
"fmt"
"go.mongodb.org/mongo-driver/bson/bsontype"
)
// ElementTypeError specifies that a method to obtain a BSON value an incorrect type was called on a bson.Value.
//
// TODO: rename this ValueTypeError.
type ElementTypeError struct {
Method string
Type bsontype.Type
}
// Error implements the error interface.
func (ete ElementTypeError) Error() string {
return "Call of " + ete.Method + " on " + ete.Type.String() + " type"
}
// Elem represents a BSON element.
//
// NOTE: Element cannot be the value of a map nor a property of a struct without special handling.
// The default encoders and decoders will not process Element correctly. To do so would require
// information loss since an Element contains a key, but the keys used when encoding a struct are
// the struct field names. Instead of using an Element, use a Value as a value in a map or a
// property of a struct.
type Elem struct {
Key string
Value Val
}
// Equal compares e and e2 and returns true if they are equal.
func (e Elem) Equal(e2 Elem) bool {
if e.Key != e2.Key {
return false
}
return e.Value.Equal(e2.Value)
}
func (e Elem) String() string {
// TODO(GODRIVER-612): When bsoncore has appenders for extended JSON use that here.
return fmt.Sprintf(`bson.Element{"%s": %v}`, e.Key, e.Value)
}
+231
View File
@@ -0,0 +1,231 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsonx
import (
"bytes"
"fmt"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// MDoc is an unordered, type safe, concise BSON document representation. This type should not be
// used if you require ordering of values or duplicate keys.
type MDoc map[string]Val
// ReadMDoc will create a Doc using the provided slice of bytes. If the
// slice of bytes is not a valid BSON document, this method will return an error.
func ReadMDoc(b []byte) (MDoc, error) {
doc := make(MDoc)
err := doc.UnmarshalBSON(b)
if err != nil {
return nil, err
}
return doc, nil
}
// Copy makes a shallow copy of this document.
func (d MDoc) Copy() MDoc {
d2 := make(MDoc, len(d))
for k, v := range d {
d2[k] = v
}
return d2
}
// Lookup searches the document and potentially subdocuments or arrays for the
// provided key. Each key provided to this method represents a layer of depth.
//
// This method will return an empty Value if they key does not exist. To know if they key actually
// exists, use LookupErr.
func (d MDoc) Lookup(key ...string) Val {
val, _ := d.LookupErr(key...)
return val
}
// LookupErr searches the document and potentially subdocuments or arrays for the
// provided key. Each key provided to this method represents a layer of depth.
func (d MDoc) LookupErr(key ...string) (Val, error) {
elem, err := d.LookupElementErr(key...)
return elem.Value, err
}
// LookupElement searches the document and potentially subdocuments or arrays for the
// provided key. Each key provided to this method represents a layer of depth.
//
// This method will return an empty Element if they key does not exist. To know if they key actually
// exists, use LookupElementErr.
func (d MDoc) LookupElement(key ...string) Elem {
elem, _ := d.LookupElementErr(key...)
return elem
}
// LookupElementErr searches the document and potentially subdocuments for the
// provided key. Each key provided to this method represents a layer of depth.
func (d MDoc) LookupElementErr(key ...string) (Elem, error) {
// KeyNotFound operates by being created where the error happens and then the depth is
// incremented by 1 as each function unwinds. Whenever this function returns, it also assigns
// the Key slice to the key slice it has. This ensures that the proper depth is identified and
// the proper keys.
if len(key) == 0 {
return Elem{}, KeyNotFound{Key: key}
}
var elem Elem
var err error
val, ok := d[key[0]]
if !ok {
return Elem{}, KeyNotFound{Key: key}
}
if len(key) == 1 {
return Elem{Key: key[0], Value: val}, nil
}
switch val.Type() {
case bsontype.EmbeddedDocument:
switch tt := val.primitive.(type) {
case Doc:
elem, err = tt.LookupElementErr(key[1:]...)
case MDoc:
elem, err = tt.LookupElementErr(key[1:]...)
}
default:
return Elem{}, KeyNotFound{Type: val.Type()}
}
switch tt := err.(type) {
case KeyNotFound:
tt.Depth++
tt.Key = key
return Elem{}, tt
case nil:
return elem, nil
default:
return Elem{}, err // We can't actually hit this.
}
}
// MarshalBSONValue implements the bsoncodec.ValueMarshaler interface.
//
// This method will never return an error.
func (d MDoc) MarshalBSONValue() (bsontype.Type, []byte, error) {
if d == nil {
// TODO: Should we do this?
return bsontype.Null, nil, nil
}
data, _ := d.MarshalBSON()
return bsontype.EmbeddedDocument, data, nil
}
// MarshalBSON implements the Marshaler interface.
//
// This method will never return an error.
func (d MDoc) MarshalBSON() ([]byte, error) { return d.AppendMarshalBSON(nil) }
// AppendMarshalBSON marshals Doc to BSON bytes, appending to dst.
//
// This method will never return an error.
func (d MDoc) AppendMarshalBSON(dst []byte) ([]byte, error) {
idx, dst := bsoncore.ReserveLength(dst)
for k, v := range d {
t, data, _ := v.MarshalBSONValue() // Value.MarshalBSONValue never returns an error.
dst = append(dst, byte(t))
dst = append(dst, k...)
dst = append(dst, 0x00)
dst = append(dst, data...)
}
dst = append(dst, 0x00)
dst = bsoncore.UpdateLength(dst, idx, int32(len(dst[idx:])))
return dst, nil
}
// UnmarshalBSON implements the Unmarshaler interface.
func (d *MDoc) UnmarshalBSON(b []byte) error {
if d == nil {
return ErrNilDocument
}
if err := bsoncore.Document(b).Validate(); err != nil {
return err
}
elems, err := bsoncore.Document(b).Elements()
if err != nil {
return err
}
var val Val
for _, elem := range elems {
rawv := elem.Value()
err = val.UnmarshalBSONValue(rawv.Type, rawv.Data)
if err != nil {
return err
}
(*d)[elem.Key()] = val
}
return nil
}
// Equal compares this document to another, returning true if they are equal.
func (d MDoc) Equal(id IDoc) bool {
switch tt := id.(type) {
case MDoc:
d2 := tt
if len(d) != len(d2) {
return false
}
for key, value := range d {
value2, ok := d2[key]
if !ok {
return false
}
if !value.Equal(value2) {
return false
}
}
case Doc:
unique := make(map[string]struct{})
for _, elem := range tt {
unique[elem.Key] = struct{}{}
val, ok := d[elem.Key]
if !ok {
return false
}
if !val.Equal(elem.Value) {
return false
}
}
if len(unique) != len(d) {
return false
}
case nil:
return d == nil
default:
return false
}
return true
}
// String implements the fmt.Stringer interface.
func (d MDoc) String() string {
var buf bytes.Buffer
buf.Write([]byte("bson.Document{"))
first := true
for key, value := range d {
if !first {
buf.Write([]byte(", "))
}
fmt.Fprintf(&buf, "%v", Elem{Key: key, Value: value})
first = false
}
buf.WriteByte('}')
return buf.String()
}
func (MDoc) idoc() {}
+637
View File
@@ -0,0 +1,637 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsonx
import (
"errors"
"fmt"
"reflect"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
)
var primitiveCodecs PrimitiveCodecs
var tDocument = reflect.TypeOf((Doc)(nil))
var tArray = reflect.TypeOf((Arr)(nil))
var tValue = reflect.TypeOf(Val{})
var tElementSlice = reflect.TypeOf(([]Elem)(nil))
// PrimitiveCodecs is a namespace for all of the default bsoncodec.Codecs for the primitive types
// defined in this package.
type PrimitiveCodecs struct{}
// RegisterPrimitiveCodecs will register the encode and decode methods attached to PrimitiveCodecs
// with the provided RegistryBuilder. if rb is nil, a new empty RegistryBuilder will be created.
func (pc PrimitiveCodecs) RegisterPrimitiveCodecs(rb *bsoncodec.RegistryBuilder) {
if rb == nil {
panic(errors.New("argument to RegisterPrimitiveCodecs must not be nil"))
}
rb.
RegisterTypeEncoder(tDocument, bsoncodec.ValueEncoderFunc(pc.DocumentEncodeValue)).
RegisterTypeEncoder(tArray, bsoncodec.ValueEncoderFunc(pc.ArrayEncodeValue)).
RegisterTypeEncoder(tValue, bsoncodec.ValueEncoderFunc(pc.ValueEncodeValue)).
RegisterTypeEncoder(tElementSlice, bsoncodec.ValueEncoderFunc(pc.ElementSliceEncodeValue)).
RegisterTypeDecoder(tDocument, bsoncodec.ValueDecoderFunc(pc.DocumentDecodeValue)).
RegisterTypeDecoder(tArray, bsoncodec.ValueDecoderFunc(pc.ArrayDecodeValue)).
RegisterTypeDecoder(tValue, bsoncodec.ValueDecoderFunc(pc.ValueDecodeValue)).
RegisterTypeDecoder(tElementSlice, bsoncodec.ValueDecoderFunc(pc.ElementSliceDecodeValue))
}
// DocumentEncodeValue is the ValueEncoderFunc for *Document.
func (pc PrimitiveCodecs) DocumentEncodeValue(ec bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {
if !val.IsValid() || val.Type() != tDocument {
return bsoncodec.ValueEncoderError{Name: "DocumentEncodeValue", Types: []reflect.Type{tDocument}, Received: val}
}
if val.IsNil() {
return vw.WriteNull()
}
doc := val.Interface().(Doc)
dw, err := vw.WriteDocument()
if err != nil {
return err
}
return pc.encodeDocument(ec, dw, doc)
}
// DocumentDecodeValue is the ValueDecoderFunc for *Document.
func (pc PrimitiveCodecs) DocumentDecodeValue(dctx bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if !val.CanSet() || val.Type() != tDocument {
return bsoncodec.ValueDecoderError{Name: "DocumentDecodeValue", Types: []reflect.Type{tDocument}, Received: val}
}
return pc.documentDecodeValue(dctx, vr, val.Addr().Interface().(*Doc))
}
func (pc PrimitiveCodecs) documentDecodeValue(dctx bsoncodec.DecodeContext, vr bsonrw.ValueReader, doc *Doc) error {
dr, err := vr.ReadDocument()
if err != nil {
return err
}
return pc.decodeDocument(dctx, dr, doc)
}
// ArrayEncodeValue is the ValueEncoderFunc for *Array.
func (pc PrimitiveCodecs) ArrayEncodeValue(ec bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {
if !val.IsValid() || val.Type() != tArray {
return bsoncodec.ValueEncoderError{Name: "ArrayEncodeValue", Types: []reflect.Type{tArray}, Received: val}
}
if val.IsNil() {
return vw.WriteNull()
}
arr := val.Interface().(Arr)
aw, err := vw.WriteArray()
if err != nil {
return err
}
for _, val := range arr {
dvw, err := aw.WriteArrayElement()
if err != nil {
return err
}
err = pc.encodeValue(ec, dvw, val)
if err != nil {
return err
}
}
return aw.WriteArrayEnd()
}
// ArrayDecodeValue is the ValueDecoderFunc for *Array.
func (pc PrimitiveCodecs) ArrayDecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if !val.CanSet() || val.Type() != tArray {
return bsoncodec.ValueDecoderError{Name: "ArrayDecodeValue", Types: []reflect.Type{tArray}, Received: val}
}
ar, err := vr.ReadArray()
if err != nil {
return err
}
if val.IsNil() {
val.Set(reflect.MakeSlice(tArray, 0, 0))
}
val.SetLen(0)
for {
vr, err := ar.ReadValue()
if err == bsonrw.ErrEOA {
break
}
if err != nil {
return err
}
var elem Val
err = pc.valueDecodeValue(dc, vr, &elem)
if err != nil {
return err
}
val.Set(reflect.Append(val, reflect.ValueOf(elem)))
}
return nil
}
// ElementSliceEncodeValue is the ValueEncoderFunc for []*Element.
func (pc PrimitiveCodecs) ElementSliceEncodeValue(ec bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {
if !val.IsValid() || val.Type() != tElementSlice {
return bsoncodec.ValueEncoderError{Name: "ElementSliceEncodeValue", Types: []reflect.Type{tElementSlice}, Received: val}
}
if val.IsNil() {
return vw.WriteNull()
}
return pc.DocumentEncodeValue(ec, vw, val.Convert(tDocument))
}
// ElementSliceDecodeValue is the ValueDecoderFunc for []*Element.
func (pc PrimitiveCodecs) ElementSliceDecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if !val.CanSet() || val.Type() != tElementSlice {
return bsoncodec.ValueDecoderError{Name: "ElementSliceDecodeValue", Types: []reflect.Type{tElementSlice}, Received: val}
}
if val.IsNil() {
val.Set(reflect.MakeSlice(val.Type(), 0, 0))
}
val.SetLen(0)
dr, err := vr.ReadDocument()
if err != nil {
return err
}
elems := make([]reflect.Value, 0)
for {
key, vr, err := dr.ReadElement()
if err == bsonrw.ErrEOD {
break
}
if err != nil {
return err
}
var elem Elem
err = pc.elementDecodeValue(dc, vr, key, &elem)
if err != nil {
return err
}
elems = append(elems, reflect.ValueOf(elem))
}
val.Set(reflect.Append(val, elems...))
return nil
}
// ValueEncodeValue is the ValueEncoderFunc for *Value.
func (pc PrimitiveCodecs) ValueEncodeValue(ec bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {
if !val.IsValid() || val.Type() != tValue {
return bsoncodec.ValueEncoderError{Name: "ValueEncodeValue", Types: []reflect.Type{tValue}, Received: val}
}
v := val.Interface().(Val)
return pc.encodeValue(ec, vw, v)
}
// ValueDecodeValue is the ValueDecoderFunc for *Value.
func (pc PrimitiveCodecs) ValueDecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if !val.CanSet() || val.Type() != tValue {
return bsoncodec.ValueDecoderError{Name: "ValueDecodeValue", Types: []reflect.Type{tValue}, Received: val}
}
return pc.valueDecodeValue(dc, vr, val.Addr().Interface().(*Val))
}
// encodeDocument is a separate function that we use because CodeWithScope
// returns us a DocumentWriter and we need to do the same logic that we would do
// for a document but cannot use a Codec.
func (pc PrimitiveCodecs) encodeDocument(ec bsoncodec.EncodeContext, dw bsonrw.DocumentWriter, doc Doc) error {
for _, elem := range doc {
dvw, err := dw.WriteDocumentElement(elem.Key)
if err != nil {
return err
}
err = pc.encodeValue(ec, dvw, elem.Value)
if err != nil {
return err
}
}
return dw.WriteDocumentEnd()
}
// DecodeDocument haves decoding into a Doc from a bsonrw.DocumentReader.
func (pc PrimitiveCodecs) DecodeDocument(dctx bsoncodec.DecodeContext, dr bsonrw.DocumentReader, pdoc *Doc) error {
return pc.decodeDocument(dctx, dr, pdoc)
}
func (pc PrimitiveCodecs) decodeDocument(dctx bsoncodec.DecodeContext, dr bsonrw.DocumentReader, pdoc *Doc) error {
if *pdoc == nil {
*pdoc = make(Doc, 0)
}
*pdoc = (*pdoc)[:0]
for {
key, vr, err := dr.ReadElement()
if err == bsonrw.ErrEOD {
break
}
if err != nil {
return err
}
var elem Elem
err = pc.elementDecodeValue(dctx, vr, key, &elem)
if err != nil {
return err
}
*pdoc = append(*pdoc, elem)
}
return nil
}
func (pc PrimitiveCodecs) elementDecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, key string, elem *Elem) error {
var val Val
switch vr.Type() {
case bsontype.Double:
f64, err := vr.ReadDouble()
if err != nil {
return err
}
val = Double(f64)
case bsontype.String:
str, err := vr.ReadString()
if err != nil {
return err
}
val = String(str)
case bsontype.EmbeddedDocument:
var embeddedDoc Doc
err := pc.documentDecodeValue(dc, vr, &embeddedDoc)
if err != nil {
return err
}
val = Document(embeddedDoc)
case bsontype.Array:
arr := reflect.New(tArray).Elem()
err := pc.ArrayDecodeValue(dc, vr, arr)
if err != nil {
return err
}
val = Array(arr.Interface().(Arr))
case bsontype.Binary:
data, subtype, err := vr.ReadBinary()
if err != nil {
return err
}
val = Binary(subtype, data)
case bsontype.Undefined:
err := vr.ReadUndefined()
if err != nil {
return err
}
val = Undefined()
case bsontype.ObjectID:
oid, err := vr.ReadObjectID()
if err != nil {
return err
}
val = ObjectID(oid)
case bsontype.Boolean:
b, err := vr.ReadBoolean()
if err != nil {
return err
}
val = Boolean(b)
case bsontype.DateTime:
dt, err := vr.ReadDateTime()
if err != nil {
return err
}
val = DateTime(dt)
case bsontype.Null:
err := vr.ReadNull()
if err != nil {
return err
}
val = Null()
case bsontype.Regex:
pattern, options, err := vr.ReadRegex()
if err != nil {
return err
}
val = Regex(pattern, options)
case bsontype.DBPointer:
ns, pointer, err := vr.ReadDBPointer()
if err != nil {
return err
}
val = DBPointer(ns, pointer)
case bsontype.JavaScript:
js, err := vr.ReadJavascript()
if err != nil {
return err
}
val = JavaScript(js)
case bsontype.Symbol:
symbol, err := vr.ReadSymbol()
if err != nil {
return err
}
val = Symbol(symbol)
case bsontype.CodeWithScope:
code, scope, err := vr.ReadCodeWithScope()
if err != nil {
return err
}
var doc Doc
err = pc.decodeDocument(dc, scope, &doc)
if err != nil {
return err
}
val = CodeWithScope(code, doc)
case bsontype.Int32:
i32, err := vr.ReadInt32()
if err != nil {
return err
}
val = Int32(i32)
case bsontype.Timestamp:
t, i, err := vr.ReadTimestamp()
if err != nil {
return err
}
val = Timestamp(t, i)
case bsontype.Int64:
i64, err := vr.ReadInt64()
if err != nil {
return err
}
val = Int64(i64)
case bsontype.Decimal128:
d128, err := vr.ReadDecimal128()
if err != nil {
return err
}
val = Decimal128(d128)
case bsontype.MinKey:
err := vr.ReadMinKey()
if err != nil {
return err
}
val = MinKey()
case bsontype.MaxKey:
err := vr.ReadMaxKey()
if err != nil {
return err
}
val = MaxKey()
default:
return fmt.Errorf("Cannot read unknown BSON type %s", vr.Type())
}
*elem = Elem{Key: key, Value: val}
return nil
}
// encodeValue does not validation, and the callers must perform validation on val before calling
// this method.
func (pc PrimitiveCodecs) encodeValue(ec bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val Val) error {
var err error
switch val.Type() {
case bsontype.Double:
err = vw.WriteDouble(val.Double())
case bsontype.String:
err = vw.WriteString(val.StringValue())
case bsontype.EmbeddedDocument:
var encoder bsoncodec.ValueEncoder
encoder, err = ec.LookupEncoder(tDocument)
if err != nil {
break
}
err = encoder.EncodeValue(ec, vw, reflect.ValueOf(val.Document()))
case bsontype.Array:
var encoder bsoncodec.ValueEncoder
encoder, err = ec.LookupEncoder(tArray)
if err != nil {
break
}
err = encoder.EncodeValue(ec, vw, reflect.ValueOf(val.Array()))
case bsontype.Binary:
// TODO: FIX THIS (╯°□°)╯︵ ┻━┻
subtype, data := val.Binary()
err = vw.WriteBinaryWithSubtype(data, subtype)
case bsontype.Undefined:
err = vw.WriteUndefined()
case bsontype.ObjectID:
err = vw.WriteObjectID(val.ObjectID())
case bsontype.Boolean:
err = vw.WriteBoolean(val.Boolean())
case bsontype.DateTime:
err = vw.WriteDateTime(val.DateTime())
case bsontype.Null:
err = vw.WriteNull()
case bsontype.Regex:
err = vw.WriteRegex(val.Regex())
case bsontype.DBPointer:
err = vw.WriteDBPointer(val.DBPointer())
case bsontype.JavaScript:
err = vw.WriteJavascript(val.JavaScript())
case bsontype.Symbol:
err = vw.WriteSymbol(val.Symbol())
case bsontype.CodeWithScope:
code, scope := val.CodeWithScope()
var cwsw bsonrw.DocumentWriter
cwsw, err = vw.WriteCodeWithScope(code)
if err != nil {
break
}
err = pc.encodeDocument(ec, cwsw, scope)
case bsontype.Int32:
err = vw.WriteInt32(val.Int32())
case bsontype.Timestamp:
err = vw.WriteTimestamp(val.Timestamp())
case bsontype.Int64:
err = vw.WriteInt64(val.Int64())
case bsontype.Decimal128:
err = vw.WriteDecimal128(val.Decimal128())
case bsontype.MinKey:
err = vw.WriteMinKey()
case bsontype.MaxKey:
err = vw.WriteMaxKey()
default:
err = fmt.Errorf("%T is not a valid BSON type to encode", val.Type())
}
return err
}
func (pc PrimitiveCodecs) valueDecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val *Val) error {
switch vr.Type() {
case bsontype.Double:
f64, err := vr.ReadDouble()
if err != nil {
return err
}
*val = Double(f64)
case bsontype.String:
str, err := vr.ReadString()
if err != nil {
return err
}
*val = String(str)
case bsontype.EmbeddedDocument:
var embeddedDoc Doc
err := pc.documentDecodeValue(dc, vr, &embeddedDoc)
if err != nil {
return err
}
*val = Document(embeddedDoc)
case bsontype.Array:
arr := reflect.New(tArray).Elem()
err := pc.ArrayDecodeValue(dc, vr, arr)
if err != nil {
return err
}
*val = Array(arr.Interface().(Arr))
case bsontype.Binary:
data, subtype, err := vr.ReadBinary()
if err != nil {
return err
}
*val = Binary(subtype, data)
case bsontype.Undefined:
err := vr.ReadUndefined()
if err != nil {
return err
}
*val = Undefined()
case bsontype.ObjectID:
oid, err := vr.ReadObjectID()
if err != nil {
return err
}
*val = ObjectID(oid)
case bsontype.Boolean:
b, err := vr.ReadBoolean()
if err != nil {
return err
}
*val = Boolean(b)
case bsontype.DateTime:
dt, err := vr.ReadDateTime()
if err != nil {
return err
}
*val = DateTime(dt)
case bsontype.Null:
err := vr.ReadNull()
if err != nil {
return err
}
*val = Null()
case bsontype.Regex:
pattern, options, err := vr.ReadRegex()
if err != nil {
return err
}
*val = Regex(pattern, options)
case bsontype.DBPointer:
ns, pointer, err := vr.ReadDBPointer()
if err != nil {
return err
}
*val = DBPointer(ns, pointer)
case bsontype.JavaScript:
js, err := vr.ReadJavascript()
if err != nil {
return err
}
*val = JavaScript(js)
case bsontype.Symbol:
symbol, err := vr.ReadSymbol()
if err != nil {
return err
}
*val = Symbol(symbol)
case bsontype.CodeWithScope:
code, scope, err := vr.ReadCodeWithScope()
if err != nil {
return err
}
var scopeDoc Doc
err = pc.decodeDocument(dc, scope, &scopeDoc)
if err != nil {
return err
}
*val = CodeWithScope(code, scopeDoc)
case bsontype.Int32:
i32, err := vr.ReadInt32()
if err != nil {
return err
}
*val = Int32(i32)
case bsontype.Timestamp:
t, i, err := vr.ReadTimestamp()
if err != nil {
return err
}
*val = Timestamp(t, i)
case bsontype.Int64:
i64, err := vr.ReadInt64()
if err != nil {
return err
}
*val = Int64(i64)
case bsontype.Decimal128:
d128, err := vr.ReadDecimal128()
if err != nil {
return err
}
*val = Decimal128(d128)
case bsontype.MinKey:
err := vr.ReadMinKey()
if err != nil {
return err
}
*val = MinKey()
case bsontype.MaxKey:
err := vr.ReadMaxKey()
if err != nil {
return err
}
*val = MaxKey()
default:
return fmt.Errorf("Cannot read unknown BSON type %s", vr.Type())
}
return nil
}
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsonx
import (
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
)
// DefaultRegistry is the default bsoncodec.Registry. It contains the default codecs and the
// primitive codecs.
var DefaultRegistry = NewRegistryBuilder().Build()
// NewRegistryBuilder creates a new RegistryBuilder configured with the default encoders and
// decoders from the bsoncodec.DefaultValueEncoders and bsoncodec.DefaultValueDecoders types and the
// PrimitiveCodecs type in this package.
func NewRegistryBuilder() *bsoncodec.RegistryBuilder {
rb := bsoncodec.NewRegistryBuilder()
bsoncodec.DefaultValueEncoders{}.RegisterDefaultEncoders(rb)
bsoncodec.DefaultValueDecoders{}.RegisterDefaultDecoders(rb)
bson.PrimitiveCodecs{}.RegisterPrimitiveCodecs(rb)
primitiveCodecs.RegisterPrimitiveCodecs(rb)
return rb
}
+866
View File
@@ -0,0 +1,866 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package bsonx
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// Val represents a BSON value.
type Val struct {
// NOTE: The bootstrap is a small amount of space that'll be on the stack. At 15 bytes this
// doesn't make this type any larger, since there are 7 bytes of padding and we want an int64 to
// store small values (e.g. boolean, double, int64, etc...). The primitive property is where all
// of the larger values go. They will use either Go primitives or the primitive.* types.
t bsontype.Type
bootstrap [15]byte
primitive interface{}
}
func (v Val) string() string {
if v.primitive != nil {
return v.primitive.(string)
}
// The string will either end with a null byte or it fills the entire bootstrap space.
length := v.bootstrap[0]
return string(v.bootstrap[1 : length+1])
}
func (v Val) writestring(str string) Val {
switch {
case len(str) < 15:
v.bootstrap[0] = uint8(len(str))
copy(v.bootstrap[1:], str)
default:
v.primitive = str
}
return v
}
func (v Val) i64() int64 {
return int64(v.bootstrap[0]) | int64(v.bootstrap[1])<<8 | int64(v.bootstrap[2])<<16 |
int64(v.bootstrap[3])<<24 | int64(v.bootstrap[4])<<32 | int64(v.bootstrap[5])<<40 |
int64(v.bootstrap[6])<<48 | int64(v.bootstrap[7])<<56
}
func (v Val) writei64(i64 int64) Val {
v.bootstrap[0] = byte(i64)
v.bootstrap[1] = byte(i64 >> 8)
v.bootstrap[2] = byte(i64 >> 16)
v.bootstrap[3] = byte(i64 >> 24)
v.bootstrap[4] = byte(i64 >> 32)
v.bootstrap[5] = byte(i64 >> 40)
v.bootstrap[6] = byte(i64 >> 48)
v.bootstrap[7] = byte(i64 >> 56)
return v
}
// IsZero returns true if this value is zero or a BSON null.
func (v Val) IsZero() bool { return v.t == bsontype.Type(0) || v.t == bsontype.Null }
func (v Val) String() string {
// TODO(GODRIVER-612): When bsoncore has appenders for extended JSON use that here.
return fmt.Sprintf("%v", v.Interface())
}
// Interface returns the Go value of this Value as an empty interface.
//
// This method will return nil if it is empty, otherwise it will return a Go primitive or a
// primitive.* instance.
func (v Val) Interface() interface{} {
switch v.Type() {
case bsontype.Double:
return v.Double()
case bsontype.String:
return v.StringValue()
case bsontype.EmbeddedDocument:
switch v.primitive.(type) {
case Doc:
return v.primitive.(Doc)
case MDoc:
return v.primitive.(MDoc)
default:
return primitive.Null{}
}
case bsontype.Array:
return v.Array()
case bsontype.Binary:
return v.primitive.(primitive.Binary)
case bsontype.Undefined:
return primitive.Undefined{}
case bsontype.ObjectID:
return v.ObjectID()
case bsontype.Boolean:
return v.Boolean()
case bsontype.DateTime:
return v.DateTime()
case bsontype.Null:
return primitive.Null{}
case bsontype.Regex:
return v.primitive.(primitive.Regex)
case bsontype.DBPointer:
return v.primitive.(primitive.DBPointer)
case bsontype.JavaScript:
return v.JavaScript()
case bsontype.Symbol:
return v.Symbol()
case bsontype.CodeWithScope:
return v.primitive.(primitive.CodeWithScope)
case bsontype.Int32:
return v.Int32()
case bsontype.Timestamp:
t, i := v.Timestamp()
return primitive.Timestamp{T: t, I: i}
case bsontype.Int64:
return v.Int64()
case bsontype.Decimal128:
return v.Decimal128()
case bsontype.MinKey:
return primitive.MinKey{}
case bsontype.MaxKey:
return primitive.MaxKey{}
default:
return primitive.Null{}
}
}
// MarshalBSONValue implements the bsoncodec.ValueMarshaler interface.
func (v Val) MarshalBSONValue() (bsontype.Type, []byte, error) {
return v.MarshalAppendBSONValue(nil)
}
// MarshalAppendBSONValue is similar to MarshalBSONValue, but allows the caller to specify a slice
// to add the bytes to.
func (v Val) MarshalAppendBSONValue(dst []byte) (bsontype.Type, []byte, error) {
t := v.Type()
switch v.Type() {
case bsontype.Double:
dst = bsoncore.AppendDouble(dst, v.Double())
case bsontype.String:
dst = bsoncore.AppendString(dst, v.String())
case bsontype.EmbeddedDocument:
switch v.primitive.(type) {
case Doc:
t, dst, _ = v.primitive.(Doc).MarshalBSONValue() // Doc.MarshalBSONValue never returns an error.
case MDoc:
t, dst, _ = v.primitive.(MDoc).MarshalBSONValue() // MDoc.MarshalBSONValue never returns an error.
}
case bsontype.Array:
t, dst, _ = v.Array().MarshalBSONValue() // Arr.MarshalBSON never returns an error.
case bsontype.Binary:
subtype, bindata := v.Binary()
dst = bsoncore.AppendBinary(dst, subtype, bindata)
case bsontype.Undefined:
case bsontype.ObjectID:
dst = bsoncore.AppendObjectID(dst, v.ObjectID())
case bsontype.Boolean:
dst = bsoncore.AppendBoolean(dst, v.Boolean())
case bsontype.DateTime:
dst = bsoncore.AppendDateTime(dst, v.DateTime())
case bsontype.Null:
case bsontype.Regex:
pattern, options := v.Regex()
dst = bsoncore.AppendRegex(dst, pattern, options)
case bsontype.DBPointer:
ns, ptr := v.DBPointer()
dst = bsoncore.AppendDBPointer(dst, ns, ptr)
case bsontype.JavaScript:
dst = bsoncore.AppendJavaScript(dst, v.JavaScript())
case bsontype.Symbol:
dst = bsoncore.AppendSymbol(dst, v.Symbol())
case bsontype.CodeWithScope:
code, doc := v.CodeWithScope()
var scope []byte
scope, _ = doc.MarshalBSON() // Doc.MarshalBSON never returns an error.
dst = bsoncore.AppendCodeWithScope(dst, code, scope)
case bsontype.Int32:
dst = bsoncore.AppendInt32(dst, v.Int32())
case bsontype.Timestamp:
t, i := v.Timestamp()
dst = bsoncore.AppendTimestamp(dst, t, i)
case bsontype.Int64:
dst = bsoncore.AppendInt64(dst, v.Int64())
case bsontype.Decimal128:
dst = bsoncore.AppendDecimal128(dst, v.Decimal128())
case bsontype.MinKey:
case bsontype.MaxKey:
default:
panic(fmt.Errorf("invalid BSON type %v", t))
}
return t, dst, nil
}
// UnmarshalBSONValue implements the bsoncodec.ValueUnmarshaler interface.
func (v *Val) UnmarshalBSONValue(t bsontype.Type, data []byte) error {
if v == nil {
return errors.New("cannot unmarshal into nil Value")
}
var err error
var ok = true
var rem []byte
switch t {
case bsontype.Double:
var f64 float64
f64, rem, ok = bsoncore.ReadDouble(data)
*v = Double(f64)
case bsontype.String:
var str string
str, rem, ok = bsoncore.ReadString(data)
*v = String(str)
case bsontype.EmbeddedDocument:
var raw []byte
var doc Doc
raw, rem, ok = bsoncore.ReadDocument(data)
doc, err = ReadDoc(raw)
*v = Document(doc)
case bsontype.Array:
var raw []byte
arr := make(Arr, 0)
raw, rem, ok = bsoncore.ReadArray(data)
err = arr.UnmarshalBSONValue(t, raw)
*v = Array(arr)
case bsontype.Binary:
var subtype byte
var bindata []byte
subtype, bindata, rem, ok = bsoncore.ReadBinary(data)
*v = Binary(subtype, bindata)
case bsontype.Undefined:
*v = Undefined()
case bsontype.ObjectID:
var oid primitive.ObjectID
oid, rem, ok = bsoncore.ReadObjectID(data)
*v = ObjectID(oid)
case bsontype.Boolean:
var b bool
b, rem, ok = bsoncore.ReadBoolean(data)
*v = Boolean(b)
case bsontype.DateTime:
var dt int64
dt, rem, ok = bsoncore.ReadDateTime(data)
*v = DateTime(dt)
case bsontype.Null:
*v = Null()
case bsontype.Regex:
var pattern, options string
pattern, options, rem, ok = bsoncore.ReadRegex(data)
*v = Regex(pattern, options)
case bsontype.DBPointer:
var ns string
var ptr primitive.ObjectID
ns, ptr, rem, ok = bsoncore.ReadDBPointer(data)
*v = DBPointer(ns, ptr)
case bsontype.JavaScript:
var js string
js, rem, ok = bsoncore.ReadJavaScript(data)
*v = JavaScript(js)
case bsontype.Symbol:
var symbol string
symbol, rem, ok = bsoncore.ReadSymbol(data)
*v = Symbol(symbol)
case bsontype.CodeWithScope:
var raw []byte
var code string
var scope Doc
code, raw, rem, ok = bsoncore.ReadCodeWithScope(data)
scope, err = ReadDoc(raw)
*v = CodeWithScope(code, scope)
case bsontype.Int32:
var i32 int32
i32, rem, ok = bsoncore.ReadInt32(data)
*v = Int32(i32)
case bsontype.Timestamp:
var i, t uint32
t, i, rem, ok = bsoncore.ReadTimestamp(data)
*v = Timestamp(t, i)
case bsontype.Int64:
var i64 int64
i64, rem, ok = bsoncore.ReadInt64(data)
*v = Int64(i64)
case bsontype.Decimal128:
var d128 primitive.Decimal128
d128, rem, ok = bsoncore.ReadDecimal128(data)
*v = Decimal128(d128)
case bsontype.MinKey:
*v = MinKey()
case bsontype.MaxKey:
*v = MaxKey()
default:
err = fmt.Errorf("invalid BSON type %v", t)
}
if !ok && err == nil {
err = bsoncore.NewInsufficientBytesError(data, rem)
}
return err
}
// Type returns the BSON type of this value.
func (v Val) Type() bsontype.Type {
if v.t == bsontype.Type(0) {
return bsontype.Null
}
return v.t
}
// IsNumber returns true if the type of v is a numberic BSON type.
func (v Val) IsNumber() bool {
switch v.Type() {
case bsontype.Double, bsontype.Int32, bsontype.Int64, bsontype.Decimal128:
return true
default:
return false
}
}
// Double returns the BSON double value the Value represents. It panics if the value is a BSON type
// other than double.
func (v Val) Double() float64 {
if v.t != bsontype.Double {
panic(ElementTypeError{"bson.Value.Double", v.t})
}
return math.Float64frombits(binary.LittleEndian.Uint64(v.bootstrap[0:8]))
}
// DoubleOK is the same as Double, but returns a boolean instead of panicking.
func (v Val) DoubleOK() (float64, bool) {
if v.t != bsontype.Double {
return 0, false
}
return math.Float64frombits(binary.LittleEndian.Uint64(v.bootstrap[0:8])), true
}
// StringValue returns the BSON string the Value represents. It panics if the value is a BSON type
// other than string.
//
// NOTE: This method is called StringValue to avoid it implementing the
// fmt.Stringer interface.
func (v Val) StringValue() string {
if v.t != bsontype.String {
panic(ElementTypeError{"bson.Value.StringValue", v.t})
}
return v.string()
}
// StringValueOK is the same as StringValue, but returns a boolean instead of
// panicking.
func (v Val) StringValueOK() (string, bool) {
if v.t != bsontype.String {
return "", false
}
return v.string(), true
}
func (v Val) asDoc() Doc {
doc, ok := v.primitive.(Doc)
if ok {
return doc
}
mdoc := v.primitive.(MDoc)
for k, v := range mdoc {
doc = append(doc, Elem{k, v})
}
return doc
}
func (v Val) asMDoc() MDoc {
mdoc, ok := v.primitive.(MDoc)
if ok {
return mdoc
}
mdoc = make(MDoc)
doc := v.primitive.(Doc)
for _, elem := range doc {
mdoc[elem.Key] = elem.Value
}
return mdoc
}
// Document returns the BSON embedded document value the Value represents. It panics if the value
// is a BSON type other than embedded document.
func (v Val) Document() Doc {
if v.t != bsontype.EmbeddedDocument {
panic(ElementTypeError{"bson.Value.Document", v.t})
}
return v.asDoc()
}
// DocumentOK is the same as Document, except it returns a boolean
// instead of panicking.
func (v Val) DocumentOK() (Doc, bool) {
if v.t != bsontype.EmbeddedDocument {
return nil, false
}
return v.asDoc(), true
}
// MDocument returns the BSON embedded document value the Value represents. It panics if the value
// is a BSON type other than embedded document.
func (v Val) MDocument() MDoc {
if v.t != bsontype.EmbeddedDocument {
panic(ElementTypeError{"bson.Value.MDocument", v.t})
}
return v.asMDoc()
}
// MDocumentOK is the same as Document, except it returns a boolean
// instead of panicking.
func (v Val) MDocumentOK() (MDoc, bool) {
if v.t != bsontype.EmbeddedDocument {
return nil, false
}
return v.asMDoc(), true
}
// Array returns the BSON array value the Value represents. It panics if the value is a BSON type
// other than array.
func (v Val) Array() Arr {
if v.t != bsontype.Array {
panic(ElementTypeError{"bson.Value.Array", v.t})
}
return v.primitive.(Arr)
}
// ArrayOK is the same as Array, except it returns a boolean
// instead of panicking.
func (v Val) ArrayOK() (Arr, bool) {
if v.t != bsontype.Array {
return nil, false
}
return v.primitive.(Arr), true
}
// Binary returns the BSON binary value the Value represents. It panics if the value is a BSON type
// other than binary.
func (v Val) Binary() (byte, []byte) {
if v.t != bsontype.Binary {
panic(ElementTypeError{"bson.Value.Binary", v.t})
}
bin := v.primitive.(primitive.Binary)
return bin.Subtype, bin.Data
}
// BinaryOK is the same as Binary, except it returns a boolean instead of
// panicking.
func (v Val) BinaryOK() (byte, []byte, bool) {
if v.t != bsontype.Binary {
return 0x00, nil, false
}
bin := v.primitive.(primitive.Binary)
return bin.Subtype, bin.Data, true
}
// Undefined returns the BSON undefined the Value represents. It panics if the value is a BSON type
// other than binary.
func (v Val) Undefined() {
if v.t != bsontype.Undefined {
panic(ElementTypeError{"bson.Value.Undefined", v.t})
}
}
// UndefinedOK is the same as Undefined, except it returns a boolean instead of
// panicking.
func (v Val) UndefinedOK() bool {
return v.t == bsontype.Undefined
}
// ObjectID returns the BSON ObjectID the Value represents. It panics if the value is a BSON type
// other than ObjectID.
func (v Val) ObjectID() primitive.ObjectID {
if v.t != bsontype.ObjectID {
panic(ElementTypeError{"bson.Value.ObjectID", v.t})
}
var oid primitive.ObjectID
copy(oid[:], v.bootstrap[:12])
return oid
}
// ObjectIDOK is the same as ObjectID, except it returns a boolean instead of
// panicking.
func (v Val) ObjectIDOK() (primitive.ObjectID, bool) {
if v.t != bsontype.ObjectID {
return primitive.ObjectID{}, false
}
var oid primitive.ObjectID
copy(oid[:], v.bootstrap[:12])
return oid, true
}
// Boolean returns the BSON boolean the Value represents. It panics if the value is a BSON type
// other than boolean.
func (v Val) Boolean() bool {
if v.t != bsontype.Boolean {
panic(ElementTypeError{"bson.Value.Boolean", v.t})
}
return v.bootstrap[0] == 0x01
}
// BooleanOK is the same as Boolean, except it returns a boolean instead of
// panicking.
func (v Val) BooleanOK() (bool, bool) {
if v.t != bsontype.Boolean {
return false, false
}
return v.bootstrap[0] == 0x01, true
}
// DateTime returns the BSON datetime the Value represents. It panics if the value is a BSON type
// other than datetime.
func (v Val) DateTime() int64 {
if v.t != bsontype.DateTime {
panic(ElementTypeError{"bson.Value.DateTime", v.t})
}
return v.i64()
}
// DateTimeOK is the same as DateTime, except it returns a boolean instead of
// panicking.
func (v Val) DateTimeOK() (int64, bool) {
if v.t != bsontype.DateTime {
return 0, false
}
return v.i64(), true
}
// Time returns the BSON datetime the Value represents as time.Time. It panics if the value is a BSON
// type other than datetime.
func (v Val) Time() time.Time {
if v.t != bsontype.DateTime {
panic(ElementTypeError{"bson.Value.Time", v.t})
}
i := v.i64()
return time.Unix(i/1000, i%1000*1000000)
}
// TimeOK is the same as Time, except it returns a boolean instead of
// panicking.
func (v Val) TimeOK() (time.Time, bool) {
if v.t != bsontype.DateTime {
return time.Time{}, false
}
i := v.i64()
return time.Unix(i/1000, i%1000*1000000), true
}
// Null returns the BSON undefined the Value represents. It panics if the value is a BSON type
// other than binary.
func (v Val) Null() {
if v.t != bsontype.Null && v.t != bsontype.Type(0) {
panic(ElementTypeError{"bson.Value.Null", v.t})
}
}
// NullOK is the same as Null, except it returns a boolean instead of
// panicking.
func (v Val) NullOK() bool {
if v.t != bsontype.Null && v.t != bsontype.Type(0) {
return false
}
return true
}
// Regex returns the BSON regex the Value represents. It panics if the value is a BSON type
// other than regex.
func (v Val) Regex() (pattern, options string) {
if v.t != bsontype.Regex {
panic(ElementTypeError{"bson.Value.Regex", v.t})
}
regex := v.primitive.(primitive.Regex)
return regex.Pattern, regex.Options
}
// RegexOK is the same as Regex, except that it returns a boolean
// instead of panicking.
func (v Val) RegexOK() (pattern, options string, ok bool) {
if v.t != bsontype.Regex {
return "", "", false
}
regex := v.primitive.(primitive.Regex)
return regex.Pattern, regex.Options, true
}
// DBPointer returns the BSON dbpointer the Value represents. It panics if the value is a BSON type
// other than dbpointer.
func (v Val) DBPointer() (string, primitive.ObjectID) {
if v.t != bsontype.DBPointer {
panic(ElementTypeError{"bson.Value.DBPointer", v.t})
}
dbptr := v.primitive.(primitive.DBPointer)
return dbptr.DB, dbptr.Pointer
}
// DBPointerOK is the same as DBPoitner, except that it returns a boolean
// instead of panicking.
func (v Val) DBPointerOK() (string, primitive.ObjectID, bool) {
if v.t != bsontype.DBPointer {
return "", primitive.ObjectID{}, false
}
dbptr := v.primitive.(primitive.DBPointer)
return dbptr.DB, dbptr.Pointer, true
}
// JavaScript returns the BSON JavaScript the Value represents. It panics if the value is a BSON type
// other than JavaScript.
func (v Val) JavaScript() string {
if v.t != bsontype.JavaScript {
panic(ElementTypeError{"bson.Value.JavaScript", v.t})
}
return v.string()
}
// JavaScriptOK is the same as Javascript, except that it returns a boolean
// instead of panicking.
func (v Val) JavaScriptOK() (string, bool) {
if v.t != bsontype.JavaScript {
return "", false
}
return v.string(), true
}
// Symbol returns the BSON symbol the Value represents. It panics if the value is a BSON type
// other than symbol.
func (v Val) Symbol() string {
if v.t != bsontype.Symbol {
panic(ElementTypeError{"bson.Value.Symbol", v.t})
}
return v.string()
}
// SymbolOK is the same as Javascript, except that it returns a boolean
// instead of panicking.
func (v Val) SymbolOK() (string, bool) {
if v.t != bsontype.Symbol {
return "", false
}
return v.string(), true
}
// CodeWithScope returns the BSON code with scope value the Value represents. It panics if the
// value is a BSON type other than code with scope.
func (v Val) CodeWithScope() (string, Doc) {
if v.t != bsontype.CodeWithScope {
panic(ElementTypeError{"bson.Value.CodeWithScope", v.t})
}
cws := v.primitive.(primitive.CodeWithScope)
return string(cws.Code), cws.Scope.(Doc)
}
// CodeWithScopeOK is the same as JavascriptWithScope,
// except that it returns a boolean instead of panicking.
func (v Val) CodeWithScopeOK() (string, Doc, bool) {
if v.t != bsontype.CodeWithScope {
return "", nil, false
}
cws := v.primitive.(primitive.CodeWithScope)
return string(cws.Code), cws.Scope.(Doc), true
}
// Int32 returns the BSON int32 the Value represents. It panics if the value is a BSON type
// other than int32.
func (v Val) Int32() int32 {
if v.t != bsontype.Int32 {
panic(ElementTypeError{"bson.Value.Int32", v.t})
}
return int32(v.bootstrap[0]) | int32(v.bootstrap[1])<<8 |
int32(v.bootstrap[2])<<16 | int32(v.bootstrap[3])<<24
}
// Int32OK is the same as Int32, except that it returns a boolean instead of
// panicking.
func (v Val) Int32OK() (int32, bool) {
if v.t != bsontype.Int32 {
return 0, false
}
return int32(v.bootstrap[0]) | int32(v.bootstrap[1])<<8 |
int32(v.bootstrap[2])<<16 | int32(v.bootstrap[3])<<24,
true
}
// Timestamp returns the BSON timestamp the Value represents. It panics if the value is a
// BSON type other than timestamp.
func (v Val) Timestamp() (t, i uint32) {
if v.t != bsontype.Timestamp {
panic(ElementTypeError{"bson.Value.Timestamp", v.t})
}
return uint32(v.bootstrap[4]) | uint32(v.bootstrap[5])<<8 |
uint32(v.bootstrap[6])<<16 | uint32(v.bootstrap[7])<<24,
uint32(v.bootstrap[0]) | uint32(v.bootstrap[1])<<8 |
uint32(v.bootstrap[2])<<16 | uint32(v.bootstrap[3])<<24
}
// TimestampOK is the same as Timestamp, except that it returns a boolean
// instead of panicking.
func (v Val) TimestampOK() (t uint32, i uint32, ok bool) {
if v.t != bsontype.Timestamp {
return 0, 0, false
}
return uint32(v.bootstrap[4]) | uint32(v.bootstrap[5])<<8 |
uint32(v.bootstrap[6])<<16 | uint32(v.bootstrap[7])<<24,
uint32(v.bootstrap[0]) | uint32(v.bootstrap[1])<<8 |
uint32(v.bootstrap[2])<<16 | uint32(v.bootstrap[3])<<24,
true
}
// Int64 returns the BSON int64 the Value represents. It panics if the value is a BSON type
// other than int64.
func (v Val) Int64() int64 {
if v.t != bsontype.Int64 {
panic(ElementTypeError{"bson.Value.Int64", v.t})
}
return v.i64()
}
// Int64OK is the same as Int64, except that it returns a boolean instead of
// panicking.
func (v Val) Int64OK() (int64, bool) {
if v.t != bsontype.Int64 {
return 0, false
}
return v.i64(), true
}
// Decimal128 returns the BSON decimal128 value the Value represents. It panics if the value is a
// BSON type other than decimal128.
func (v Val) Decimal128() primitive.Decimal128 {
if v.t != bsontype.Decimal128 {
panic(ElementTypeError{"bson.Value.Decimal128", v.t})
}
return v.primitive.(primitive.Decimal128)
}
// Decimal128OK is the same as Decimal128, except that it returns a boolean
// instead of panicking.
func (v Val) Decimal128OK() (primitive.Decimal128, bool) {
if v.t != bsontype.Decimal128 {
return primitive.Decimal128{}, false
}
return v.primitive.(primitive.Decimal128), true
}
// MinKey returns the BSON minkey the Value represents. It panics if the value is a BSON type
// other than binary.
func (v Val) MinKey() {
if v.t != bsontype.MinKey {
panic(ElementTypeError{"bson.Value.MinKey", v.t})
}
}
// MinKeyOK is the same as MinKey, except it returns a boolean instead of
// panicking.
func (v Val) MinKeyOK() bool {
return v.t == bsontype.MinKey
}
// MaxKey returns the BSON maxkey the Value represents. It panics if the value is a BSON type
// other than binary.
func (v Val) MaxKey() {
if v.t != bsontype.MaxKey {
panic(ElementTypeError{"bson.Value.MaxKey", v.t})
}
}
// MaxKeyOK is the same as MaxKey, except it returns a boolean instead of
// panicking.
func (v Val) MaxKeyOK() bool {
return v.t == bsontype.MaxKey
}
// Equal compares v to v2 and returns true if they are equal. Unknown BSON types are
// never equal. Two empty values are equal.
func (v Val) Equal(v2 Val) bool {
if v.Type() != v2.Type() {
return false
}
if v.IsZero() && v2.IsZero() {
return true
}
switch v.Type() {
case bsontype.Double, bsontype.DateTime, bsontype.Timestamp, bsontype.Int64:
return bytes.Equal(v.bootstrap[0:8], v2.bootstrap[0:8])
case bsontype.String:
return v.string() == v2.string()
case bsontype.EmbeddedDocument:
return v.equalDocs(v2)
case bsontype.Array:
return v.Array().Equal(v2.Array())
case bsontype.Binary:
return v.primitive.(primitive.Binary).Equal(v2.primitive.(primitive.Binary))
case bsontype.Undefined:
return true
case bsontype.ObjectID:
return bytes.Equal(v.bootstrap[0:12], v2.bootstrap[0:12])
case bsontype.Boolean:
return v.bootstrap[0] == v2.bootstrap[0]
case bsontype.Null:
return true
case bsontype.Regex:
return v.primitive.(primitive.Regex).Equal(v2.primitive.(primitive.Regex))
case bsontype.DBPointer:
return v.primitive.(primitive.DBPointer).Equal(v2.primitive.(primitive.DBPointer))
case bsontype.JavaScript:
return v.JavaScript() == v2.JavaScript()
case bsontype.Symbol:
return v.Symbol() == v2.Symbol()
case bsontype.CodeWithScope:
code1, scope1 := v.primitive.(primitive.CodeWithScope).Code, v.primitive.(primitive.CodeWithScope).Scope
code2, scope2 := v2.primitive.(primitive.CodeWithScope).Code, v2.primitive.(primitive.CodeWithScope).Scope
return code1 == code2 && v.equalInterfaceDocs(scope1, scope2)
case bsontype.Int32:
return v.Int32() == v2.Int32()
case bsontype.Decimal128:
h, l := v.Decimal128().GetBytes()
h2, l2 := v2.Decimal128().GetBytes()
return h == h2 && l == l2
case bsontype.MinKey:
return true
case bsontype.MaxKey:
return true
default:
return false
}
}
func (v Val) equalDocs(v2 Val) bool {
_, ok1 := v.primitive.(MDoc)
_, ok2 := v2.primitive.(MDoc)
if ok1 || ok2 {
return v.asMDoc().Equal(v2.asMDoc())
}
return v.asDoc().Equal(v2.asDoc())
}
func (Val) equalInterfaceDocs(i, i2 interface{}) bool {
switch d := i.(type) {
case MDoc:
d2, ok := i2.(IDoc)
if !ok {
return false
}
return d.Equal(d2)
case Doc:
d2, ok := i2.(IDoc)
if !ok {
return false
}
return d.Equal(d2)
case nil:
return i2 == nil
default:
return false
}
}
+23
View File
@@ -0,0 +1,23 @@
# Driver Library Design
This document outlines the design for this package.
## Deployment, Server, and Connection
Acquiring a `Connection` from a `Server` selected from a `Deployment` enables sending and receiving
wire messages. A `Deployment` represents an set of MongoDB servers and a `Server` represents a
member of that set. These three types form the operation execution stack.
### Compression
Compression is handled by Connection type while uncompression is handled automatically by the
Operation type. This is done because the compressor to use for compressing a wire message is
chosen by the connection during handshake, while uncompression can be performed without this
information. This does make the design of compression non-symmetric, but it makes the design simpler
to implement and more consistent.
## Operation
The `Operation` type handles executing a series of commands using a `Deployment`. For most uses
`Operation` will only execute a single command, but the main use case for a series of commands is
batch split write commands, such as insert. The type itself is heavily documented, so reading the
code and comments together should provide an understanding of how the type works.
This type is not meant to be used directly by callers. Instead a wrapping type should be defined
using the IDL.
+225
View File
@@ -0,0 +1,225 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"context"
"errors"
"fmt"
"go.mongodb.org/mongo-driver/mongo/address"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/operation"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// AuthenticatorFactory constructs an authenticator.
type AuthenticatorFactory func(cred *Cred) (Authenticator, error)
var authFactories = make(map[string]AuthenticatorFactory)
func init() {
RegisterAuthenticatorFactory("", newDefaultAuthenticator)
RegisterAuthenticatorFactory(SCRAMSHA1, newScramSHA1Authenticator)
RegisterAuthenticatorFactory(SCRAMSHA256, newScramSHA256Authenticator)
RegisterAuthenticatorFactory(MONGODBCR, newMongoDBCRAuthenticator)
RegisterAuthenticatorFactory(PLAIN, newPlainAuthenticator)
RegisterAuthenticatorFactory(GSSAPI, newGSSAPIAuthenticator)
RegisterAuthenticatorFactory(MongoDBX509, newMongoDBX509Authenticator)
RegisterAuthenticatorFactory(MongoDBAWS, newMongoDBAWSAuthenticator)
}
// CreateAuthenticator creates an authenticator.
func CreateAuthenticator(name string, cred *Cred) (Authenticator, error) {
if f, ok := authFactories[name]; ok {
return f(cred)
}
return nil, newAuthError(fmt.Sprintf("unknown authenticator: %s", name), nil)
}
// RegisterAuthenticatorFactory registers the authenticator factory.
func RegisterAuthenticatorFactory(name string, factory AuthenticatorFactory) {
authFactories[name] = factory
}
// HandshakeOptions packages options that can be passed to the Handshaker()
// function. DBUser is optional but must be of the form <dbname.username>;
// if non-empty, then the connection will do SASL mechanism negotiation.
type HandshakeOptions struct {
AppName string
Authenticator Authenticator
Compressors []string
DBUser string
PerformAuthentication func(description.Server) bool
ClusterClock *session.ClusterClock
ServerAPI *driver.ServerAPIOptions
LoadBalanced bool
}
type authHandshaker struct {
wrapped driver.Handshaker
options *HandshakeOptions
handshakeInfo driver.HandshakeInformation
conversation SpeculativeConversation
}
var _ driver.Handshaker = (*authHandshaker)(nil)
// GetHandshakeInformation performs the initial MongoDB handshake to retrieve the required information for the provided
// connection.
func (ah *authHandshaker) GetHandshakeInformation(ctx context.Context, addr address.Address, conn driver.Connection) (driver.HandshakeInformation, error) {
if ah.wrapped != nil {
return ah.wrapped.GetHandshakeInformation(ctx, addr, conn)
}
op := operation.NewHello().
AppName(ah.options.AppName).
Compressors(ah.options.Compressors).
SASLSupportedMechs(ah.options.DBUser).
ClusterClock(ah.options.ClusterClock).
ServerAPI(ah.options.ServerAPI).
LoadBalanced(ah.options.LoadBalanced)
if ah.options.Authenticator != nil {
if speculativeAuth, ok := ah.options.Authenticator.(SpeculativeAuthenticator); ok {
var err error
ah.conversation, err = speculativeAuth.CreateSpeculativeConversation()
if err != nil {
return driver.HandshakeInformation{}, newAuthError("failed to create conversation", err)
}
firstMsg, err := ah.conversation.FirstMessage()
if err != nil {
return driver.HandshakeInformation{}, newAuthError("failed to create speculative authentication message", err)
}
op = op.SpeculativeAuthenticate(firstMsg)
}
}
var err error
ah.handshakeInfo, err = op.GetHandshakeInformation(ctx, addr, conn)
if err != nil {
return driver.HandshakeInformation{}, newAuthError("handshake failure", err)
}
return ah.handshakeInfo, nil
}
// FinishHandshake performs authentication for conn if necessary.
func (ah *authHandshaker) FinishHandshake(ctx context.Context, conn driver.Connection) error {
performAuth := ah.options.PerformAuthentication
if performAuth == nil {
performAuth = func(serv description.Server) bool {
// Authentication is possible against all server types except arbiters
return serv.Kind != description.RSArbiter
}
}
desc := conn.Description()
if performAuth(desc) && ah.options.Authenticator != nil {
cfg := &Config{
Description: desc,
Connection: conn,
ClusterClock: ah.options.ClusterClock,
HandshakeInfo: ah.handshakeInfo,
ServerAPI: ah.options.ServerAPI,
}
if err := ah.authenticate(ctx, cfg); err != nil {
return newAuthError("auth error", err)
}
}
if ah.wrapped == nil {
return nil
}
return ah.wrapped.FinishHandshake(ctx, conn)
}
func (ah *authHandshaker) authenticate(ctx context.Context, cfg *Config) error {
// If the initial hello reply included a response to the speculative authentication attempt, we only need to
// conduct the remainder of the conversation.
if speculativeResponse := ah.handshakeInfo.SpeculativeAuthenticate; speculativeResponse != nil {
// Defensively ensure that the server did not include a response if speculative auth was not attempted.
if ah.conversation == nil {
return errors.New("speculative auth was not attempted but the server included a response")
}
return ah.conversation.Finish(ctx, cfg, speculativeResponse)
}
// If the server does not support speculative authentication or the first attempt was not successful, we need to
// perform authentication from scratch.
return ah.options.Authenticator.Auth(ctx, cfg)
}
// Handshaker creates a connection handshaker for the given authenticator.
func Handshaker(h driver.Handshaker, options *HandshakeOptions) driver.Handshaker {
return &authHandshaker{
wrapped: h,
options: options,
}
}
// Config holds the information necessary to perform an authentication attempt.
type Config struct {
Description description.Server
Connection driver.Connection
ClusterClock *session.ClusterClock
HandshakeInfo driver.HandshakeInformation
ServerAPI *driver.ServerAPIOptions
}
// Authenticator handles authenticating a connection.
type Authenticator interface {
// Auth authenticates the connection.
Auth(context.Context, *Config) error
}
func newAuthError(msg string, inner error) error {
return &Error{
message: msg,
inner: inner,
}
}
func newError(err error, mech string) error {
return &Error{
message: fmt.Sprintf("unable to authenticate using mechanism \"%s\"", mech),
inner: err,
}
}
// Error is an error that occurred during authentication.
type Error struct {
message string
inner error
}
func (e *Error) Error() string {
if e.inner == nil {
return e.message
}
return fmt.Sprintf("%s: %s", e.message, e.inner)
}
// Inner returns the wrapped error.
func (e *Error) Inner() error {
return e.inner
}
// Unwrap returns the underlying error.
func (e *Error) Unwrap() error {
return e.inner
}
// Message returns the message.
func (e *Error) Message() string {
return e.message
}
+347
View File
@@ -0,0 +1,347 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver/auth/internal/awsv4"
)
type clientState int
const (
clientStarting clientState = iota
clientFirst
clientFinal
clientDone
)
type awsConversation struct {
state clientState
valid bool
nonce []byte
username string
password string
token string
}
type serverMessage struct {
Nonce primitive.Binary `bson:"s"`
Host string `bson:"h"`
}
type ecsResponse struct {
AccessKeyID string `json:"AccessKeyId"`
SecretAccessKey string `json:"SecretAccessKey"`
Token string `json:"Token"`
}
const (
amzDateFormat = "20060102T150405Z"
awsRelativeURI = "http://169.254.170.2/"
awsEC2URI = "http://169.254.169.254/"
awsEC2RolePath = "latest/meta-data/iam/security-credentials/"
awsEC2TokenPath = "latest/api/token"
defaultRegion = "us-east-1"
maxHostLength = 255
defaultHTTPTimeout = 10 * time.Second
responceNonceLength = 64
)
// Step takes a string provided from a server (or just an empty string for the
// very first conversation step) and attempts to move the authentication
// conversation forward. It returns a string to be sent to the server or an
// error if the server message is invalid. Calling Step after a conversation
// completes is also an error.
func (ac *awsConversation) Step(challenge []byte) (response []byte, err error) {
switch ac.state {
case clientStarting:
ac.state = clientFirst
response = ac.firstMsg()
case clientFirst:
ac.state = clientFinal
response, err = ac.finalMsg(challenge)
case clientFinal:
ac.state = clientDone
ac.valid = true
default:
response, err = nil, errors.New("Conversation already completed")
}
return
}
// Done returns true if the conversation is completed or has errored.
func (ac *awsConversation) Done() bool {
return ac.state == clientDone
}
// Valid returns true if the conversation successfully authenticated with the
// server, including counter-validation that the server actually has the
// user's stored credentials.
func (ac *awsConversation) Valid() bool {
return ac.valid
}
func getRegion(host string) (string, error) {
region := defaultRegion
if len(host) == 0 {
return "", errors.New("invalid STS host: empty")
}
if len(host) > maxHostLength {
return "", errors.New("invalid STS host: too large")
}
// The implicit region for sts.amazonaws.com is us-east-1
if host == "sts.amazonaws.com" {
return region, nil
}
if strings.HasPrefix(host, ".") || strings.HasSuffix(host, ".") || strings.Contains(host, "..") {
return "", errors.New("invalid STS host: empty part")
}
// If the host has multiple parts, the second part is the region
parts := strings.Split(host, ".")
if len(parts) >= 2 {
region = parts[1]
}
return region, nil
}
func (ac *awsConversation) validateAndMakeCredentials() (*awsv4.StaticProvider, error) {
if ac.username != "" && ac.password == "" {
return nil, errors.New("ACCESS_KEY_ID is set, but SECRET_ACCESS_KEY is missing")
}
if ac.username == "" && ac.password != "" {
return nil, errors.New("SECRET_ACCESS_KEY is set, but ACCESS_KEY_ID is missing")
}
if ac.username == "" && ac.password == "" && ac.token != "" {
return nil, errors.New("AWS_SESSION_TOKEN is set, but ACCESS_KEY_ID and SECRET_ACCESS_KEY are missing")
}
if ac.username != "" || ac.password != "" || ac.token != "" {
return &awsv4.StaticProvider{Value: awsv4.Value{
AccessKeyID: ac.username,
SecretAccessKey: ac.password,
SessionToken: ac.token,
}}, nil
}
return nil, nil
}
func executeAWSHTTPRequest(req *http.Request) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultHTTPTimeout)
defer cancel()
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()
return ioutil.ReadAll(resp.Body)
}
func (ac *awsConversation) getEC2Credentials() (*awsv4.StaticProvider, error) {
// get token
req, err := http.NewRequest("PUT", awsEC2URI+awsEC2TokenPath, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "30")
token, err := executeAWSHTTPRequest(req)
if err != nil {
return nil, err
}
if len(token) == 0 {
return nil, errors.New("unable to retrieve token from EC2 metadata")
}
tokenStr := string(token)
// get role name
req, err = http.NewRequest("GET", awsEC2URI+awsEC2RolePath, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-aws-ec2-metadata-token", tokenStr)
role, err := executeAWSHTTPRequest(req)
if err != nil {
return nil, err
}
if len(role) == 0 {
return nil, errors.New("unable to retrieve role_name from EC2 metadata")
}
// get credentials
pathWithRole := awsEC2URI + awsEC2RolePath + string(role)
req, err = http.NewRequest("GET", pathWithRole, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-aws-ec2-metadata-token", tokenStr)
creds, err := executeAWSHTTPRequest(req)
if err != nil {
return nil, err
}
var es2Resp ecsResponse
err = json.Unmarshal(creds, &es2Resp)
if err != nil {
return nil, err
}
ac.username = es2Resp.AccessKeyID
ac.password = es2Resp.SecretAccessKey
ac.token = es2Resp.Token
return ac.validateAndMakeCredentials()
}
func (ac *awsConversation) getCredentials() (*awsv4.StaticProvider, error) {
// Credentials passed through URI
creds, err := ac.validateAndMakeCredentials()
if creds != nil || err != nil {
return creds, err
}
// Credentials from environment variables
ac.username = os.Getenv("AWS_ACCESS_KEY_ID")
ac.password = os.Getenv("AWS_SECRET_ACCESS_KEY")
ac.token = os.Getenv("AWS_SESSION_TOKEN")
creds, err = ac.validateAndMakeCredentials()
if creds != nil || err != nil {
return creds, err
}
// Credentials from ECS metadata
relativeEcsURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
if len(relativeEcsURI) > 0 {
fullURI := awsRelativeURI + relativeEcsURI
req, err := http.NewRequest("GET", fullURI, nil)
if err != nil {
return nil, err
}
body, err := executeAWSHTTPRequest(req)
if err != nil {
return nil, err
}
var espResp ecsResponse
err = json.Unmarshal(body, &espResp)
if err != nil {
return nil, err
}
ac.username = espResp.AccessKeyID
ac.password = espResp.SecretAccessKey
ac.token = espResp.Token
creds, err = ac.validateAndMakeCredentials()
if creds != nil || err != nil {
return creds, err
}
}
// Credentials from EC2 metadata
creds, err = ac.getEC2Credentials()
if creds == nil && err == nil {
return nil, errors.New("unable to get credentials")
}
return creds, err
}
func (ac *awsConversation) firstMsg() []byte {
// Values are cached for use in final message parameters
ac.nonce = make([]byte, 32)
_, _ = rand.Read(ac.nonce)
idx, msg := bsoncore.AppendDocumentStart(nil)
msg = bsoncore.AppendInt32Element(msg, "p", 110)
msg = bsoncore.AppendBinaryElement(msg, "r", 0x00, ac.nonce)
msg, _ = bsoncore.AppendDocumentEnd(msg, idx)
return msg
}
func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) {
var sm serverMessage
err := bson.Unmarshal(s1, &sm)
if err != nil {
return nil, err
}
// Check nonce prefix
if sm.Nonce.Subtype != 0x00 {
return nil, errors.New("server reply contained unexpected binary subtype")
}
if len(sm.Nonce.Data) != responceNonceLength {
return nil, fmt.Errorf("server reply nonce was not %v bytes", responceNonceLength)
}
if !bytes.HasPrefix(sm.Nonce.Data, ac.nonce) {
return nil, errors.New("server nonce did not extend client nonce")
}
region, err := getRegion(sm.Host)
if err != nil {
return nil, err
}
creds, err := ac.getCredentials()
if err != nil {
return nil, err
}
currentTime := time.Now().UTC()
body := "Action=GetCallerIdentity&Version=2011-06-15"
// Create http.Request
req, _ := http.NewRequest("POST", "/", strings.NewReader(body))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Content-Length", "43")
req.Host = sm.Host
req.Header.Set("X-Amz-Date", currentTime.Format(amzDateFormat))
if len(ac.token) > 0 {
req.Header.Set("X-Amz-Security-Token", ac.token)
}
req.Header.Set("X-MongoDB-Server-Nonce", base64.StdEncoding.EncodeToString(sm.Nonce.Data))
req.Header.Set("X-MongoDB-GS2-CB-Flag", "n")
// Create signer with credentials
signer := awsv4.NewSigner(creds)
// Get signed header
_, err = signer.Sign(req, strings.NewReader(body), "sts", region, currentTime)
if err != nil {
return nil, err
}
// create message
idx, msg := bsoncore.AppendDocumentStart(nil)
msg = bsoncore.AppendStringElement(msg, "a", req.Header.Get("Authorization"))
msg = bsoncore.AppendStringElement(msg, "d", req.Header.Get("X-Amz-Date"))
if len(ac.token) > 0 {
msg = bsoncore.AppendStringElement(msg, "t", ac.token)
}
msg, _ = bsoncore.AppendDocumentEnd(msg, idx)
return msg, nil
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"context"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// SpeculativeConversation represents an authentication conversation that can be merged with the initial connection
// handshake.
//
// FirstMessage method returns the first message to be sent to the server. This message will be included in the initial
// hello command.
//
// Finish takes the server response to the initial message and conducts the remainder of the conversation to
// authenticate the provided connection.
type SpeculativeConversation interface {
FirstMessage() (bsoncore.Document, error)
Finish(ctx context.Context, cfg *Config, firstResponse bsoncore.Document) error
}
// SpeculativeAuthenticator represents an authenticator that supports speculative authentication.
type SpeculativeAuthenticator interface {
CreateSpeculativeConversation() (SpeculativeConversation, error)
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
// Cred is a user's credential.
type Cred struct {
Source string
Username string
Password string
PasswordSet bool
Props map[string]string
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"context"
"fmt"
"go.mongodb.org/mongo-driver/mongo/description"
)
func newDefaultAuthenticator(cred *Cred) (Authenticator, error) {
scram, err := newScramSHA256Authenticator(cred)
if err != nil {
return nil, newAuthError("failed to create internal authenticator", err)
}
speculative, ok := scram.(SpeculativeAuthenticator)
if !ok {
typeErr := fmt.Errorf("expected SCRAM authenticator to be SpeculativeAuthenticator but got %T", scram)
return nil, newAuthError("failed to create internal authenticator", typeErr)
}
return &DefaultAuthenticator{
Cred: cred,
speculativeAuthenticator: speculative,
}, nil
}
// DefaultAuthenticator uses SCRAM-SHA-1 or MONGODB-CR depending
// on the server version.
type DefaultAuthenticator struct {
Cred *Cred
// The authenticator to use for speculative authentication. Because the correct auth mechanism is unknown when doing
// the initial hello, SCRAM-SHA-256 is used for the speculative attempt.
speculativeAuthenticator SpeculativeAuthenticator
}
var _ SpeculativeAuthenticator = (*DefaultAuthenticator)(nil)
// CreateSpeculativeConversation creates a speculative conversation for SCRAM authentication.
func (a *DefaultAuthenticator) CreateSpeculativeConversation() (SpeculativeConversation, error) {
return a.speculativeAuthenticator.CreateSpeculativeConversation()
}
// Auth authenticates the connection.
func (a *DefaultAuthenticator) Auth(ctx context.Context, cfg *Config) error {
var actual Authenticator
var err error
switch chooseAuthMechanism(cfg) {
case SCRAMSHA256:
actual, err = newScramSHA256Authenticator(a.Cred)
case SCRAMSHA1:
actual, err = newScramSHA1Authenticator(a.Cred)
default:
actual, err = newMongoDBCRAuthenticator(a.Cred)
}
if err != nil {
return newAuthError("error creating authenticator", err)
}
return actual.Auth(ctx, cfg)
}
// If a server provides a list of supported mechanisms, we choose
// SCRAM-SHA-256 if it exists or else MUST use SCRAM-SHA-1.
// Otherwise, we decide based on what is supported.
func chooseAuthMechanism(cfg *Config) string {
if saslSupportedMechs := cfg.HandshakeInfo.SaslSupportedMechs; saslSupportedMechs != nil {
for _, v := range saslSupportedMechs {
if v == SCRAMSHA256 {
return v
}
}
return SCRAMSHA1
}
if err := scramSHA1Supported(cfg.HandshakeInfo.Description.WireVersion); err == nil {
return SCRAMSHA1
}
return MONGODBCR
}
// scramSHA1Supported returns an error if the given server version does not support scram-sha-1.
func scramSHA1Supported(wireVersion *description.VersionRange) error {
if wireVersion != nil && wireVersion.Max < 3 {
return fmt.Errorf("SCRAM-SHA-1 is only supported for servers 3.0 or newer")
}
return nil
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
// Package auth is not for public use.
//
// The API for packages in the 'private' directory have no stability
// guarantee.
//
// The packages within the 'private' directory would normally be put into an
// 'internal' directory to prohibit their use outside the 'mongo' directory.
// However, some MongoDB tools require very low-level access to the building
// blocks of a driver, so we have placed them under 'private' to allow these
// packages to be imported by projects that need them.
//
// These package APIs may be modified in backwards-incompatible ways at any
// time.
//
// You are strongly discouraged from directly using any packages
// under 'private'.
package auth
+59
View File
@@ -0,0 +1,59 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build gssapi && (windows || linux || darwin)
// +build gssapi
// +build windows linux darwin
package auth
import (
"context"
"fmt"
"net"
"go.mongodb.org/mongo-driver/x/mongo/driver/auth/internal/gssapi"
)
// GSSAPI is the mechanism name for GSSAPI.
const GSSAPI = "GSSAPI"
func newGSSAPIAuthenticator(cred *Cred) (Authenticator, error) {
if cred.Source != "" && cred.Source != "$external" {
return nil, newAuthError("GSSAPI source must be empty or $external", nil)
}
return &GSSAPIAuthenticator{
Username: cred.Username,
Password: cred.Password,
PasswordSet: cred.PasswordSet,
Props: cred.Props,
}, nil
}
// GSSAPIAuthenticator uses the GSSAPI algorithm over SASL to authenticate a connection.
type GSSAPIAuthenticator struct {
Username string
Password string
PasswordSet bool
Props map[string]string
}
// Auth authenticates the connection.
func (a *GSSAPIAuthenticator) Auth(ctx context.Context, cfg *Config) error {
target := cfg.Description.Addr.String()
hostname, _, err := net.SplitHostPort(target)
if err != nil {
return newAuthError(fmt.Sprintf("invalid endpoint (%s) specified: %s", target, err), nil)
}
client, err := gssapi.New(hostname, a.Username, a.Password, a.PasswordSet, a.Props)
if err != nil {
return newAuthError("error creating gssapi", err)
}
return ConductSaslConversation(ctx, cfg, "$external", client)
}
@@ -0,0 +1,17 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build !gssapi
// +build !gssapi
package auth
// GSSAPI is the mechanism name for GSSAPI.
const GSSAPI = "GSSAPI"
func newGSSAPIAuthenticator(cred *Cred) (Authenticator, error) {
return nil, newAuthError("GSSAPI support not enabled during build (-tags gssapi)", nil)
}
@@ -0,0 +1,22 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build gssapi && !windows && !linux && !darwin
// +build gssapi,!windows,!linux,!darwin
package auth
import (
"fmt"
"runtime"
)
// GSSAPI is the mechanism name for GSSAPI.
const GSSAPI = "GSSAPI"
func newGSSAPIAuthenticator(cred *Cred) (Authenticator, error) {
return nil, newAuthError(fmt.Sprintf("GSSAPI is not supported on %s", runtime.GOOS), nil)
}
@@ -0,0 +1,63 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Based on github.com/aws/aws-sdk-go by Amazon.com, Inc. with code from:
// - github.com/aws/aws-sdk-go/blob/v1.34.28/aws/credentials/static_provider.go
// - github.com/aws/aws-sdk-go/blob/v1.34.28/aws/credentials/credentials.go
// See THIRD-PARTY-NOTICES for original license terms
package awsv4
import (
"errors"
)
// StaticProviderName provides a name of Static provider
const StaticProviderName = "StaticProvider"
var (
// ErrStaticCredentialsEmpty is emitted when static credentials are empty.
ErrStaticCredentialsEmpty = errors.New("EmptyStaticCreds: static credentials are empty")
)
// A Value is the AWS credentials value for individual credential fields.
type Value struct {
// AWS Access key ID
AccessKeyID string
// AWS Secret Access Key
SecretAccessKey string
// AWS Session Token
SessionToken string
// Provider used to get credentials
ProviderName string
}
// HasKeys returns if the credentials Value has both AccessKeyID and
// SecretAccessKey value set.
func (v Value) HasKeys() bool {
return len(v.AccessKeyID) != 0 && len(v.SecretAccessKey) != 0
}
// A StaticProvider is a set of credentials which are set programmatically,
// and will never expire.
type StaticProvider struct {
Value
}
// Retrieve returns the credentials or error if the credentials are invalid.
func (s *StaticProvider) Retrieve() (Value, error) {
if s.AccessKeyID == "" || s.SecretAccessKey == "" {
return Value{ProviderName: StaticProviderName}, ErrStaticCredentialsEmpty
}
if len(s.Value.ProviderName) == 0 {
s.Value.ProviderName = StaticProviderName
}
return s.Value, nil
}
@@ -0,0 +1,15 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Based on github.com/aws/aws-sdk-go v1.34.28 by Amazon.com, Inc.
// See THIRD-PARTY-NOTICES for original license terms
// Package awsv4 implements signing for AWS V4 signer with static credentials,
// and is based on and modified from code in the package aws-sdk-go. The
// modifications remove non-static credentials, support for non-sts services,
// and the options for v4.Signer. They also reduce the number of non-Go
// library dependencies.
package awsv4
@@ -0,0 +1,80 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Based on github.com/aws/aws-sdk-go by Amazon.com, Inc. with code from:
// - github.com/aws/aws-sdk-go/blob/v1.34.28/aws/request/request.go
// See THIRD-PARTY-NOTICES for original license terms
package awsv4
import (
"net/http"
"strings"
)
// Returns host from request
func getHost(r *http.Request) string {
if r.Host != "" {
return r.Host
}
if r.URL == nil {
return ""
}
return r.URL.Host
}
// Hostname returns u.Host, without any port number.
//
// If Host is an IPv6 literal with a port number, Hostname returns the
// IPv6 literal without the square brackets. IPv6 literals may include
// a zone identifier.
//
// Copied from the Go 1.8 standard library (net/url)
func stripPort(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return hostport
}
if i := strings.IndexByte(hostport, ']'); i != -1 {
return strings.TrimPrefix(hostport[:i], "[")
}
return hostport[:colon]
}
// Port returns the port part of u.Host, without the leading colon.
// If u.Host doesn't contain a port, Port returns an empty string.
//
// Copied from the Go 1.8 standard library (net/url)
func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return ""
}
if i := strings.Index(hostport, "]:"); i != -1 {
return hostport[i+len("]:"):]
}
if strings.Contains(hostport, "]") {
return ""
}
return hostport[colon+len(":"):]
}
// Returns true if the specified URI is using the standard port
// (i.e. port 80 for HTTP URIs or 443 for HTTPS URIs)
func isDefaultPort(scheme, port string) bool {
if port == "" {
return true
}
lowerCaseScheme := strings.ToLower(scheme)
if (lowerCaseScheme == "http" && port == "80") || (lowerCaseScheme == "https" && port == "443") {
return true
}
return false
}
@@ -0,0 +1,46 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Based on github.com/aws/aws-sdk-go by Amazon.com, Inc. with code from:
// - github.com/aws/aws-sdk-go/blob/v1.34.28/private/protocol/rest/build.go
// See THIRD-PARTY-NOTICES for original license terms
package awsv4
import (
"bytes"
"fmt"
)
// Whether the byte value can be sent without escaping in AWS URLs
var noEscape [256]bool
func init() {
for i := 0; i < len(noEscape); i++ {
// AWS expects every character except these to be escaped
noEscape[i] = (i >= 'A' && i <= 'Z') ||
(i >= 'a' && i <= 'z') ||
(i >= '0' && i <= '9') ||
i == '-' ||
i == '.' ||
i == '_' ||
i == '~'
}
}
// EscapePath escapes part of a URL path in Amazon style
func EscapePath(path string, encodeSep bool) string {
var buf bytes.Buffer
for i := 0; i < len(path); i++ {
c := path[i]
if noEscape[c] || (c == '/' && !encodeSep) {
buf.WriteByte(c)
} else {
fmt.Fprintf(&buf, "%%%02X", c)
}
}
return buf.String()
}
@@ -0,0 +1,98 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Based on github.com/aws/aws-sdk-go by Amazon.com, Inc. with code from:
// - github.com/aws/aws-sdk-go/blob/v1.34.28/aws/signer/v4/header_rules.go
// - github.com/aws/aws-sdk-go/blob/v1.34.28/internal/strings/strings.go
// See THIRD-PARTY-NOTICES for original license terms
package awsv4
import (
"strings"
)
// validator houses a set of rule needed for validation of a
// string value
type rules []rule
// rule interface allows for more flexible rules and just simply
// checks whether or not a value adheres to that rule
type rule interface {
IsValid(value string) bool
}
// IsValid will iterate through all rules and see if any rules
// apply to the value and supports nested rules
func (r rules) IsValid(value string) bool {
for _, rule := range r {
if rule.IsValid(value) {
return true
}
}
return false
}
// mapRule generic rule for maps
type mapRule map[string]struct{}
// IsValid for the map rule satisfies whether it exists in the map
func (m mapRule) IsValid(value string) bool {
_, ok := m[value]
return ok
}
// allowlist is a generic rule for allowlisting
type allowlist struct {
rule
}
// IsValid for allowlist checks if the value is within the allowlist
func (a allowlist) IsValid(value string) bool {
return a.rule.IsValid(value)
}
// denylist is a generic rule for denylisting
type denylist struct {
rule
}
// IsValid for allowlist checks if the value is within the allowlist
func (d denylist) IsValid(value string) bool {
return !d.rule.IsValid(value)
}
type patterns []string
// hasPrefixFold tests whether the string s begins with prefix, interpreted as UTF-8 strings,
// under Unicode case-folding.
func hasPrefixFold(s, prefix string) bool {
return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix)
}
// IsValid for patterns checks each pattern and returns if a match has
// been found
func (p patterns) IsValid(value string) bool {
for _, pattern := range p {
if hasPrefixFold(value, pattern) {
return true
}
}
return false
}
// inclusiveRules rules allow for rules to depend on one another
type inclusiveRules []rule
// IsValid will return true if all rules are true
func (r inclusiveRules) IsValid(value string) bool {
for _, rule := range r {
if !rule.IsValid(value) {
return false
}
}
return true
}
@@ -0,0 +1,472 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Based on github.com/aws/aws-sdk-go by Amazon.com, Inc. with code from:
// - github.com/aws/aws-sdk-go/blob/v1.34.28/aws/request/request.go
// - github.com/aws/aws-sdk-go/blob/v1.34.28/aws/signer/v4/v4.go
// - github.com/aws/aws-sdk-go/blob/v1.34.28/aws/signer/v4/uri_path.go
// - github.com/aws/aws-sdk-go/blob/v1.34.28/aws/types.go
// See THIRD-PARTY-NOTICES for original license terms
package awsv4
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
const (
authorizationHeader = "Authorization"
authHeaderSignatureElem = "Signature="
authHeaderPrefix = "AWS4-HMAC-SHA256"
timeFormat = "20060102T150405Z"
shortTimeFormat = "20060102"
awsV4Request = "aws4_request"
// emptyStringSHA256 is a SHA256 of an empty string
emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
)
var ignoredHeaders = rules{
denylist{
mapRule{
authorizationHeader: struct{}{},
"User-Agent": struct{}{},
"X-Amzn-Trace-Id": struct{}{},
},
},
}
// Signer applies AWS v4 signing to given request. Use this to sign requests
// that need to be signed with AWS V4 Signatures.
type Signer struct {
Credentials *StaticProvider
}
// NewSigner returns a Signer pointer configured with the credentials and optional
// option values provided. If not options are provided the Signer will use its
// default configuration.
func NewSigner(credentials *StaticProvider) *Signer {
v4 := &Signer{
Credentials: credentials,
}
return v4
}
type signingCtx struct {
ServiceName string
Region string
Request *http.Request
Body io.ReadSeeker
Query url.Values
Time time.Time
SignedHeaderVals http.Header
credValues Value
bodyDigest string
signedHeaders string
canonicalHeaders string
canonicalString string
credentialString string
stringToSign string
signature string
authorization string
}
// Sign signs AWS v4 requests with the provided body, service name, region the
// request is made to, and time the request is signed at. The signTime allows
// you to specify that a request is signed for the future, and cannot be
// used until then.
//
// Returns a list of HTTP headers that were included in the signature or an
// error if signing the request failed. Generally for signed requests this value
// is not needed as the full request context will be captured by the http.Request
// value. It is included for reference though.
//
// Sign will set the request's Body to be the `body` parameter passed in. If
// the body is not already an io.ReadCloser, it will be wrapped within one. If
// a `nil` body parameter passed to Sign, the request's Body field will be
// also set to nil. Its important to note that this functionality will not
// change the request's ContentLength of the request.
//
// Sign differs from Presign in that it will sign the request using HTTP
// header values. This type of signing is intended for http.Request values that
// will not be shared, or are shared in a way the header values on the request
// will not be lost.
//
// The requests body is an io.ReadSeeker so the SHA256 of the body can be
// generated. To bypass the signer computing the hash you can set the
// "X-Amz-Content-Sha256" header with a precomputed value. The signer will
// only compute the hash if the request header value is empty.
func (v4 Signer) Sign(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) {
return v4.signWithBody(r, body, service, region, signTime)
}
func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) {
ctx := &signingCtx{
Request: r,
Body: body,
Query: r.URL.Query(),
Time: signTime,
ServiceName: service,
Region: region,
}
for key := range ctx.Query {
sort.Strings(ctx.Query[key])
}
if ctx.isRequestSigned() {
ctx.Time = time.Now()
}
var err error
ctx.credValues, err = v4.Credentials.Retrieve()
if err != nil {
return http.Header{}, err
}
ctx.sanitizeHostForHeader()
ctx.assignAmzQueryValues()
if err := ctx.build(); err != nil {
return nil, err
}
var reader io.ReadCloser
if body != nil {
var ok bool
if reader, ok = body.(io.ReadCloser); !ok {
reader = ioutil.NopCloser(body)
}
}
r.Body = reader
return ctx.SignedHeaderVals, nil
}
// sanitizeHostForHeader removes default port from host and updates request.Host
func (ctx *signingCtx) sanitizeHostForHeader() {
r := ctx.Request
host := getHost(r)
port := portOnly(host)
if port != "" && isDefaultPort(r.URL.Scheme, port) {
r.Host = stripPort(host)
}
}
func (ctx *signingCtx) assignAmzQueryValues() {
if ctx.credValues.SessionToken != "" {
ctx.Request.Header.Set("X-Amz-Security-Token", ctx.credValues.SessionToken)
}
}
func (ctx *signingCtx) build() error {
ctx.buildTime() // no depends
ctx.buildCredentialString() // no depends
if err := ctx.buildBodyDigest(); err != nil {
return err
}
unsignedHeaders := ctx.Request.Header
ctx.buildCanonicalHeaders(ignoredHeaders, unsignedHeaders)
ctx.buildCanonicalString() // depends on canon headers / signed headers
ctx.buildStringToSign() // depends on canon string
ctx.buildSignature() // depends on string to sign
parts := []string{
authHeaderPrefix + " Credential=" + ctx.credValues.AccessKeyID + "/" + ctx.credentialString,
"SignedHeaders=" + ctx.signedHeaders,
authHeaderSignatureElem + ctx.signature,
}
ctx.Request.Header.Set(authorizationHeader, strings.Join(parts, ", "))
return nil
}
// GetSignedRequestSignature attempts to extract the signature of the request.
// Returning an error if the request is unsigned, or unable to extract the
// signature.
func GetSignedRequestSignature(r *http.Request) ([]byte, error) {
if auth := r.Header.Get(authorizationHeader); len(auth) != 0 {
ps := strings.Split(auth, ", ")
for _, p := range ps {
if idx := strings.Index(p, authHeaderSignatureElem); idx >= 0 {
sig := p[len(authHeaderSignatureElem):]
if len(sig) == 0 {
return nil, fmt.Errorf("invalid request signature authorization header")
}
return hex.DecodeString(sig)
}
}
}
if sig := r.URL.Query().Get("X-Amz-Signature"); len(sig) != 0 {
return hex.DecodeString(sig)
}
return nil, fmt.Errorf("request not signed")
}
func (ctx *signingCtx) buildTime() {
ctx.Request.Header.Set("X-Amz-Date", formatTime(ctx.Time))
}
func (ctx *signingCtx) buildCredentialString() {
ctx.credentialString = buildSigningScope(ctx.Region, ctx.ServiceName, ctx.Time)
}
func (ctx *signingCtx) buildCanonicalHeaders(r rule, header http.Header) {
headers := make([]string, 0, len(header))
headers = append(headers, "host")
for k, v := range header {
if !r.IsValid(k) {
continue // ignored header
}
if ctx.SignedHeaderVals == nil {
ctx.SignedHeaderVals = make(http.Header)
}
lowerCaseKey := strings.ToLower(k)
if _, ok := ctx.SignedHeaderVals[lowerCaseKey]; ok {
// include additional values
ctx.SignedHeaderVals[lowerCaseKey] = append(ctx.SignedHeaderVals[lowerCaseKey], v...)
continue
}
headers = append(headers, lowerCaseKey)
ctx.SignedHeaderVals[lowerCaseKey] = v
}
sort.Strings(headers)
ctx.signedHeaders = strings.Join(headers, ";")
headerValues := make([]string, len(headers))
for i, k := range headers {
if k == "host" {
if ctx.Request.Host != "" {
headerValues[i] = "host:" + ctx.Request.Host
} else {
headerValues[i] = "host:" + ctx.Request.URL.Host
}
} else {
headerValues[i] = k + ":" +
strings.Join(ctx.SignedHeaderVals[k], ",")
}
}
stripExcessSpaces(headerValues)
ctx.canonicalHeaders = strings.Join(headerValues, "\n")
}
func getURIPath(u *url.URL) string {
var uri string
if len(u.Opaque) > 0 {
uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/")
} else {
uri = u.EscapedPath()
}
if len(uri) == 0 {
uri = "/"
}
return uri
}
func (ctx *signingCtx) buildCanonicalString() {
ctx.Request.URL.RawQuery = strings.Replace(ctx.Query.Encode(), "+", "%20", -1)
uri := getURIPath(ctx.Request.URL)
uri = EscapePath(uri, false)
ctx.canonicalString = strings.Join([]string{
ctx.Request.Method,
uri,
ctx.Request.URL.RawQuery,
ctx.canonicalHeaders + "\n",
ctx.signedHeaders,
ctx.bodyDigest,
}, "\n")
}
func (ctx *signingCtx) buildStringToSign() {
ctx.stringToSign = strings.Join([]string{
authHeaderPrefix,
formatTime(ctx.Time),
ctx.credentialString,
hex.EncodeToString(hashSHA256([]byte(ctx.canonicalString))),
}, "\n")
}
func (ctx *signingCtx) buildSignature() {
creds := deriveSigningKey(ctx.Region, ctx.ServiceName, ctx.credValues.SecretAccessKey, ctx.Time)
signature := hmacSHA256(creds, []byte(ctx.stringToSign))
ctx.signature = hex.EncodeToString(signature)
}
func (ctx *signingCtx) buildBodyDigest() error {
hash := ctx.Request.Header.Get("X-Amz-Content-Sha256")
if hash == "" {
if ctx.Body == nil {
hash = emptyStringSHA256
} else {
hashBytes, err := makeSha256Reader(ctx.Body)
if err != nil {
return err
}
hash = hex.EncodeToString(hashBytes)
}
}
ctx.bodyDigest = hash
return nil
}
// isRequestSigned returns if the request is currently signed or presigned
func (ctx *signingCtx) isRequestSigned() bool {
return ctx.Request.Header.Get("Authorization") != ""
}
func hmacSHA256(key []byte, data []byte) []byte {
hash := hmac.New(sha256.New, key)
hash.Write(data)
return hash.Sum(nil)
}
func hashSHA256(data []byte) []byte {
hash := sha256.New()
hash.Write(data)
return hash.Sum(nil)
}
// seekerLen attempts to get the number of bytes remaining at the seeker's
// current position. Returns the number of bytes remaining or error.
func seekerLen(s io.Seeker) (int64, error) {
curOffset, err := s.Seek(0, io.SeekCurrent)
if err != nil {
return 0, err
}
endOffset, err := s.Seek(0, io.SeekEnd)
if err != nil {
return 0, err
}
_, err = s.Seek(curOffset, io.SeekStart)
if err != nil {
return 0, err
}
return endOffset - curOffset, nil
}
func makeSha256Reader(reader io.ReadSeeker) (hashBytes []byte, err error) {
hash := sha256.New()
start, err := reader.Seek(0, io.SeekCurrent)
if err != nil {
return nil, err
}
defer func() {
// ensure error is return if unable to seek back to start of payload.
_, err = reader.Seek(start, io.SeekStart)
}()
// Use CopyN to avoid allocating the 32KB buffer in io.Copy for bodies
// smaller than 32KB. Fall back to io.Copy if we fail to determine the size.
size, err := seekerLen(reader)
if err != nil {
_, _ = io.Copy(hash, reader)
} else {
_, _ = io.CopyN(hash, reader, size)
}
return hash.Sum(nil), nil
}
const doubleSpace = " "
// stripExcessSpaces will rewrite the passed in slice's string values to not
// contain multiple side-by-side spaces.
func stripExcessSpaces(vals []string) {
var j, k, l, m, spaces int
for i, str := range vals {
// Trim trailing spaces
for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- {
}
// Trim leading spaces
for k = 0; k < j && str[k] == ' '; k++ {
}
str = str[k : j+1]
// Strip multiple spaces.
j = strings.Index(str, doubleSpace)
if j < 0 {
vals[i] = str
continue
}
buf := []byte(str)
for k, m, l = j, j, len(buf); k < l; k++ {
if buf[k] == ' ' {
if spaces == 0 {
// First space.
buf[m] = buf[k]
m++
}
spaces++
} else {
// End of multiple spaces.
spaces = 0
buf[m] = buf[k]
m++
}
}
vals[i] = string(buf[:m])
}
}
func buildSigningScope(region, service string, dt time.Time) string {
return strings.Join([]string{
formatShortTime(dt),
region,
service,
awsV4Request,
}, "/")
}
func deriveSigningKey(region, service, secretKey string, dt time.Time) []byte {
keyDate := hmacSHA256([]byte("AWS4"+secretKey), []byte(formatShortTime(dt)))
keyRegion := hmacSHA256(keyDate, []byte(region))
keyService := hmacSHA256(keyRegion, []byte(service))
signingKey := hmacSHA256(keyService, []byte(awsV4Request))
return signingKey
}
func formatShortTime(dt time.Time) string {
return dt.UTC().Format(shortTimeFormat)
}
func formatTime(dt time.Time) string {
return dt.UTC().Format(timeFormat)
}
@@ -0,0 +1,167 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build gssapi && (linux || darwin)
// +build gssapi
// +build linux darwin
package gssapi
/*
#cgo linux CFLAGS: -DGOOS_linux
#cgo linux LDFLAGS: -lgssapi_krb5 -lkrb5
#cgo darwin CFLAGS: -DGOOS_darwin
#cgo darwin LDFLAGS: -framework GSS
#include "gss_wrapper.h"
*/
import "C"
import (
"fmt"
"runtime"
"strings"
"unsafe"
)
// New creates a new SaslClient. The target parameter should be a hostname with no port.
func New(target, username, password string, passwordSet bool, props map[string]string) (*SaslClient, error) {
serviceName := "mongodb"
for key, value := range props {
switch strings.ToUpper(key) {
case "CANONICALIZE_HOST_NAME":
return nil, fmt.Errorf("CANONICALIZE_HOST_NAME is not supported when using gssapi on %s", runtime.GOOS)
case "SERVICE_REALM":
return nil, fmt.Errorf("SERVICE_REALM is not supported when using gssapi on %s", runtime.GOOS)
case "SERVICE_NAME":
serviceName = value
case "SERVICE_HOST":
target = value
default:
return nil, fmt.Errorf("unknown mechanism property %s", key)
}
}
servicePrincipalName := fmt.Sprintf("%s@%s", serviceName, target)
return &SaslClient{
servicePrincipalName: servicePrincipalName,
username: username,
password: password,
passwordSet: passwordSet,
}, nil
}
type SaslClient struct {
servicePrincipalName string
username string
password string
passwordSet bool
// state
state C.gssapi_client_state
contextComplete bool
done bool
}
func (sc *SaslClient) Close() {
C.gssapi_client_destroy(&sc.state)
}
func (sc *SaslClient) Start() (string, []byte, error) {
const mechName = "GSSAPI"
cservicePrincipalName := C.CString(sc.servicePrincipalName)
defer C.free(unsafe.Pointer(cservicePrincipalName))
var cusername *C.char
var cpassword *C.char
if sc.username != "" {
cusername = C.CString(sc.username)
defer C.free(unsafe.Pointer(cusername))
if sc.passwordSet {
cpassword = C.CString(sc.password)
defer C.free(unsafe.Pointer(cpassword))
}
}
status := C.gssapi_client_init(&sc.state, cservicePrincipalName, cusername, cpassword)
if status != C.GSSAPI_OK {
return mechName, nil, sc.getError("unable to initialize client")
}
payload, err := sc.Next(nil)
return mechName, payload, err
}
func (sc *SaslClient) Next(challenge []byte) ([]byte, error) {
var buf unsafe.Pointer
var bufLen C.size_t
var outBuf unsafe.Pointer
var outBufLen C.size_t
if sc.contextComplete {
if sc.username == "" {
var cusername *C.char
status := C.gssapi_client_username(&sc.state, &cusername)
if status != C.GSSAPI_OK {
return nil, sc.getError("unable to acquire username")
}
defer C.free(unsafe.Pointer(cusername))
sc.username = C.GoString((*C.char)(unsafe.Pointer(cusername)))
}
bytes := append([]byte{1, 0, 0, 0}, []byte(sc.username)...)
buf = unsafe.Pointer(&bytes[0])
bufLen = C.size_t(len(bytes))
status := C.gssapi_client_wrap_msg(&sc.state, buf, bufLen, &outBuf, &outBufLen)
if status != C.GSSAPI_OK {
return nil, sc.getError("unable to wrap authz")
}
sc.done = true
} else {
if len(challenge) > 0 {
buf = unsafe.Pointer(&challenge[0])
bufLen = C.size_t(len(challenge))
}
status := C.gssapi_client_negotiate(&sc.state, buf, bufLen, &outBuf, &outBufLen)
switch status {
case C.GSSAPI_OK:
sc.contextComplete = true
case C.GSSAPI_CONTINUE:
default:
return nil, sc.getError("unable to negotiate with server")
}
}
if outBuf != nil {
defer C.free(outBuf)
}
return C.GoBytes(outBuf, C.int(outBufLen)), nil
}
func (sc *SaslClient) Completed() bool {
return sc.done
}
func (sc *SaslClient) getError(prefix string) error {
var desc *C.char
status := C.gssapi_error_desc(sc.state.maj_stat, sc.state.min_stat, &desc)
if status != C.GSSAPI_OK {
if desc != nil {
C.free(unsafe.Pointer(desc))
}
return fmt.Errorf("%s: (%v, %v)", prefix, sc.state.maj_stat, sc.state.min_stat)
}
defer C.free(unsafe.Pointer(desc))
return fmt.Errorf("%s: %v(%v,%v)", prefix, C.GoString(desc), int32(sc.state.maj_stat), int32(sc.state.min_stat))
}
@@ -0,0 +1,254 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//+build gssapi
//+build linux darwin
#include <string.h>
#include <stdio.h>
#include "gss_wrapper.h"
OM_uint32 gssapi_canonicalize_name(
OM_uint32* minor_status,
char *input_name,
gss_OID input_name_type,
gss_name_t *output_name
)
{
OM_uint32 major_status;
gss_name_t imported_name = GSS_C_NO_NAME;
gss_buffer_desc buffer = GSS_C_EMPTY_BUFFER;
buffer.value = input_name;
buffer.length = strlen(input_name);
major_status = gss_import_name(minor_status, &buffer, input_name_type, &imported_name);
if (GSS_ERROR(major_status)) {
return major_status;
}
major_status = gss_canonicalize_name(minor_status, imported_name, (gss_OID)gss_mech_krb5, output_name);
if (imported_name != GSS_C_NO_NAME) {
OM_uint32 ignored;
gss_release_name(&ignored, &imported_name);
}
return major_status;
}
int gssapi_error_desc(
OM_uint32 maj_stat,
OM_uint32 min_stat,
char **desc
)
{
OM_uint32 stat = maj_stat;
int stat_type = GSS_C_GSS_CODE;
if (min_stat != 0) {
stat = min_stat;
stat_type = GSS_C_MECH_CODE;
}
OM_uint32 local_maj_stat, local_min_stat;
OM_uint32 msg_ctx = 0;
gss_buffer_desc desc_buffer;
do
{
local_maj_stat = gss_display_status(
&local_min_stat,
stat,
stat_type,
GSS_C_NO_OID,
&msg_ctx,
&desc_buffer
);
if (GSS_ERROR(local_maj_stat)) {
return GSSAPI_ERROR;
}
if (*desc) {
free(*desc);
}
*desc = malloc(desc_buffer.length+1);
memcpy(*desc, desc_buffer.value, desc_buffer.length+1);
gss_release_buffer(&local_min_stat, &desc_buffer);
}
while(msg_ctx != 0);
return GSSAPI_OK;
}
int gssapi_client_init(
gssapi_client_state *client,
char* spn,
char* username,
char* password
)
{
client->cred = GSS_C_NO_CREDENTIAL;
client->ctx = GSS_C_NO_CONTEXT;
client->maj_stat = gssapi_canonicalize_name(&client->min_stat, spn, GSS_C_NT_HOSTBASED_SERVICE, &client->spn);
if (GSS_ERROR(client->maj_stat)) {
return GSSAPI_ERROR;
}
if (username) {
gss_name_t name;
client->maj_stat = gssapi_canonicalize_name(&client->min_stat, username, GSS_C_NT_USER_NAME, &name);
if (GSS_ERROR(client->maj_stat)) {
return GSSAPI_ERROR;
}
if (password) {
gss_buffer_desc password_buffer;
password_buffer.value = password;
password_buffer.length = strlen(password);
client->maj_stat = gss_acquire_cred_with_password(&client->min_stat, name, &password_buffer, GSS_C_INDEFINITE, GSS_C_NO_OID_SET, GSS_C_INITIATE, &client->cred, NULL, NULL);
} else {
client->maj_stat = gss_acquire_cred(&client->min_stat, name, GSS_C_INDEFINITE, GSS_C_NO_OID_SET, GSS_C_INITIATE, &client->cred, NULL, NULL);
}
if (GSS_ERROR(client->maj_stat)) {
return GSSAPI_ERROR;
}
OM_uint32 ignored;
gss_release_name(&ignored, &name);
}
return GSSAPI_OK;
}
int gssapi_client_username(
gssapi_client_state *client,
char** username
)
{
OM_uint32 ignored;
gss_name_t name = GSS_C_NO_NAME;
client->maj_stat = gss_inquire_context(&client->min_stat, client->ctx, &name, NULL, NULL, NULL, NULL, NULL, NULL);
if (GSS_ERROR(client->maj_stat)) {
return GSSAPI_ERROR;
}
gss_buffer_desc name_buffer;
client->maj_stat = gss_display_name(&client->min_stat, name, &name_buffer, NULL);
if (GSS_ERROR(client->maj_stat)) {
gss_release_name(&ignored, &name);
return GSSAPI_ERROR;
}
*username = malloc(name_buffer.length+1);
memcpy(*username, name_buffer.value, name_buffer.length+1);
gss_release_buffer(&ignored, &name_buffer);
gss_release_name(&ignored, &name);
return GSSAPI_OK;
}
int gssapi_client_negotiate(
gssapi_client_state *client,
void* input,
size_t input_length,
void** output,
size_t* output_length
)
{
gss_buffer_desc input_buffer = GSS_C_EMPTY_BUFFER;
gss_buffer_desc output_buffer = GSS_C_EMPTY_BUFFER;
if (input) {
input_buffer.value = input;
input_buffer.length = input_length;
}
client->maj_stat = gss_init_sec_context(
&client->min_stat,
client->cred,
&client->ctx,
client->spn,
GSS_C_NO_OID,
GSS_C_MUTUAL_FLAG | GSS_C_SEQUENCE_FLAG,
0,
GSS_C_NO_CHANNEL_BINDINGS,
&input_buffer,
NULL,
&output_buffer,
NULL,
NULL
);
if (output_buffer.length) {
*output = malloc(output_buffer.length);
*output_length = output_buffer.length;
memcpy(*output, output_buffer.value, output_buffer.length);
OM_uint32 ignored;
gss_release_buffer(&ignored, &output_buffer);
}
if (GSS_ERROR(client->maj_stat)) {
return GSSAPI_ERROR;
} else if (client->maj_stat == GSS_S_CONTINUE_NEEDED) {
return GSSAPI_CONTINUE;
}
return GSSAPI_OK;
}
int gssapi_client_wrap_msg(
gssapi_client_state *client,
void* input,
size_t input_length,
void** output,
size_t* output_length
)
{
gss_buffer_desc input_buffer = GSS_C_EMPTY_BUFFER;
gss_buffer_desc output_buffer = GSS_C_EMPTY_BUFFER;
input_buffer.value = input;
input_buffer.length = input_length;
client->maj_stat = gss_wrap(&client->min_stat, client->ctx, 0, GSS_C_QOP_DEFAULT, &input_buffer, NULL, &output_buffer);
if (output_buffer.length) {
*output = malloc(output_buffer.length);
*output_length = output_buffer.length;
memcpy(*output, output_buffer.value, output_buffer.length);
gss_release_buffer(&client->min_stat, &output_buffer);
}
if (GSS_ERROR(client->maj_stat)) {
return GSSAPI_ERROR;
}
return GSSAPI_OK;
}
int gssapi_client_destroy(
gssapi_client_state *client
)
{
OM_uint32 ignored;
if (client->ctx != GSS_C_NO_CONTEXT) {
gss_delete_sec_context(&ignored, &client->ctx, GSS_C_NO_BUFFER);
}
if (client->spn != GSS_C_NO_NAME) {
gss_release_name(&ignored, &client->spn);
}
if (client->cred != GSS_C_NO_CREDENTIAL) {
gss_release_cred(&ignored, &client->cred);
}
return GSSAPI_OK;
}
@@ -0,0 +1,72 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//+build gssapi
//+build linux darwin
#ifndef GSS_WRAPPER_H
#define GSS_WRAPPER_H
#include <stdlib.h>
#ifdef GOOS_linux
#include <gssapi/gssapi.h>
#include <gssapi/gssapi_krb5.h>
#endif
#ifdef GOOS_darwin
#include <GSS/GSS.h>
#endif
#define GSSAPI_OK 0
#define GSSAPI_CONTINUE 1
#define GSSAPI_ERROR 2
typedef struct {
gss_name_t spn;
gss_cred_id_t cred;
gss_ctx_id_t ctx;
OM_uint32 maj_stat;
OM_uint32 min_stat;
} gssapi_client_state;
int gssapi_error_desc(
OM_uint32 maj_stat,
OM_uint32 min_stat,
char **desc
);
int gssapi_client_init(
gssapi_client_state *client,
char* spn,
char* username,
char* password
);
int gssapi_client_username(
gssapi_client_state *client,
char** username
);
int gssapi_client_negotiate(
gssapi_client_state *client,
void* input,
size_t input_length,
void** output,
size_t* output_length
);
int gssapi_client_wrap_msg(
gssapi_client_state *client,
void* input,
size_t input_length,
void** output,
size_t* output_length
);
int gssapi_client_destroy(
gssapi_client_state *client
);
#endif
@@ -0,0 +1,353 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build gssapi && windows
// +build gssapi,windows
package gssapi
// #include "sspi_wrapper.h"
import "C"
import (
"fmt"
"net"
"strconv"
"strings"
"sync"
"unsafe"
)
// New creates a new SaslClient. The target parameter should be a hostname with no port.
func New(target, username, password string, passwordSet bool, props map[string]string) (*SaslClient, error) {
initOnce.Do(initSSPI)
if initError != nil {
return nil, initError
}
var err error
serviceName := "mongodb"
serviceRealm := ""
canonicalizeHostName := false
var serviceHostSet bool
for key, value := range props {
switch strings.ToUpper(key) {
case "CANONICALIZE_HOST_NAME":
canonicalizeHostName, err = strconv.ParseBool(value)
if err != nil {
return nil, fmt.Errorf("%s must be a boolean (true, false, 0, 1) but got '%s'", key, value)
}
case "SERVICE_REALM":
serviceRealm = value
case "SERVICE_NAME":
serviceName = value
case "SERVICE_HOST":
serviceHostSet = true
target = value
}
}
if canonicalizeHostName {
// Should not canonicalize the SERVICE_HOST
if serviceHostSet {
return nil, fmt.Errorf("CANONICALIZE_HOST_NAME and SERVICE_HOST canonot both be specified")
}
names, err := net.LookupAddr(target)
if err != nil || len(names) == 0 {
return nil, fmt.Errorf("unable to canonicalize hostname: %s", err)
}
target = names[0]
if target[len(target)-1] == '.' {
target = target[:len(target)-1]
}
}
servicePrincipalName := fmt.Sprintf("%s/%s", serviceName, target)
if serviceRealm != "" {
servicePrincipalName += "@" + serviceRealm
}
return &SaslClient{
servicePrincipalName: servicePrincipalName,
username: username,
password: password,
passwordSet: passwordSet,
}, nil
}
type SaslClient struct {
servicePrincipalName string
username string
password string
passwordSet bool
// state
state C.sspi_client_state
contextComplete bool
done bool
}
func (sc *SaslClient) Close() {
C.sspi_client_destroy(&sc.state)
}
func (sc *SaslClient) Start() (string, []byte, error) {
const mechName = "GSSAPI"
var cusername *C.char
var cpassword *C.char
if sc.username != "" {
cusername = C.CString(sc.username)
defer C.free(unsafe.Pointer(cusername))
if sc.passwordSet {
cpassword = C.CString(sc.password)
defer C.free(unsafe.Pointer(cpassword))
}
}
status := C.sspi_client_init(&sc.state, cusername, cpassword)
if status != C.SSPI_OK {
return mechName, nil, sc.getError("unable to intitialize client")
}
payload, err := sc.Next(nil)
return mechName, payload, err
}
func (sc *SaslClient) Next(challenge []byte) ([]byte, error) {
var outBuf C.PVOID
var outBufLen C.ULONG
if sc.contextComplete {
if sc.username == "" {
var cusername *C.char
status := C.sspi_client_username(&sc.state, &cusername)
if status != C.SSPI_OK {
return nil, sc.getError("unable to acquire username")
}
defer C.free(unsafe.Pointer(cusername))
sc.username = C.GoString((*C.char)(unsafe.Pointer(cusername)))
}
bytes := append([]byte{1, 0, 0, 0}, []byte(sc.username)...)
buf := (C.PVOID)(unsafe.Pointer(&bytes[0]))
bufLen := C.ULONG(len(bytes))
status := C.sspi_client_wrap_msg(&sc.state, buf, bufLen, &outBuf, &outBufLen)
if status != C.SSPI_OK {
return nil, sc.getError("unable to wrap authz")
}
sc.done = true
} else {
var buf C.PVOID
var bufLen C.ULONG
if len(challenge) > 0 {
buf = (C.PVOID)(unsafe.Pointer(&challenge[0]))
bufLen = C.ULONG(len(challenge))
}
cservicePrincipalName := C.CString(sc.servicePrincipalName)
defer C.free(unsafe.Pointer(cservicePrincipalName))
status := C.sspi_client_negotiate(&sc.state, cservicePrincipalName, buf, bufLen, &outBuf, &outBufLen)
switch status {
case C.SSPI_OK:
sc.contextComplete = true
case C.SSPI_CONTINUE:
default:
return nil, sc.getError("unable to negotiate with server")
}
}
if outBuf != C.PVOID(nil) {
defer C.free(unsafe.Pointer(outBuf))
}
return C.GoBytes(unsafe.Pointer(outBuf), C.int(outBufLen)), nil
}
func (sc *SaslClient) Completed() bool {
return sc.done
}
func (sc *SaslClient) getError(prefix string) error {
return getError(prefix, sc.state.status)
}
var initOnce sync.Once
var initError error
func initSSPI() {
rc := C.sspi_init()
if rc != 0 {
initError = fmt.Errorf("error initializing sspi: %v", rc)
}
}
func getError(prefix string, status C.SECURITY_STATUS) error {
var s string
switch status {
case C.SEC_E_ALGORITHM_MISMATCH:
s = "The client and server cannot communicate because they do not possess a common algorithm."
case C.SEC_E_BAD_BINDINGS:
s = "The SSPI channel bindings supplied by the client are incorrect."
case C.SEC_E_BAD_PKGID:
s = "The requested package identifier does not exist."
case C.SEC_E_BUFFER_TOO_SMALL:
s = "The buffers supplied to the function are not large enough to contain the information."
case C.SEC_E_CANNOT_INSTALL:
s = "The security package cannot initialize successfully and should not be installed."
case C.SEC_E_CANNOT_PACK:
s = "The package is unable to pack the context."
case C.SEC_E_CERT_EXPIRED:
s = "The received certificate has expired."
case C.SEC_E_CERT_UNKNOWN:
s = "An unknown error occurred while processing the certificate."
case C.SEC_E_CERT_WRONG_USAGE:
s = "The certificate is not valid for the requested usage."
case C.SEC_E_CONTEXT_EXPIRED:
s = "The application is referencing a context that has already been closed. A properly written application should not receive this error."
case C.SEC_E_CROSSREALM_DELEGATION_FAILURE:
s = "The server attempted to make a Kerberos-constrained delegation request for a target outside the server's realm."
case C.SEC_E_CRYPTO_SYSTEM_INVALID:
s = "The cryptographic system or checksum function is not valid because a required function is unavailable."
case C.SEC_E_DECRYPT_FAILURE:
s = "The specified data could not be decrypted."
case C.SEC_E_DELEGATION_REQUIRED:
s = "The requested operation cannot be completed. The computer must be trusted for delegation"
case C.SEC_E_DOWNGRADE_DETECTED:
s = "The system detected a possible attempt to compromise security. Verify that the server that authenticated you can be contacted."
case C.SEC_E_ENCRYPT_FAILURE:
s = "The specified data could not be encrypted."
case C.SEC_E_ILLEGAL_MESSAGE:
s = "The message received was unexpected or badly formatted."
case C.SEC_E_INCOMPLETE_CREDENTIALS:
s = "The credentials supplied were not complete and could not be verified. The context could not be initialized."
case C.SEC_E_INCOMPLETE_MESSAGE:
s = "The message supplied was incomplete. The signature was not verified."
case C.SEC_E_INSUFFICIENT_MEMORY:
s = "Not enough memory is available to complete the request."
case C.SEC_E_INTERNAL_ERROR:
s = "An error occurred that did not map to an SSPI error code."
case C.SEC_E_INVALID_HANDLE:
s = "The handle passed to the function is not valid."
case C.SEC_E_INVALID_TOKEN:
s = "The token passed to the function is not valid."
case C.SEC_E_ISSUING_CA_UNTRUSTED:
s = "An untrusted certification authority (CA) was detected while processing the smart card certificate used for authentication."
case C.SEC_E_ISSUING_CA_UNTRUSTED_KDC:
s = "An untrusted CA was detected while processing the domain controller certificate used for authentication. The system event log contains additional information."
case C.SEC_E_KDC_CERT_EXPIRED:
s = "The domain controller certificate used for smart card logon has expired."
case C.SEC_E_KDC_CERT_REVOKED:
s = "The domain controller certificate used for smart card logon has been revoked."
case C.SEC_E_KDC_INVALID_REQUEST:
s = "A request that is not valid was sent to the KDC."
case C.SEC_E_KDC_UNABLE_TO_REFER:
s = "The KDC was unable to generate a referral for the service requested."
case C.SEC_E_KDC_UNKNOWN_ETYPE:
s = "The requested encryption type is not supported by the KDC."
case C.SEC_E_LOGON_DENIED:
s = "The logon has been denied"
case C.SEC_E_MAX_REFERRALS_EXCEEDED:
s = "The number of maximum ticket referrals has been exceeded."
case C.SEC_E_MESSAGE_ALTERED:
s = "The message supplied for verification has been altered."
case C.SEC_E_MULTIPLE_ACCOUNTS:
s = "The received certificate was mapped to multiple accounts."
case C.SEC_E_MUST_BE_KDC:
s = "The local computer must be a Kerberos domain controller (KDC)"
case C.SEC_E_NO_AUTHENTICATING_AUTHORITY:
s = "No authority could be contacted for authentication."
case C.SEC_E_NO_CREDENTIALS:
s = "No credentials are available."
case C.SEC_E_NO_IMPERSONATION:
s = "No impersonation is allowed for this context."
case C.SEC_E_NO_IP_ADDRESSES:
s = "Unable to accomplish the requested task because the local computer does not have any IP addresses."
case C.SEC_E_NO_KERB_KEY:
s = "No Kerberos key was found."
case C.SEC_E_NO_PA_DATA:
s = "Policy administrator (PA) data is needed to determine the encryption type"
case C.SEC_E_NO_S4U_PROT_SUPPORT:
s = "The Kerberos subsystem encountered an error. A service for user protocol request was made against a domain controller which does not support service for a user."
case C.SEC_E_NO_TGT_REPLY:
s = "The client is trying to negotiate a context and the server requires a user-to-user connection"
case C.SEC_E_NOT_OWNER:
s = "The caller of the function does not own the credentials."
case C.SEC_E_OK:
s = "The operation completed successfully."
case C.SEC_E_OUT_OF_SEQUENCE:
s = "The message supplied for verification is out of sequence."
case C.SEC_E_PKINIT_CLIENT_FAILURE:
s = "The smart card certificate used for authentication is not trusted."
case C.SEC_E_PKINIT_NAME_MISMATCH:
s = "The client certificate does not contain a valid UPN or does not match the client name in the logon request."
case C.SEC_E_QOP_NOT_SUPPORTED:
s = "The quality of protection attribute is not supported by this package."
case C.SEC_E_REVOCATION_OFFLINE_C:
s = "The revocation status of the smart card certificate used for authentication could not be determined."
case C.SEC_E_REVOCATION_OFFLINE_KDC:
s = "The revocation status of the domain controller certificate used for smart card authentication could not be determined. The system event log contains additional information."
case C.SEC_E_SECPKG_NOT_FOUND:
s = "The security package was not recognized."
case C.SEC_E_SECURITY_QOS_FAILED:
s = "The security context could not be established due to a failure in the requested quality of service (for example"
case C.SEC_E_SHUTDOWN_IN_PROGRESS:
s = "A system shutdown is in progress."
case C.SEC_E_SMARTCARD_CERT_EXPIRED:
s = "The smart card certificate used for authentication has expired."
case C.SEC_E_SMARTCARD_CERT_REVOKED:
s = "The smart card certificate used for authentication has been revoked. Additional information may exist in the event log."
case C.SEC_E_SMARTCARD_LOGON_REQUIRED:
s = "Smart card logon is required and was not used."
case C.SEC_E_STRONG_CRYPTO_NOT_SUPPORTED:
s = "The other end of the security negotiation requires strong cryptography"
case C.SEC_E_TARGET_UNKNOWN:
s = "The target was not recognized."
case C.SEC_E_TIME_SKEW:
s = "The clocks on the client and server computers do not match."
case C.SEC_E_TOO_MANY_PRINCIPALS:
s = "The KDC reply contained more than one principal name."
case C.SEC_E_UNFINISHED_CONTEXT_DELETED:
s = "A security context was deleted before the context was completed. This is considered a logon failure."
case C.SEC_E_UNKNOWN_CREDENTIALS:
s = "The credentials provided were not recognized."
case C.SEC_E_UNSUPPORTED_FUNCTION:
s = "The requested function is not supported."
case C.SEC_E_UNSUPPORTED_PREAUTH:
s = "An unsupported preauthentication mechanism was presented to the Kerberos package."
case C.SEC_E_UNTRUSTED_ROOT:
s = "The certificate chain was issued by an authority that is not trusted."
case C.SEC_E_WRONG_CREDENTIAL_HANDLE:
s = "The supplied credential handle does not match the credential associated with the security context."
case C.SEC_E_WRONG_PRINCIPAL:
s = "The target principal name is incorrect."
case C.SEC_I_COMPLETE_AND_CONTINUE:
s = "The function completed successfully"
case C.SEC_I_COMPLETE_NEEDED:
s = "The function completed successfully"
case C.SEC_I_CONTEXT_EXPIRED:
s = "The message sender has finished using the connection and has initiated a shutdown. For information about initiating or recognizing a shutdown"
case C.SEC_I_CONTINUE_NEEDED:
s = "The function completed successfully"
case C.SEC_I_INCOMPLETE_CREDENTIALS:
s = "The credentials supplied were not complete and could not be verified. Additional information can be returned from the context."
case C.SEC_I_LOCAL_LOGON:
s = "The logon was completed"
case C.SEC_I_NO_LSA_CONTEXT:
s = "There is no LSA mode context associated with this context."
case C.SEC_I_RENEGOTIATE:
s = "The context data must be renegotiated with the peer."
default:
return fmt.Errorf("%s: 0x%x", prefix, uint32(status))
}
return fmt.Errorf("%s: %s(0x%x)", prefix, s, uint32(status))
}
@@ -0,0 +1,249 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//+build gssapi,windows
#include "sspi_wrapper.h"
static HINSTANCE sspi_secur32_dll = NULL;
static PSecurityFunctionTable sspi_functions = NULL;
static const LPSTR SSPI_PACKAGE_NAME = "kerberos";
int sspi_init(
)
{
// Load the secur32.dll library using its exact path. Passing the exact DLL path rather than allowing LoadLibrary to
// search in different locations removes the possibility of DLL preloading attacks. We use GetSystemDirectoryA and
// LoadLibraryA rather than the GetSystemDirectory/LoadLibrary aliases to ensure the ANSI versions are used so we
// don't have to account for variations in char sizes if UNICODE is enabled.
// Passing a 0 size will return the required buffer length to hold the path, including the null terminator.
int requiredLen = GetSystemDirectoryA(NULL, 0);
if (!requiredLen) {
return GetLastError();
}
// Allocate a buffer to hold the system directory + "\secur32.dll" (length 12, not including null terminator).
int actualLen = requiredLen + 12;
char *directoryBuffer = (char *) calloc(1, actualLen);
int directoryLen = GetSystemDirectoryA(directoryBuffer, actualLen);
if (!directoryLen) {
free(directoryBuffer);
return GetLastError();
}
// Append the DLL name to the buffer.
char *dllName = "\\secur32.dll";
strcpy_s(&(directoryBuffer[directoryLen]), actualLen - directoryLen, dllName);
sspi_secur32_dll = LoadLibraryA(directoryBuffer);
free(directoryBuffer);
if (!sspi_secur32_dll) {
return GetLastError();
}
INIT_SECURITY_INTERFACE init_security_interface = (INIT_SECURITY_INTERFACE)GetProcAddress(sspi_secur32_dll, SECURITY_ENTRYPOINT);
if (!init_security_interface) {
return -1;
}
sspi_functions = (*init_security_interface)();
if (!sspi_functions) {
return -2;
}
return SSPI_OK;
}
int sspi_client_init(
sspi_client_state *client,
char* username,
char* password
)
{
TimeStamp timestamp;
if (username) {
if (password) {
SEC_WINNT_AUTH_IDENTITY auth_identity;
#ifdef _UNICODE
auth_identity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
#else
auth_identity.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI;
#endif
auth_identity.User = (LPSTR) username;
auth_identity.UserLength = strlen(username);
auth_identity.Password = (LPSTR) password;
auth_identity.PasswordLength = strlen(password);
auth_identity.Domain = NULL;
auth_identity.DomainLength = 0;
client->status = sspi_functions->AcquireCredentialsHandle(NULL, SSPI_PACKAGE_NAME, SECPKG_CRED_OUTBOUND, NULL, &auth_identity, NULL, NULL, &client->cred, &timestamp);
} else {
client->status = sspi_functions->AcquireCredentialsHandle(username, SSPI_PACKAGE_NAME, SECPKG_CRED_OUTBOUND, NULL, NULL, NULL, NULL, &client->cred, &timestamp);
}
} else {
client->status = sspi_functions->AcquireCredentialsHandle(NULL, SSPI_PACKAGE_NAME, SECPKG_CRED_OUTBOUND, NULL, NULL, NULL, NULL, &client->cred, &timestamp);
}
if (client->status != SEC_E_OK) {
return SSPI_ERROR;
}
return SSPI_OK;
}
int sspi_client_username(
sspi_client_state *client,
char** username
)
{
SecPkgCredentials_Names names;
client->status = sspi_functions->QueryCredentialsAttributes(&client->cred, SECPKG_CRED_ATTR_NAMES, &names);
if (client->status != SEC_E_OK) {
return SSPI_ERROR;
}
int len = strlen(names.sUserName) + 1;
*username = malloc(len);
memcpy(*username, names.sUserName, len);
sspi_functions->FreeContextBuffer(names.sUserName);
return SSPI_OK;
}
int sspi_client_negotiate(
sspi_client_state *client,
char* spn,
PVOID input,
ULONG input_length,
PVOID* output,
ULONG* output_length
)
{
SecBufferDesc inbuf;
SecBuffer in_bufs[1];
SecBufferDesc outbuf;
SecBuffer out_bufs[1];
if (client->has_ctx > 0) {
inbuf.ulVersion = SECBUFFER_VERSION;
inbuf.cBuffers = 1;
inbuf.pBuffers = in_bufs;
in_bufs[0].pvBuffer = input;
in_bufs[0].cbBuffer = input_length;
in_bufs[0].BufferType = SECBUFFER_TOKEN;
}
outbuf.ulVersion = SECBUFFER_VERSION;
outbuf.cBuffers = 1;
outbuf.pBuffers = out_bufs;
out_bufs[0].pvBuffer = NULL;
out_bufs[0].cbBuffer = 0;
out_bufs[0].BufferType = SECBUFFER_TOKEN;
ULONG context_attr = 0;
client->status = sspi_functions->InitializeSecurityContext(
&client->cred,
client->has_ctx > 0 ? &client->ctx : NULL,
(LPSTR) spn,
ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_MUTUAL_AUTH,
0,
SECURITY_NETWORK_DREP,
client->has_ctx > 0 ? &inbuf : NULL,
0,
&client->ctx,
&outbuf,
&context_attr,
NULL);
if (client->status != SEC_E_OK && client->status != SEC_I_CONTINUE_NEEDED) {
return SSPI_ERROR;
}
client->has_ctx = 1;
*output = malloc(out_bufs[0].cbBuffer);
*output_length = out_bufs[0].cbBuffer;
memcpy(*output, out_bufs[0].pvBuffer, *output_length);
sspi_functions->FreeContextBuffer(out_bufs[0].pvBuffer);
if (client->status == SEC_I_CONTINUE_NEEDED) {
return SSPI_CONTINUE;
}
return SSPI_OK;
}
int sspi_client_wrap_msg(
sspi_client_state *client,
PVOID input,
ULONG input_length,
PVOID* output,
ULONG* output_length
)
{
SecPkgContext_Sizes sizes;
client->status = sspi_functions->QueryContextAttributes(&client->ctx, SECPKG_ATTR_SIZES, &sizes);
if (client->status != SEC_E_OK) {
return SSPI_ERROR;
}
char *msg = malloc((sizes.cbSecurityTrailer + input_length + sizes.cbBlockSize) * sizeof(char));
memcpy(&msg[sizes.cbSecurityTrailer], input, input_length);
SecBuffer wrap_bufs[3];
SecBufferDesc wrap_buf_desc;
wrap_buf_desc.cBuffers = 3;
wrap_buf_desc.pBuffers = wrap_bufs;
wrap_buf_desc.ulVersion = SECBUFFER_VERSION;
wrap_bufs[0].cbBuffer = sizes.cbSecurityTrailer;
wrap_bufs[0].BufferType = SECBUFFER_TOKEN;
wrap_bufs[0].pvBuffer = msg;
wrap_bufs[1].cbBuffer = input_length;
wrap_bufs[1].BufferType = SECBUFFER_DATA;
wrap_bufs[1].pvBuffer = msg + sizes.cbSecurityTrailer;
wrap_bufs[2].cbBuffer = sizes.cbBlockSize;
wrap_bufs[2].BufferType = SECBUFFER_PADDING;
wrap_bufs[2].pvBuffer = msg + sizes.cbSecurityTrailer + input_length;
client->status = sspi_functions->EncryptMessage(&client->ctx, SECQOP_WRAP_NO_ENCRYPT, &wrap_buf_desc, 0);
if (client->status != SEC_E_OK) {
free(msg);
return SSPI_ERROR;
}
*output_length = wrap_bufs[0].cbBuffer + wrap_bufs[1].cbBuffer + wrap_bufs[2].cbBuffer;
*output = malloc(*output_length);
memcpy(*output, wrap_bufs[0].pvBuffer, wrap_bufs[0].cbBuffer);
memcpy(*output + wrap_bufs[0].cbBuffer, wrap_bufs[1].pvBuffer, wrap_bufs[1].cbBuffer);
memcpy(*output + wrap_bufs[0].cbBuffer + wrap_bufs[1].cbBuffer, wrap_bufs[2].pvBuffer, wrap_bufs[2].cbBuffer);
free(msg);
return SSPI_OK;
}
int sspi_client_destroy(
sspi_client_state *client
)
{
if (client->has_ctx > 0) {
sspi_functions->DeleteSecurityContext(&client->ctx);
}
sspi_functions->FreeCredentialsHandle(&client->cred);
return SSPI_OK;
}
@@ -0,0 +1,64 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//+build gssapi,windows
#ifndef SSPI_WRAPPER_H
#define SSPI_WRAPPER_H
#define SECURITY_WIN32 1 /* Required for SSPI */
#include <windows.h>
#include <sspi.h>
#define SSPI_OK 0
#define SSPI_CONTINUE 1
#define SSPI_ERROR 2
typedef struct {
CredHandle cred;
CtxtHandle ctx;
int has_ctx;
SECURITY_STATUS status;
} sspi_client_state;
int sspi_init();
int sspi_client_init(
sspi_client_state *client,
char* username,
char* password
);
int sspi_client_username(
sspi_client_state *client,
char** username
);
int sspi_client_negotiate(
sspi_client_state *client,
char* spn,
PVOID input,
ULONG input_length,
PVOID* output,
ULONG* output_length
);
int sspi_client_wrap_msg(
sspi_client_state *client,
PVOID input,
ULONG input_length,
PVOID* output,
ULONG* output_length
);
int sspi_client_destroy(
sspi_client_state *client
);
#endif
+76
View File
@@ -0,0 +1,76 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"context"
)
// MongoDBAWS is the mechanism name for MongoDBAWS.
const MongoDBAWS = "MONGODB-AWS"
func newMongoDBAWSAuthenticator(cred *Cred) (Authenticator, error) {
if cred.Source != "" && cred.Source != "$external" {
return nil, newAuthError("MONGODB-AWS source must be empty or $external", nil)
}
return &MongoDBAWSAuthenticator{
source: cred.Source,
username: cred.Username,
password: cred.Password,
sessionToken: cred.Props["AWS_SESSION_TOKEN"],
}, nil
}
// MongoDBAWSAuthenticator uses AWS-IAM credentials over SASL to authenticate a connection.
type MongoDBAWSAuthenticator struct {
source string
username string
password string
sessionToken string
}
// Auth authenticates the connection.
func (a *MongoDBAWSAuthenticator) Auth(ctx context.Context, cfg *Config) error {
adapter := &awsSaslAdapter{
conversation: &awsConversation{
username: a.username,
password: a.password,
token: a.sessionToken,
},
}
err := ConductSaslConversation(ctx, cfg, a.source, adapter)
if err != nil {
return newAuthError("sasl conversation error", err)
}
return nil
}
type awsSaslAdapter struct {
conversation *awsConversation
}
var _ SaslClient = (*awsSaslAdapter)(nil)
func (a *awsSaslAdapter) Start() (string, []byte, error) {
step, err := a.conversation.Step(nil)
if err != nil {
return MongoDBAWS, nil, err
}
return MongoDBAWS, step, nil
}
func (a *awsSaslAdapter) Next(challenge []byte) ([]byte, error) {
step, err := a.conversation.Step(challenge)
if err != nil {
return nil, err
}
return step, nil
}
func (a *awsSaslAdapter) Completed() bool {
return a.conversation.Done()
}
+110
View File
@@ -0,0 +1,110 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"context"
"fmt"
"io"
// Ignore gosec warning "Blocklisted import crypto/md5: weak cryptographic primitive". We need
// to use MD5 here to implement the MONGODB-CR specification.
/* #nosec G501 */
"crypto/md5"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/operation"
)
// MONGODBCR is the mechanism name for MONGODB-CR.
//
// The MONGODB-CR authentication mechanism is deprecated in MongoDB 3.6 and removed in
// MongoDB 4.0.
const MONGODBCR = "MONGODB-CR"
func newMongoDBCRAuthenticator(cred *Cred) (Authenticator, error) {
return &MongoDBCRAuthenticator{
DB: cred.Source,
Username: cred.Username,
Password: cred.Password,
}, nil
}
// MongoDBCRAuthenticator uses the MONGODB-CR algorithm to authenticate a connection.
//
// The MONGODB-CR authentication mechanism is deprecated in MongoDB 3.6 and removed in
// MongoDB 4.0.
type MongoDBCRAuthenticator struct {
DB string
Username string
Password string
}
// Auth authenticates the connection.
//
// The MONGODB-CR authentication mechanism is deprecated in MongoDB 3.6 and removed in
// MongoDB 4.0.
func (a *MongoDBCRAuthenticator) Auth(ctx context.Context, cfg *Config) error {
db := a.DB
if db == "" {
db = defaultAuthDB
}
doc := bsoncore.BuildDocumentFromElements(nil, bsoncore.AppendInt32Element(nil, "getnonce", 1))
cmd := operation.NewCommand(doc).
Database(db).
Deployment(driver.SingleConnectionDeployment{cfg.Connection}).
ClusterClock(cfg.ClusterClock).
ServerAPI(cfg.ServerAPI)
err := cmd.Execute(ctx)
if err != nil {
return newError(err, MONGODBCR)
}
rdr := cmd.Result()
var getNonceResult struct {
Nonce string `bson:"nonce"`
}
err = bson.Unmarshal(rdr, &getNonceResult)
if err != nil {
return newAuthError("unmarshal error", err)
}
doc = bsoncore.BuildDocumentFromElements(nil,
bsoncore.AppendInt32Element(nil, "authenticate", 1),
bsoncore.AppendStringElement(nil, "user", a.Username),
bsoncore.AppendStringElement(nil, "nonce", getNonceResult.Nonce),
bsoncore.AppendStringElement(nil, "key", a.createKey(getNonceResult.Nonce)),
)
cmd = operation.NewCommand(doc).
Database(db).
Deployment(driver.SingleConnectionDeployment{cfg.Connection}).
ClusterClock(cfg.ClusterClock).
ServerAPI(cfg.ServerAPI)
err = cmd.Execute(ctx)
if err != nil {
return newError(err, MONGODBCR)
}
return nil
}
func (a *MongoDBCRAuthenticator) createKey(nonce string) string {
// Ignore gosec warning "Use of weak cryptographic primitive". We need to use MD5 here to
// implement the MONGODB-CR specification.
/* #nosec G401 */
h := md5.New()
_, _ = io.WriteString(h, nonce)
_, _ = io.WriteString(h, a.Username)
_, _ = io.WriteString(h, mongoPasswordDigest(a.Username, a.Password))
return fmt.Sprintf("%x", h.Sum(nil))
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"context"
)
// PLAIN is the mechanism name for PLAIN.
const PLAIN = "PLAIN"
func newPlainAuthenticator(cred *Cred) (Authenticator, error) {
return &PlainAuthenticator{
Username: cred.Username,
Password: cred.Password,
}, nil
}
// PlainAuthenticator uses the PLAIN algorithm over SASL to authenticate a connection.
type PlainAuthenticator struct {
Username string
Password string
}
// Auth authenticates the connection.
func (a *PlainAuthenticator) Auth(ctx context.Context, cfg *Config) error {
return ConductSaslConversation(ctx, cfg, "$external", &plainSaslClient{
username: a.Username,
password: a.Password,
})
}
type plainSaslClient struct {
username string
password string
}
var _ SaslClient = (*plainSaslClient)(nil)
func (c *plainSaslClient) Start() (string, []byte, error) {
b := []byte("\x00" + c.username + "\x00" + c.password)
return PLAIN, b, nil
}
func (c *plainSaslClient) Next(challenge []byte) ([]byte, error) {
return nil, newAuthError("unexpected server challenge", nil)
}
func (c *plainSaslClient) Completed() bool {
return true
}
+174
View File
@@ -0,0 +1,174 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"context"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/operation"
)
// SaslClient is the client piece of a sasl conversation.
type SaslClient interface {
Start() (string, []byte, error)
Next(challenge []byte) ([]byte, error)
Completed() bool
}
// SaslClientCloser is a SaslClient that has resources to clean up.
type SaslClientCloser interface {
SaslClient
Close()
}
// ExtraOptionsSaslClient is a SaslClient that appends options to the saslStart command.
type ExtraOptionsSaslClient interface {
StartCommandOptions() bsoncore.Document
}
// saslConversation represents a SASL conversation. This type implements the SpeculativeConversation interface so the
// conversation can be executed in multi-step speculative fashion.
type saslConversation struct {
client SaslClient
source string
mechanism string
speculative bool
}
var _ SpeculativeConversation = (*saslConversation)(nil)
func newSaslConversation(client SaslClient, source string, speculative bool) *saslConversation {
authSource := source
if authSource == "" {
authSource = defaultAuthDB
}
return &saslConversation{
client: client,
source: authSource,
speculative: speculative,
}
}
// FirstMessage returns the first message to be sent to the server. This message contains a "db" field so it can be used
// for speculative authentication.
func (sc *saslConversation) FirstMessage() (bsoncore.Document, error) {
var payload []byte
var err error
sc.mechanism, payload, err = sc.client.Start()
if err != nil {
return nil, err
}
saslCmdElements := [][]byte{
bsoncore.AppendInt32Element(nil, "saslStart", 1),
bsoncore.AppendStringElement(nil, "mechanism", sc.mechanism),
bsoncore.AppendBinaryElement(nil, "payload", 0x00, payload),
}
if sc.speculative {
// The "db" field is only appended for speculative auth because the hello command is executed against admin
// so this is needed to tell the server the user's auth source. For a non-speculative attempt, the SASL commands
// will be executed against the auth source.
saslCmdElements = append(saslCmdElements, bsoncore.AppendStringElement(nil, "db", sc.source))
}
if extraOptionsClient, ok := sc.client.(ExtraOptionsSaslClient); ok {
optionsDoc := extraOptionsClient.StartCommandOptions()
saslCmdElements = append(saslCmdElements, bsoncore.AppendDocumentElement(nil, "options", optionsDoc))
}
return bsoncore.BuildDocumentFromElements(nil, saslCmdElements...), nil
}
type saslResponse struct {
ConversationID int `bson:"conversationId"`
Code int `bson:"code"`
Done bool `bson:"done"`
Payload []byte `bson:"payload"`
}
// Finish completes the conversation based on the first server response to authenticate the given connection.
func (sc *saslConversation) Finish(ctx context.Context, cfg *Config, firstResponse bsoncore.Document) error {
if closer, ok := sc.client.(SaslClientCloser); ok {
defer closer.Close()
}
var saslResp saslResponse
err := bson.Unmarshal(firstResponse, &saslResp)
if err != nil {
fullErr := fmt.Errorf("unmarshal error: %v", err)
return newError(fullErr, sc.mechanism)
}
cid := saslResp.ConversationID
var payload []byte
var rdr bsoncore.Document
for {
if saslResp.Code != 0 {
return newError(err, sc.mechanism)
}
if saslResp.Done && sc.client.Completed() {
return nil
}
payload, err = sc.client.Next(saslResp.Payload)
if err != nil {
return newError(err, sc.mechanism)
}
if saslResp.Done && sc.client.Completed() {
return nil
}
doc := bsoncore.BuildDocumentFromElements(nil,
bsoncore.AppendInt32Element(nil, "saslContinue", 1),
bsoncore.AppendInt32Element(nil, "conversationId", int32(cid)),
bsoncore.AppendBinaryElement(nil, "payload", 0x00, payload),
)
saslContinueCmd := operation.NewCommand(doc).
Database(sc.source).
Deployment(driver.SingleConnectionDeployment{cfg.Connection}).
ClusterClock(cfg.ClusterClock).
ServerAPI(cfg.ServerAPI)
err = saslContinueCmd.Execute(ctx)
if err != nil {
return newError(err, sc.mechanism)
}
rdr = saslContinueCmd.Result()
err = bson.Unmarshal(rdr, &saslResp)
if err != nil {
fullErr := fmt.Errorf("unmarshal error: %v", err)
return newError(fullErr, sc.mechanism)
}
}
}
// ConductSaslConversation runs a full SASL conversation to authenticate the given connection.
func ConductSaslConversation(ctx context.Context, cfg *Config, authSource string, client SaslClient) error {
// Create a non-speculative SASL conversation.
conversation := newSaslConversation(client, authSource, false)
saslStartDoc, err := conversation.FirstMessage()
if err != nil {
return newError(err, conversation.mechanism)
}
saslStartCmd := operation.NewCommand(saslStartDoc).
Database(authSource).
Deployment(driver.SingleConnectionDeployment{cfg.Connection}).
ClusterClock(cfg.ClusterClock).
ServerAPI(cfg.ServerAPI)
if err := saslStartCmd.Execute(ctx); err != nil {
return newError(err, conversation.mechanism)
}
return conversation.Finish(ctx, cfg, saslStartCmd.Result())
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
// Copyright (C) MongoDB, Inc. 2018-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"context"
"fmt"
"github.com/xdg-go/scram"
"github.com/xdg-go/stringprep"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
const (
// SCRAMSHA1 holds the mechanism name "SCRAM-SHA-1"
SCRAMSHA1 = "SCRAM-SHA-1"
// SCRAMSHA256 holds the mechanism name "SCRAM-SHA-256"
SCRAMSHA256 = "SCRAM-SHA-256"
)
var (
// Additional options for the saslStart command to enable a shorter SCRAM conversation
scramStartOptions bsoncore.Document = bsoncore.BuildDocumentFromElements(nil,
bsoncore.AppendBooleanElement(nil, "skipEmptyExchange", true),
)
)
func newScramSHA1Authenticator(cred *Cred) (Authenticator, error) {
passdigest := mongoPasswordDigest(cred.Username, cred.Password)
client, err := scram.SHA1.NewClientUnprepped(cred.Username, passdigest, "")
if err != nil {
return nil, newAuthError("error initializing SCRAM-SHA-1 client", err)
}
client.WithMinIterations(4096)
return &ScramAuthenticator{
mechanism: SCRAMSHA1,
source: cred.Source,
client: client,
}, nil
}
func newScramSHA256Authenticator(cred *Cred) (Authenticator, error) {
passprep, err := stringprep.SASLprep.Prepare(cred.Password)
if err != nil {
return nil, newAuthError(fmt.Sprintf("error SASLprepping password '%s'", cred.Password), err)
}
client, err := scram.SHA256.NewClientUnprepped(cred.Username, passprep, "")
if err != nil {
return nil, newAuthError("error initializing SCRAM-SHA-256 client", err)
}
client.WithMinIterations(4096)
return &ScramAuthenticator{
mechanism: SCRAMSHA256,
source: cred.Source,
client: client,
}, nil
}
// ScramAuthenticator uses the SCRAM algorithm over SASL to authenticate a connection.
type ScramAuthenticator struct {
mechanism string
source string
client *scram.Client
}
var _ SpeculativeAuthenticator = (*ScramAuthenticator)(nil)
// Auth authenticates the provided connection by conducting a full SASL conversation.
func (a *ScramAuthenticator) Auth(ctx context.Context, cfg *Config) error {
err := ConductSaslConversation(ctx, cfg, a.source, a.createSaslClient())
if err != nil {
return newAuthError("sasl conversation error", err)
}
return nil
}
// CreateSpeculativeConversation creates a speculative conversation for SCRAM authentication.
func (a *ScramAuthenticator) CreateSpeculativeConversation() (SpeculativeConversation, error) {
return newSaslConversation(a.createSaslClient(), a.source, true), nil
}
func (a *ScramAuthenticator) createSaslClient() SaslClient {
return &scramSaslAdapter{
conversation: a.client.NewConversation(),
mechanism: a.mechanism,
}
}
type scramSaslAdapter struct {
mechanism string
conversation *scram.ClientConversation
}
var _ SaslClient = (*scramSaslAdapter)(nil)
var _ ExtraOptionsSaslClient = (*scramSaslAdapter)(nil)
func (a *scramSaslAdapter) Start() (string, []byte, error) {
step, err := a.conversation.Step("")
if err != nil {
return a.mechanism, nil, err
}
return a.mechanism, []byte(step), nil
}
func (a *scramSaslAdapter) Next(challenge []byte) ([]byte, error) {
step, err := a.conversation.Step(string(challenge))
if err != nil {
return nil, err
}
return []byte(step), nil
}
func (a *scramSaslAdapter) Completed() bool {
return a.conversation.Done()
}
func (*scramSaslAdapter) StartCommandOptions() bsoncore.Document {
return scramStartOptions
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"fmt"
"io"
// Ignore gosec warning "Blocklisted import crypto/md5: weak cryptographic primitive". We need
// to use MD5 here to implement the SCRAM specification.
/* #nosec G501 */
"crypto/md5"
)
const defaultAuthDB = "admin"
func mongoPasswordDigest(username, password string) string {
// Ignore gosec warning "Use of weak cryptographic primitive". We need to use MD5 here to
// implement the SCRAM specification.
/* #nosec G401 */
h := md5.New()
_, _ = io.WriteString(h, username)
_, _ = io.WriteString(h, ":mongo:")
_, _ = io.WriteString(h, password)
return fmt.Sprintf("%x", h.Sum(nil))
}
+85
View File
@@ -0,0 +1,85 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package auth
import (
"context"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/operation"
)
// MongoDBX509 is the mechanism name for MongoDBX509.
const MongoDBX509 = "MONGODB-X509"
func newMongoDBX509Authenticator(cred *Cred) (Authenticator, error) {
return &MongoDBX509Authenticator{User: cred.Username}, nil
}
// MongoDBX509Authenticator uses X.509 certificates over TLS to authenticate a connection.
type MongoDBX509Authenticator struct {
User string
}
var _ SpeculativeAuthenticator = (*MongoDBX509Authenticator)(nil)
// x509 represents a X509 authentication conversation. This type implements the SpeculativeConversation interface so the
// conversation can be executed in multi-step speculative fashion.
type x509Conversation struct{}
var _ SpeculativeConversation = (*x509Conversation)(nil)
// FirstMessage returns the first message to be sent to the server.
func (c *x509Conversation) FirstMessage() (bsoncore.Document, error) {
return createFirstX509Message(description.Server{}, ""), nil
}
// createFirstX509Message creates the first message for the X509 conversation.
func createFirstX509Message(desc description.Server, user string) bsoncore.Document {
elements := [][]byte{
bsoncore.AppendInt32Element(nil, "authenticate", 1),
bsoncore.AppendStringElement(nil, "mechanism", MongoDBX509),
}
// Server versions < 3.4 require the username to be included in the message. Versions >= 3.4 will extract the
// username from the certificate.
if desc.WireVersion != nil && desc.WireVersion.Max < 5 {
elements = append(elements, bsoncore.AppendStringElement(nil, "user", user))
}
return bsoncore.BuildDocument(nil, elements...)
}
// Finish implements the SpeculativeConversation interface and is a no-op because an X509 conversation only has one
// step.
func (c *x509Conversation) Finish(context.Context, *Config, bsoncore.Document) error {
return nil
}
// CreateSpeculativeConversation creates a speculative conversation for X509 authentication.
func (a *MongoDBX509Authenticator) CreateSpeculativeConversation() (SpeculativeConversation, error) {
return &x509Conversation{}, nil
}
// Auth authenticates the provided connection by conducting an X509 authentication conversation.
func (a *MongoDBX509Authenticator) Auth(ctx context.Context, cfg *Config) error {
requestDoc := createFirstX509Message(cfg.Description, a.User)
authCmd := operation.
NewCommand(requestDoc).
Database("$external").
Deployment(driver.SingleConnectionDeployment{cfg.Connection}).
ClusterClock(cfg.ClusterClock).
ServerAPI(cfg.ServerAPI)
err := authCmd.Execute(ctx)
if err != nil {
return newAuthError("round trip error", err)
}
return nil
}
+470
View File
@@ -0,0 +1,470 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver
import (
"context"
"errors"
"fmt"
"strings"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// BatchCursor is a batch implementation of a cursor. It returns documents in entire batches instead
// of one at a time. An individual document cursor can be built on top of this batch cursor.
type BatchCursor struct {
clientSession *session.Client
clock *session.ClusterClock
comment bsoncore.Value
database string
collection string
id int64
err error
server Server
serverDescription description.Server
errorProcessor ErrorProcessor // This will only be set when pinning to a connection.
connection PinnedConnection
batchSize int32
maxTimeMS int64
currentBatch *bsoncore.DocumentSequence
firstBatch bool
cmdMonitor *event.CommandMonitor
postBatchResumeToken bsoncore.Document
crypt Crypt
serverAPI *ServerAPIOptions
// legacy server (< 3.2) fields
limit int32
numReturned int32 // number of docs returned by server
}
// CursorResponse represents the response from a command the results in a cursor. A BatchCursor can
// be constructed from a CursorResponse.
type CursorResponse struct {
Server Server
ErrorProcessor ErrorProcessor // This will only be set when pinning to a connection.
Connection PinnedConnection
Desc description.Server
FirstBatch *bsoncore.DocumentSequence
Database string
Collection string
ID int64
postBatchResumeToken bsoncore.Document
}
// NewCursorResponse constructs a cursor response from the given response and server. This method
// can be used within the ProcessResponse method for an operation.
func NewCursorResponse(info ResponseInfo) (CursorResponse, error) {
response := info.ServerResponse
cur, ok := response.Lookup("cursor").DocumentOK()
if !ok {
return CursorResponse{}, fmt.Errorf("cursor should be an embedded document but is of BSON type %s", response.Lookup("cursor").Type)
}
elems, err := cur.Elements()
if err != nil {
return CursorResponse{}, err
}
curresp := CursorResponse{Server: info.Server, Desc: info.ConnectionDescription}
for _, elem := range elems {
switch elem.Key() {
case "firstBatch":
arr, ok := elem.Value().ArrayOK()
if !ok {
return CursorResponse{}, fmt.Errorf("firstBatch should be an array but is a BSON %s", elem.Value().Type)
}
curresp.FirstBatch = &bsoncore.DocumentSequence{Style: bsoncore.ArrayStyle, Data: arr}
case "ns":
ns, ok := elem.Value().StringValueOK()
if !ok {
return CursorResponse{}, fmt.Errorf("ns should be a string but is a BSON %s", elem.Value().Type)
}
index := strings.Index(ns, ".")
if index == -1 {
return CursorResponse{}, errors.New("ns field must contain a valid namespace, but is missing '.'")
}
curresp.Database = ns[:index]
curresp.Collection = ns[index+1:]
case "id":
curresp.ID, ok = elem.Value().Int64OK()
if !ok {
return CursorResponse{}, fmt.Errorf("id should be an int64 but it is a BSON %s", elem.Value().Type)
}
case "postBatchResumeToken":
curresp.postBatchResumeToken, ok = elem.Value().DocumentOK()
if !ok {
return CursorResponse{}, fmt.Errorf("post batch resume token should be a document but it is a BSON %s", elem.Value().Type)
}
}
}
// If the deployment is behind a load balancer and the cursor has a non-zero ID, pin the cursor to a connection and
// use the same connection to execute getMore and killCursors commands.
if curresp.Desc.LoadBalanced() && curresp.ID != 0 {
// Cache the server as an ErrorProcessor to use when constructing deployments for cursor commands.
ep, ok := curresp.Server.(ErrorProcessor)
if !ok {
return CursorResponse{}, fmt.Errorf("expected Server used to establish a cursor to implement ErrorProcessor, but got %T", curresp.Server)
}
curresp.ErrorProcessor = ep
refConn, ok := info.Connection.(PinnedConnection)
if !ok {
return CursorResponse{}, fmt.Errorf("expected Connection used to establish a cursor to implement PinnedConnection, but got %T", info.Connection)
}
if err := refConn.PinToCursor(); err != nil {
return CursorResponse{}, fmt.Errorf("error incrementing connection reference count when creating a cursor: %v", err)
}
curresp.Connection = refConn
}
return curresp, nil
}
// CursorOptions are extra options that are required to construct a BatchCursor.
type CursorOptions struct {
BatchSize int32
Comment bsoncore.Value
MaxTimeMS int64
Limit int32
CommandMonitor *event.CommandMonitor
Crypt Crypt
ServerAPI *ServerAPIOptions
}
// NewBatchCursor creates a new BatchCursor from the provided parameters.
func NewBatchCursor(cr CursorResponse, clientSession *session.Client, clock *session.ClusterClock, opts CursorOptions) (*BatchCursor, error) {
ds := cr.FirstBatch
bc := &BatchCursor{
clientSession: clientSession,
clock: clock,
comment: opts.Comment,
database: cr.Database,
collection: cr.Collection,
id: cr.ID,
server: cr.Server,
connection: cr.Connection,
errorProcessor: cr.ErrorProcessor,
batchSize: opts.BatchSize,
maxTimeMS: opts.MaxTimeMS,
cmdMonitor: opts.CommandMonitor,
firstBatch: true,
postBatchResumeToken: cr.postBatchResumeToken,
crypt: opts.Crypt,
serverAPI: opts.ServerAPI,
serverDescription: cr.Desc,
}
if ds != nil {
bc.numReturned = int32(ds.DocumentCount())
}
if cr.Desc.WireVersion == nil || cr.Desc.WireVersion.Max < 4 {
bc.limit = opts.Limit
// Take as many documents from the batch as needed.
if bc.limit != 0 && bc.limit < bc.numReturned {
for i := int32(0); i < bc.limit; i++ {
_, err := ds.Next()
if err != nil {
return nil, err
}
}
ds.Data = ds.Data[:ds.Pos]
ds.ResetIterator()
}
}
bc.currentBatch = ds
return bc, nil
}
// NewEmptyBatchCursor returns a batch cursor that is empty.
func NewEmptyBatchCursor() *BatchCursor {
return &BatchCursor{currentBatch: new(bsoncore.DocumentSequence)}
}
// NewBatchCursorFromDocuments returns a batch cursor with current batch set to a sequence-style
// DocumentSequence containing the provided documents.
func NewBatchCursorFromDocuments(documents []byte) *BatchCursor {
return &BatchCursor{
currentBatch: &bsoncore.DocumentSequence{
Data: documents,
Style: bsoncore.SequenceStyle,
},
// BatchCursors created with this function have no associated ID nor server, so no getMore
// calls will be made.
id: 0,
server: nil,
}
}
// ID returns the cursor ID for this batch cursor.
func (bc *BatchCursor) ID() int64 {
return bc.id
}
// Next indicates if there is another batch available. Returning false does not necessarily indicate
// that the cursor is closed. This method will return false when an empty batch is returned.
//
// If Next returns true, there is a valid batch of documents available. If Next returns false, there
// is not a valid batch of documents available.
func (bc *BatchCursor) Next(ctx context.Context) bool {
if ctx == nil {
ctx = context.Background()
}
if bc.firstBatch {
bc.firstBatch = false
return !bc.currentBatch.Empty()
}
if bc.id == 0 || bc.server == nil {
return false
}
bc.getMore(ctx)
return !bc.currentBatch.Empty()
}
// Batch will return a DocumentSequence for the current batch of documents. The returned
// DocumentSequence is only valid until the next call to Next or Close.
func (bc *BatchCursor) Batch() *bsoncore.DocumentSequence { return bc.currentBatch }
// Err returns the latest error encountered.
func (bc *BatchCursor) Err() error { return bc.err }
// Close closes this batch cursor.
func (bc *BatchCursor) Close(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
err := bc.KillCursor(ctx)
bc.id = 0
bc.currentBatch.Data = nil
bc.currentBatch.Style = 0
bc.currentBatch.ResetIterator()
connErr := bc.unpinConnection()
if err == nil {
err = connErr
}
return err
}
func (bc *BatchCursor) unpinConnection() error {
if bc.connection == nil {
return nil
}
err := bc.connection.UnpinFromCursor()
closeErr := bc.connection.Close()
if err == nil && closeErr != nil {
err = closeErr
}
bc.connection = nil
return err
}
// Server returns the server for this cursor.
func (bc *BatchCursor) Server() Server {
return bc.server
}
func (bc *BatchCursor) clearBatch() {
bc.currentBatch.Data = bc.currentBatch.Data[:0]
}
// KillCursor kills cursor on server without closing batch cursor
func (bc *BatchCursor) KillCursor(ctx context.Context) error {
if bc.server == nil || bc.id == 0 {
return nil
}
return Operation{
CommandFn: func(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "killCursors", bc.collection)
dst = bsoncore.BuildArrayElement(dst, "cursors", bsoncore.Value{Type: bsontype.Int64, Data: bsoncore.AppendInt64(nil, bc.id)})
return dst, nil
},
Database: bc.database,
Deployment: bc.getOperationDeployment(),
Client: bc.clientSession,
Clock: bc.clock,
Legacy: LegacyKillCursors,
CommandMonitor: bc.cmdMonitor,
ServerAPI: bc.serverAPI,
}.Execute(ctx, nil)
}
func (bc *BatchCursor) getMore(ctx context.Context) {
bc.clearBatch()
if bc.id == 0 {
return
}
// Required for legacy operations which don't support limit.
numToReturn := bc.batchSize
if bc.limit != 0 && bc.numReturned+bc.batchSize >= bc.limit {
numToReturn = bc.limit - bc.numReturned
if numToReturn <= 0 {
err := bc.Close(ctx)
if err != nil {
bc.err = err
}
return
}
}
bc.err = Operation{
CommandFn: func(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendInt64Element(dst, "getMore", bc.id)
dst = bsoncore.AppendStringElement(dst, "collection", bc.collection)
if numToReturn > 0 {
dst = bsoncore.AppendInt32Element(dst, "batchSize", numToReturn)
}
if bc.maxTimeMS > 0 {
dst = bsoncore.AppendInt64Element(dst, "maxTimeMS", bc.maxTimeMS)
}
// The getMore command does not support commenting pre-4.4.
if bc.comment.Type != bsontype.Type(0) && bc.serverDescription.WireVersion.Max >= 9 {
dst = bsoncore.AppendValueElement(dst, "comment", bc.comment)
}
return dst, nil
},
Database: bc.database,
Deployment: bc.getOperationDeployment(),
ProcessResponseFn: func(info ResponseInfo) error {
response := info.ServerResponse
id, ok := response.Lookup("cursor", "id").Int64OK()
if !ok {
return fmt.Errorf("cursor.id should be an int64 but is a BSON %s", response.Lookup("cursor", "id").Type)
}
bc.id = id
batch, ok := response.Lookup("cursor", "nextBatch").ArrayOK()
if !ok {
return fmt.Errorf("cursor.nextBatch should be an array but is a BSON %s", response.Lookup("cursor", "nextBatch").Type)
}
bc.currentBatch.Style = bsoncore.ArrayStyle
bc.currentBatch.Data = batch
bc.currentBatch.ResetIterator()
bc.numReturned += int32(bc.currentBatch.DocumentCount()) // Required for legacy operations which don't support limit.
pbrt, err := response.LookupErr("cursor", "postBatchResumeToken")
if err != nil {
// I don't really understand why we don't set bc.err here
return nil
}
pbrtDoc, ok := pbrt.DocumentOK()
if !ok {
bc.err = fmt.Errorf("expected BSON type for post batch resume token to be EmbeddedDocument but got %s", pbrt.Type)
return nil
}
bc.postBatchResumeToken = pbrtDoc
return nil
},
Client: bc.clientSession,
Clock: bc.clock,
Legacy: LegacyGetMore,
CommandMonitor: bc.cmdMonitor,
Crypt: bc.crypt,
ServerAPI: bc.serverAPI,
}.Execute(ctx, nil)
// Once the cursor has been drained, we can unpin the connection if one is currently pinned.
if bc.id == 0 {
err := bc.unpinConnection()
if err != nil && bc.err == nil {
bc.err = err
}
}
// If we're in load balanced mode and the pinned connection encounters a network error, we should not use it for
// future commands. Per the spec, the connection will not be unpinned until the cursor is actually closed, but
// we set the cursor ID to 0 to ensure the Close() call will not execute a killCursors command.
if driverErr, ok := bc.err.(Error); ok && driverErr.NetworkError() && bc.connection != nil {
bc.id = 0
}
// Required for legacy operations which don't support limit.
if bc.limit != 0 && bc.numReturned >= bc.limit {
// call KillCursor instead of Close because Close will clear out the data for the current batch.
err := bc.KillCursor(ctx)
if err != nil && bc.err == nil {
bc.err = err
}
}
}
// PostBatchResumeToken returns the latest seen post batch resume token.
func (bc *BatchCursor) PostBatchResumeToken() bsoncore.Document {
return bc.postBatchResumeToken
}
// SetBatchSize sets the batchSize for future getMores.
func (bc *BatchCursor) SetBatchSize(size int32) {
bc.batchSize = size
}
func (bc *BatchCursor) getOperationDeployment() Deployment {
if bc.connection != nil {
return &loadBalancedCursorDeployment{
errorProcessor: bc.errorProcessor,
conn: bc.connection,
}
}
return SingleServerDeployment{bc.server}
}
// loadBalancedCursorDeployment is used as a Deployment for getMore and killCursors commands when pinning to a
// connection in load balanced mode. This type also functions as an ErrorProcessor to ensure that SDAM errors are
// handled for these commands in this mode.
type loadBalancedCursorDeployment struct {
errorProcessor ErrorProcessor
conn PinnedConnection
}
var _ Deployment = (*loadBalancedCursorDeployment)(nil)
var _ Server = (*loadBalancedCursorDeployment)(nil)
var _ ErrorProcessor = (*loadBalancedCursorDeployment)(nil)
func (lbcd *loadBalancedCursorDeployment) SelectServer(_ context.Context, _ description.ServerSelector) (Server, error) {
return lbcd, nil
}
func (lbcd *loadBalancedCursorDeployment) Kind() description.TopologyKind {
return description.LoadBalanced
}
func (lbcd *loadBalancedCursorDeployment) Connection(_ context.Context) (Connection, error) {
return lbcd.conn, nil
}
// MinRTT always returns 0. It implements the driver.Server interface.
func (lbcd *loadBalancedCursorDeployment) MinRTT() time.Duration {
return 0
}
// RTT90 always returns 0. It implements the driver.Server interface.
func (lbcd *loadBalancedCursorDeployment) RTT90() time.Duration {
return 0
}
func (lbcd *loadBalancedCursorDeployment) ProcessError(err error, conn Connection) ProcessErrorResult {
return lbcd.errorProcessor.ProcessError(err, conn)
}
+76
View File
@@ -0,0 +1,76 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver
import (
"errors"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// ErrDocumentTooLarge occurs when a document that is larger than the maximum size accepted by a
// server is passed to an insert command.
var ErrDocumentTooLarge = errors.New("an inserted document is too large")
// Batches contains the necessary information to batch split an operation. This is only used for write
// oeprations.
type Batches struct {
Identifier string
Documents []bsoncore.Document
Current []bsoncore.Document
Ordered *bool
}
// Valid returns true if Batches contains both an identifier and the length of Documents is greater
// than zero.
func (b *Batches) Valid() bool { return b != nil && b.Identifier != "" && len(b.Documents) > 0 }
// ClearBatch clears the Current batch. This must be called before AdvanceBatch will advance to the
// next batch.
func (b *Batches) ClearBatch() { b.Current = b.Current[:0] }
// AdvanceBatch splits the next batch using maxCount and targetBatchSize. This method will do nothing if
// the current batch has not been cleared. We do this so that when this is called during execute we
// can call it without first needing to check if we already have a batch, which makes the code
// simpler and makes retrying easier.
// The maxDocSize parameter is used to check that any one document is not too large. If the first document is bigger
// than targetBatchSize but smaller than maxDocSize, a batch of size 1 containing that document will be created.
func (b *Batches) AdvanceBatch(maxCount, targetBatchSize, maxDocSize int) error {
if len(b.Current) > 0 {
return nil
}
if maxCount <= 0 {
maxCount = 1
}
splitAfter := 0
size := 0
for i, doc := range b.Documents {
if i == maxCount {
break
}
if len(doc) > maxDocSize {
return ErrDocumentTooLarge
}
if size+len(doc) > targetBatchSize {
break
}
size += len(doc)
splitAfter++
}
// if there are no documents, take the first one.
// this can happen if there is a document that is smaller than maxDocSize but greater than targetBatchSize.
if splitAfter == 0 {
splitAfter = 1
}
b.Current, b.Documents = b.Documents[:splitAfter], b.Documents[splitAfter:]
return nil
}
+111
View File
@@ -0,0 +1,111 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver
import (
"bytes"
"compress/zlib"
"fmt"
"io"
"sync"
"github.com/golang/snappy"
"github.com/klauspost/compress/zstd"
"go.mongodb.org/mongo-driver/x/mongo/driver/wiremessage"
)
// CompressionOpts holds settings for how to compress a payload
type CompressionOpts struct {
Compressor wiremessage.CompressorID
ZlibLevel int
ZstdLevel int
UncompressedSize int32
}
var zstdEncoders = &sync.Map{}
func getZstdEncoder(l zstd.EncoderLevel) (*zstd.Encoder, error) {
v, ok := zstdEncoders.Load(l)
if ok {
return v.(*zstd.Encoder), nil
}
encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(l))
if err != nil {
return nil, err
}
zstdEncoders.Store(l, encoder)
return encoder, nil
}
// CompressPayload takes a byte slice and compresses it according to the options passed
func CompressPayload(in []byte, opts CompressionOpts) ([]byte, error) {
switch opts.Compressor {
case wiremessage.CompressorNoOp:
return in, nil
case wiremessage.CompressorSnappy:
return snappy.Encode(nil, in), nil
case wiremessage.CompressorZLib:
var b bytes.Buffer
w, err := zlib.NewWriterLevel(&b, opts.ZlibLevel)
if err != nil {
return nil, err
}
_, err = w.Write(in)
if err != nil {
return nil, err
}
err = w.Close()
if err != nil {
return nil, err
}
return b.Bytes(), nil
case wiremessage.CompressorZstd:
encoder, err := getZstdEncoder(zstd.EncoderLevelFromZstd(opts.ZstdLevel))
if err != nil {
return nil, err
}
return encoder.EncodeAll(in, nil), nil
default:
return nil, fmt.Errorf("unknown compressor ID %v", opts.Compressor)
}
}
// DecompressPayload takes a byte slice that has been compressed and undoes it according to the options passed
func DecompressPayload(in []byte, opts CompressionOpts) ([]byte, error) {
switch opts.Compressor {
case wiremessage.CompressorNoOp:
return in, nil
case wiremessage.CompressorSnappy:
uncompressed := make([]byte, opts.UncompressedSize)
return snappy.Decode(uncompressed, in)
case wiremessage.CompressorZLib:
decompressor, err := zlib.NewReader(bytes.NewReader(in))
if err != nil {
return nil, err
}
uncompressed := make([]byte, opts.UncompressedSize)
_, err = io.ReadFull(decompressor, uncompressed)
if err != nil {
return nil, err
}
return uncompressed, nil
case wiremessage.CompressorZstd:
w, err := zstd.NewReader(bytes.NewBuffer(in))
if err != nil {
return nil, err
}
defer w.Close()
var b bytes.Buffer
_, err = io.Copy(&b, w)
if err != nil {
return nil, err
}
return b.Bytes(), nil
default:
return nil, fmt.Errorf("unknown compressor ID %v", opts.Compressor)
}
}
File diff suppressed because it is too large Load Diff
+384
View File
@@ -0,0 +1,384 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver
import (
"context"
"crypto/tls"
"fmt"
"io"
"strings"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver/mongocrypt"
"go.mongodb.org/mongo-driver/x/mongo/driver/mongocrypt/options"
)
const (
defaultKmsPort = 443
defaultKmsTimeout = 10 * time.Second
)
// CollectionInfoFn is a callback used to retrieve collection information.
type CollectionInfoFn func(ctx context.Context, db string, filter bsoncore.Document) (bsoncore.Document, error)
// KeyRetrieverFn is a callback used to retrieve keys from the key vault.
type KeyRetrieverFn func(ctx context.Context, filter bsoncore.Document) ([]bsoncore.Document, error)
// MarkCommandFn is a callback used to add encryption markings to a command.
type MarkCommandFn func(ctx context.Context, db string, cmd bsoncore.Document) (bsoncore.Document, error)
// CryptOptions specifies options to configure a Crypt instance.
type CryptOptions struct {
MongoCrypt *mongocrypt.MongoCrypt
CollInfoFn CollectionInfoFn
KeyFn KeyRetrieverFn
MarkFn MarkCommandFn
TLSConfig map[string]*tls.Config
BypassAutoEncryption bool
BypassQueryAnalysis bool
}
// Crypt is an interface implemented by types that can encrypt and decrypt instances of
// bsoncore.Document.
//
// Users should rely on the driver's crypt type (used by default) for encryption and decryption
// unless they are perfectly confident in another implementation of Crypt.
type Crypt interface {
// Encrypt encrypts the given command.
Encrypt(ctx context.Context, db string, cmd bsoncore.Document) (bsoncore.Document, error)
// Decrypt decrypts the given command response.
Decrypt(ctx context.Context, cmdResponse bsoncore.Document) (bsoncore.Document, error)
// CreateDataKey creates a data key using the given KMS provider and options.
CreateDataKey(ctx context.Context, kmsProvider string, opts *options.DataKeyOptions) (bsoncore.Document, error)
// EncryptExplicit encrypts the given value with the given options.
EncryptExplicit(ctx context.Context, val bsoncore.Value, opts *options.ExplicitEncryptionOptions) (byte, []byte, error)
// DecryptExplicit decrypts the given encrypted value.
DecryptExplicit(ctx context.Context, subtype byte, data []byte) (bsoncore.Value, error)
// Close cleans up any resources associated with the Crypt instance.
Close()
// BypassAutoEncryption returns true if auto-encryption should be bypassed.
BypassAutoEncryption() bool
// RewrapDataKey attempts to rewrap the document data keys matching the filter, preparing the re-wrapped documents
// to be returned as a slice of bsoncore.Document.
RewrapDataKey(ctx context.Context, filter []byte, opts *options.RewrapManyDataKeyOptions) ([]bsoncore.Document, error)
}
// crypt consumes the libmongocrypt.MongoCrypt type to iterate the mongocrypt state machine and perform encryption
// and decryption.
type crypt struct {
mongoCrypt *mongocrypt.MongoCrypt
collInfoFn CollectionInfoFn
keyFn KeyRetrieverFn
markFn MarkCommandFn
tlsConfig map[string]*tls.Config
bypassAutoEncryption bool
}
// NewCrypt creates a new Crypt instance configured with the given AutoEncryptionOptions.
func NewCrypt(opts *CryptOptions) Crypt {
return &crypt{
mongoCrypt: opts.MongoCrypt,
collInfoFn: opts.CollInfoFn,
keyFn: opts.KeyFn,
markFn: opts.MarkFn,
tlsConfig: opts.TLSConfig,
bypassAutoEncryption: opts.BypassAutoEncryption,
}
}
// Encrypt encrypts the given command.
func (c *crypt) Encrypt(ctx context.Context, db string, cmd bsoncore.Document) (bsoncore.Document, error) {
if c.bypassAutoEncryption {
return cmd, nil
}
cryptCtx, err := c.mongoCrypt.CreateEncryptionContext(db, cmd)
if err != nil {
return nil, err
}
defer cryptCtx.Close()
return c.executeStateMachine(ctx, cryptCtx, db)
}
// Decrypt decrypts the given command response.
func (c *crypt) Decrypt(ctx context.Context, cmdResponse bsoncore.Document) (bsoncore.Document, error) {
cryptCtx, err := c.mongoCrypt.CreateDecryptionContext(cmdResponse)
if err != nil {
return nil, err
}
defer cryptCtx.Close()
return c.executeStateMachine(ctx, cryptCtx, "")
}
// CreateDataKey creates a data key using the given KMS provider and options.
func (c *crypt) CreateDataKey(ctx context.Context, kmsProvider string, opts *options.DataKeyOptions) (bsoncore.Document, error) {
cryptCtx, err := c.mongoCrypt.CreateDataKeyContext(kmsProvider, opts)
if err != nil {
return nil, err
}
defer cryptCtx.Close()
return c.executeStateMachine(ctx, cryptCtx, "")
}
// RewrapDataKey attempts to rewrap the document data keys matching the filter, preparing the re-wrapped documents to
// be returned as a slice of bsoncore.Document.
func (c *crypt) RewrapDataKey(ctx context.Context, filter []byte,
opts *options.RewrapManyDataKeyOptions) ([]bsoncore.Document, error) {
cryptCtx, err := c.mongoCrypt.RewrapDataKeyContext(filter, opts)
if err != nil {
return nil, err
}
defer cryptCtx.Close()
rewrappedBSON, err := c.executeStateMachine(ctx, cryptCtx, "")
if err != nil {
return nil, err
}
if rewrappedBSON == nil {
return nil, nil
}
// mongocrypt_ctx_rewrap_many_datakey_init wraps the documents in a BSON of the form { "v": [(BSON document), ...] }
// where each BSON document in the slice is a document containing a rewrapped datakey.
rewrappedDocumentBytes, err := rewrappedBSON.LookupErr("v")
if err != nil {
return nil, err
}
// Parse the resulting BSON as individual documents.
rewrappedDocsArray, ok := rewrappedDocumentBytes.ArrayOK()
if !ok {
return nil, fmt.Errorf("expected results from mongocrypt_ctx_rewrap_many_datakey_init to be an array")
}
rewrappedDocumentValues, err := rewrappedDocsArray.Values()
if err != nil {
return nil, err
}
rewrappedDocuments := []bsoncore.Document{}
for _, rewrappedDocumentValue := range rewrappedDocumentValues {
if rewrappedDocumentValue.Type != bsontype.EmbeddedDocument {
// If a value in the document's array returned by mongocrypt is anything other than an embedded document,
// then something is wrong and we should terminate the routine.
return nil, fmt.Errorf("expected value of type %q, got: %q",
bsontype.EmbeddedDocument.String(),
rewrappedDocumentValue.Type.String())
}
rewrappedDocuments = append(rewrappedDocuments, rewrappedDocumentValue.Document())
}
return rewrappedDocuments, nil
}
// EncryptExplicit encrypts the given value with the given options.
func (c *crypt) EncryptExplicit(ctx context.Context, val bsoncore.Value, opts *options.ExplicitEncryptionOptions) (byte, []byte, error) {
idx, doc := bsoncore.AppendDocumentStart(nil)
doc = bsoncore.AppendValueElement(doc, "v", val)
doc, _ = bsoncore.AppendDocumentEnd(doc, idx)
cryptCtx, err := c.mongoCrypt.CreateExplicitEncryptionContext(doc, opts)
if err != nil {
return 0, nil, err
}
defer cryptCtx.Close()
res, err := c.executeStateMachine(ctx, cryptCtx, "")
if err != nil {
return 0, nil, err
}
sub, data := res.Lookup("v").Binary()
return sub, data, nil
}
// DecryptExplicit decrypts the given encrypted value.
func (c *crypt) DecryptExplicit(ctx context.Context, subtype byte, data []byte) (bsoncore.Value, error) {
idx, doc := bsoncore.AppendDocumentStart(nil)
doc = bsoncore.AppendBinaryElement(doc, "v", subtype, data)
doc, _ = bsoncore.AppendDocumentEnd(doc, idx)
cryptCtx, err := c.mongoCrypt.CreateExplicitDecryptionContext(doc)
if err != nil {
return bsoncore.Value{}, err
}
defer cryptCtx.Close()
res, err := c.executeStateMachine(ctx, cryptCtx, "")
if err != nil {
return bsoncore.Value{}, err
}
return res.Lookup("v"), nil
}
// Close cleans up any resources associated with the Crypt instance.
func (c *crypt) Close() {
c.mongoCrypt.Close()
}
func (c *crypt) BypassAutoEncryption() bool {
return c.bypassAutoEncryption
}
func (c *crypt) executeStateMachine(ctx context.Context, cryptCtx *mongocrypt.Context, db string) (bsoncore.Document, error) {
var err error
for {
state := cryptCtx.State()
switch state {
case mongocrypt.NeedMongoCollInfo:
err = c.collectionInfo(ctx, cryptCtx, db)
case mongocrypt.NeedMongoMarkings:
err = c.markCommand(ctx, cryptCtx, db)
case mongocrypt.NeedMongoKeys:
err = c.retrieveKeys(ctx, cryptCtx)
case mongocrypt.NeedKms:
err = c.decryptKeys(cryptCtx)
case mongocrypt.Ready:
return cryptCtx.Finish()
case mongocrypt.Done:
return nil, nil
default:
return nil, fmt.Errorf("invalid Crypt state: %v", state)
}
if err != nil {
return nil, err
}
}
}
func (c *crypt) collectionInfo(ctx context.Context, cryptCtx *mongocrypt.Context, db string) error {
op, err := cryptCtx.NextOperation()
if err != nil {
return err
}
collInfo, err := c.collInfoFn(ctx, db, op)
if err != nil {
return err
}
if collInfo != nil {
if err = cryptCtx.AddOperationResult(collInfo); err != nil {
return err
}
}
return cryptCtx.CompleteOperation()
}
func (c *crypt) markCommand(ctx context.Context, cryptCtx *mongocrypt.Context, db string) error {
op, err := cryptCtx.NextOperation()
if err != nil {
return err
}
markedCmd, err := c.markFn(ctx, db, op)
if err != nil {
return err
}
if err = cryptCtx.AddOperationResult(markedCmd); err != nil {
return err
}
return cryptCtx.CompleteOperation()
}
func (c *crypt) retrieveKeys(ctx context.Context, cryptCtx *mongocrypt.Context) error {
op, err := cryptCtx.NextOperation()
if err != nil {
return err
}
keys, err := c.keyFn(ctx, op)
if err != nil {
return err
}
for _, key := range keys {
if err = cryptCtx.AddOperationResult(key); err != nil {
return err
}
}
return cryptCtx.CompleteOperation()
}
func (c *crypt) decryptKeys(cryptCtx *mongocrypt.Context) error {
for {
kmsCtx := cryptCtx.NextKmsContext()
if kmsCtx == nil {
break
}
if err := c.decryptKey(kmsCtx); err != nil {
return err
}
}
return cryptCtx.FinishKmsContexts()
}
func (c *crypt) decryptKey(kmsCtx *mongocrypt.KmsContext) error {
host, err := kmsCtx.HostName()
if err != nil {
return err
}
msg, err := kmsCtx.Message()
if err != nil {
return err
}
// add a port to the address if it's not already present
addr := host
if idx := strings.IndexByte(host, ':'); idx == -1 {
addr = fmt.Sprintf("%s:%d", host, defaultKmsPort)
}
kmsProvider := kmsCtx.KMSProvider()
tlsCfg := c.tlsConfig[kmsProvider]
if tlsCfg == nil {
tlsCfg = &tls.Config{MinVersion: tls.VersionTLS12}
}
conn, err := tls.Dial("tcp", addr, tlsCfg)
if err != nil {
return err
}
defer func() {
_ = conn.Close()
}()
if err = conn.SetWriteDeadline(time.Now().Add(defaultKmsTimeout)); err != nil {
return err
}
if _, err = conn.Write(msg); err != nil {
return err
}
for {
bytesNeeded := kmsCtx.BytesNeeded()
if bytesNeeded == 0 {
return nil
}
res := make([]byte, bytesNeeded)
bytesRead, err := conn.Read(res)
if err != nil && err != io.EOF {
return err
}
if err = kmsCtx.FeedResponse(res[:bytesRead]); err != nil {
return err
}
}
}
+144
View File
@@ -0,0 +1,144 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package dns
import (
"errors"
"fmt"
"net"
"runtime"
"strings"
)
// Resolver resolves DNS records.
type Resolver struct {
// Holds the functions to use for DNS lookups
LookupSRV func(string, string, string) (string, []*net.SRV, error)
LookupTXT func(string) ([]string, error)
}
// DefaultResolver is a Resolver that uses the default Resolver from the net package.
var DefaultResolver = &Resolver{net.LookupSRV, net.LookupTXT}
// ParseHosts uses the srv string and service name to get the hosts.
func (r *Resolver) ParseHosts(host string, srvName string, stopOnErr bool) ([]string, error) {
parsedHosts := strings.Split(host, ",")
if len(parsedHosts) != 1 {
return nil, fmt.Errorf("URI with SRV must include one and only one hostname")
}
return r.fetchSeedlistFromSRV(parsedHosts[0], srvName, stopOnErr)
}
// GetConnectionArgsFromTXT gets the TXT record associated with the host and returns the connection arguments.
func (r *Resolver) GetConnectionArgsFromTXT(host string) ([]string, error) {
var connectionArgsFromTXT []string
// error ignored because not finding a TXT record should not be
// considered an error.
recordsFromTXT, _ := r.LookupTXT(host)
// This is a temporary fix to get around bug https://github.com/golang/go/issues/21472.
// It will currently incorrectly concatenate multiple TXT records to one
// on windows.
if runtime.GOOS == "windows" {
recordsFromTXT = []string{strings.Join(recordsFromTXT, "")}
}
if len(recordsFromTXT) > 1 {
return nil, errors.New("multiple records from TXT not supported")
}
if len(recordsFromTXT) > 0 {
connectionArgsFromTXT = strings.FieldsFunc(recordsFromTXT[0], func(r rune) bool { return r == ';' || r == '&' })
err := validateTXTResult(connectionArgsFromTXT)
if err != nil {
return nil, err
}
}
return connectionArgsFromTXT, nil
}
func (r *Resolver) fetchSeedlistFromSRV(host string, srvName string, stopOnErr bool) ([]string, error) {
var err error
_, _, err = net.SplitHostPort(host)
if err == nil {
// we were able to successfully extract a port from the host,
// but should not be able to when using SRV
return nil, fmt.Errorf("URI with srv must not include a port number")
}
// default to "mongodb" as service name if not supplied
if srvName == "" {
srvName = "mongodb"
}
_, addresses, err := r.LookupSRV(srvName, "tcp", host)
if err != nil {
return nil, err
}
trimmedHost := strings.TrimSuffix(host, ".")
parsedHosts := make([]string, 0, len(addresses))
for _, address := range addresses {
trimmedAddressTarget := strings.TrimSuffix(address.Target, ".")
err := validateSRVResult(trimmedAddressTarget, trimmedHost)
if err != nil {
if stopOnErr {
return nil, err
}
continue
}
parsedHosts = append(parsedHosts, fmt.Sprintf("%s:%d", trimmedAddressTarget, address.Port))
}
return parsedHosts, nil
}
func validateSRVResult(recordFromSRV, inputHostName string) error {
separatedInputDomain := strings.Split(inputHostName, ".")
separatedRecord := strings.Split(recordFromSRV, ".")
if len(separatedRecord) < 2 {
return errors.New("DNS name must contain at least 2 labels")
}
if len(separatedRecord) < len(separatedInputDomain) {
return errors.New("Domain suffix from SRV record not matched input domain")
}
inputDomainSuffix := separatedInputDomain[1:]
domainSuffixOffset := len(separatedRecord) - (len(separatedInputDomain) - 1)
recordDomainSuffix := separatedRecord[domainSuffixOffset:]
for ix, label := range inputDomainSuffix {
if label != recordDomainSuffix[ix] {
return errors.New("Domain suffix from SRV record not matched input domain")
}
}
return nil
}
var allowedTXTOptions = map[string]struct{}{
"authsource": {},
"replicaset": {},
"loadbalanced": {},
}
func validateTXTResult(paramsFromTXT []string) error {
for _, param := range paramsFromTXT {
kv := strings.SplitN(param, "=", 2)
if len(kv) != 2 {
return errors.New("Invalid TXT record")
}
key := strings.ToLower(kv[0])
if _, ok := allowedTXTOptions[key]; !ok {
return fmt.Errorf("Cannot specify option '%s' in TXT record", kv[0])
}
}
return nil
}
+262
View File
@@ -0,0 +1,262 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver // import "go.mongodb.org/mongo-driver/x/mongo/driver"
import (
"context"
"time"
"go.mongodb.org/mongo-driver/mongo/address"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Deployment is implemented by types that can select a server from a deployment.
type Deployment interface {
SelectServer(context.Context, description.ServerSelector) (Server, error)
Kind() description.TopologyKind
}
// Connector represents a type that can connect to a server.
type Connector interface {
Connect() error
}
// Disconnector represents a type that can disconnect from a server.
type Disconnector interface {
Disconnect(context.Context) error
}
// Subscription represents a subscription to topology updates. A subscriber can receive updates through the
// Updates field.
type Subscription struct {
Updates <-chan description.Topology
ID uint64
}
// Subscriber represents a type to which another type can subscribe. A subscription contains a channel that
// is updated with topology descriptions.
type Subscriber interface {
Subscribe() (*Subscription, error)
Unsubscribe(*Subscription) error
}
// Server represents a MongoDB server. Implementations should pool connections and handle the
// retrieving and returning of connections.
type Server interface {
Connection(context.Context) (Connection, error)
// MinRTT returns the minimum round-trip time to the server observed over the window period.
MinRTT() time.Duration
// RTT90 returns the 90th percentile round-trip time to the server observed over the window period.
RTT90() time.Duration
}
// Connection represents a connection to a MongoDB server.
type Connection interface {
WriteWireMessage(context.Context, []byte) error
ReadWireMessage(ctx context.Context, dst []byte) ([]byte, error)
Description() description.Server
// Close closes any underlying connection and returns or frees any resources held by the
// connection. Close is idempotent and can be called multiple times, although subsequent calls
// to Close may return an error. A connection cannot be used after it is closed.
Close() error
ID() string
ServerConnectionID() *int32
Address() address.Address
Stale() bool
}
// PinnedConnection represents a Connection that can be pinned by one or more cursors or transactions. Implementations
// of this interface should maintain the following invariants:
//
// 1. Each Pin* call should increment the number of references for the connection.
// 2. Each Unpin* call should decrement the number of references for the connection.
// 3. Calls to Close() should be ignored until all resources have unpinned the connection.
type PinnedConnection interface {
Connection
PinToCursor() error
PinToTransaction() error
UnpinFromCursor() error
UnpinFromTransaction() error
}
// The session.LoadBalancedTransactionConnection type is a copy of PinnedConnection that was introduced to avoid
// import cycles. This compile-time assertion ensures that these types remain in sync if the PinnedConnection interface
// is changed in the future.
var _ PinnedConnection = (session.LoadBalancedTransactionConnection)(nil)
// LocalAddresser is a type that is able to supply its local address
type LocalAddresser interface {
LocalAddress() address.Address
}
// Expirable represents an expirable object.
type Expirable interface {
Expire() error
Alive() bool
}
// StreamerConnection represents a Connection that supports streaming wire protocol messages using the moreToCome and
// exhaustAllowed flags.
//
// The SetStreaming and CurrentlyStreaming functions correspond to the moreToCome flag on server responses. If a
// response has moreToCome set, SetStreaming(true) will be called and CurrentlyStreaming() should return true.
//
// CanStream corresponds to the exhaustAllowed flag. The operations layer will set exhaustAllowed on outgoing wire
// messages to inform the server that the driver supports streaming.
type StreamerConnection interface {
Connection
SetStreaming(bool)
CurrentlyStreaming() bool
SupportsStreaming() bool
}
// Compressor is an interface used to compress wire messages. If a Connection supports compression
// it should implement this interface as well. The CompressWireMessage method will be called during
// the execution of an operation if the wire message is allowed to be compressed.
type Compressor interface {
CompressWireMessage(src, dst []byte) ([]byte, error)
}
// ProcessErrorResult represents the result of a ErrorProcessor.ProcessError() call. Exact values for this type can be
// checked directly (e.g. res == ServerMarkedUnknown), but it is recommended that applications use the ServerChanged()
// function instead.
type ProcessErrorResult int
const (
// NoChange indicates that the error did not affect the state of the server.
NoChange ProcessErrorResult = iota
// ServerMarkedUnknown indicates that the error only resulted in the server being marked as Unknown.
ServerMarkedUnknown
// ConnectionPoolCleared indicates that the error resulted in the server being marked as Unknown and its connection
// pool being cleared.
ConnectionPoolCleared
)
// ServerChanged returns true if the ProcessErrorResult indicates that the server changed from an SDAM perspective
// during a ProcessError() call.
func (p ProcessErrorResult) ServerChanged() bool {
return p != NoChange
}
// ErrorProcessor implementations can handle processing errors, which may modify their internal state.
// If this type is implemented by a Server, then Operation.Execute will call it's ProcessError
// method after it decodes a wire message.
type ErrorProcessor interface {
ProcessError(err error, conn Connection) ProcessErrorResult
}
// HandshakeInformation contains information extracted from a MongoDB connection handshake. This is a helper type that
// augments description.Server by also tracking server connection ID and authentication-related fields. We use this type
// rather than adding authentication-related fields to description.Server to avoid retaining sensitive information in a
// user-facing type. The server connection ID is stored in this type because unlike description.Server, all handshakes are
// correlated with a single network connection.
type HandshakeInformation struct {
Description description.Server
SpeculativeAuthenticate bsoncore.Document
ServerConnectionID *int32
SaslSupportedMechs []string
}
// Handshaker is the interface implemented by types that can perform a MongoDB
// handshake over a provided driver.Connection. This is used during connection
// initialization. Implementations must be goroutine safe.
type Handshaker interface {
GetHandshakeInformation(context.Context, address.Address, Connection) (HandshakeInformation, error)
FinishHandshake(context.Context, Connection) error
}
// SingleServerDeployment is an implementation of Deployment that always returns a single server.
type SingleServerDeployment struct{ Server }
var _ Deployment = SingleServerDeployment{}
// SelectServer implements the Deployment interface. This method does not use the
// description.SelectedServer provided and instead returns the embedded Server.
func (ssd SingleServerDeployment) SelectServer(context.Context, description.ServerSelector) (Server, error) {
return ssd.Server, nil
}
// Kind implements the Deployment interface. It always returns description.Single.
func (SingleServerDeployment) Kind() description.TopologyKind { return description.Single }
// SingleConnectionDeployment is an implementation of Deployment that always returns the same Connection. This
// implementation should only be used for connection handshakes and server heartbeats as it does not implement
// ErrorProcessor, which is necessary for application operations.
type SingleConnectionDeployment struct{ C Connection }
var _ Deployment = SingleConnectionDeployment{}
var _ Server = SingleConnectionDeployment{}
// SelectServer implements the Deployment interface. This method does not use the
// description.SelectedServer provided and instead returns itself. The Connections returned from the
// Connection method have a no-op Close method.
func (ssd SingleConnectionDeployment) SelectServer(context.Context, description.ServerSelector) (Server, error) {
return ssd, nil
}
// Kind implements the Deployment interface. It always returns description.Single.
func (ssd SingleConnectionDeployment) Kind() description.TopologyKind { return description.Single }
// Connection implements the Server interface. It always returns the embedded connection.
func (ssd SingleConnectionDeployment) Connection(context.Context) (Connection, error) {
return ssd.C, nil
}
// MinRTT always returns 0. It implements the driver.Server interface.
func (ssd SingleConnectionDeployment) MinRTT() time.Duration {
return 0
}
// RTT90 always returns 0. It implements the driver.Server interface.
func (ssd SingleConnectionDeployment) RTT90() time.Duration {
return 0
}
// TODO(GODRIVER-617): We can likely use 1 type for both the Type and the RetryMode by using 2 bits for the mode and 1
// TODO bit for the type. Although in the practical sense, we might not want to do that since the type of retryability
// TODO is tied to the operation itself and isn't going change, e.g. and insert operation will always be a write,
// TODO however some operations are both reads and writes, for instance aggregate is a read but with a $out parameter
// TODO it's a write.
// Type specifies whether an operation is a read, write, or unknown.
type Type uint
// THese are the availables types of Type.
const (
_ Type = iota
Write
Read
)
// RetryMode specifies the way that retries are handled for retryable operations.
type RetryMode uint
// These are the modes available for retrying.
const (
// RetryNone disables retrying.
RetryNone RetryMode = iota
// RetryOnce will enable retrying the entire operation once.
RetryOnce
// RetryOncePerCommand will enable retrying each command associated with an operation. For
// example, if an insert is batch split into 4 commands then each of those commands is eligible
// for one retry.
RetryOncePerCommand
// RetryContext will enable retrying until the context.Context's deadline is exceeded or it is
// cancelled.
RetryContext
)
// Enabled returns if this RetryMode enables retrying.
func (rm RetryMode) Enabled() bool {
return rm == RetryOnce || rm == RetryOncePerCommand || rm == RetryContext
}
+504
View File
@@ -0,0 +1,504 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver
import (
"bytes"
"errors"
"fmt"
"strings"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/internal"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
var (
retryableCodes = []int32{11600, 11602, 10107, 13435, 13436, 189, 91, 7, 6, 89, 9001, 262}
nodeIsRecoveringCodes = []int32{11600, 11602, 13436, 189, 91}
notPrimaryCodes = []int32{10107, 13435, 10058}
nodeIsShuttingDownCodes = []int32{11600, 91}
unknownReplWriteConcernCode = int32(79)
unsatisfiableWriteConcernCode = int32(100)
)
var (
// UnknownTransactionCommitResult is an error label for unknown transaction commit results.
UnknownTransactionCommitResult = "UnknownTransactionCommitResult"
// TransientTransactionError is an error label for transient errors with transactions.
TransientTransactionError = "TransientTransactionError"
// NetworkError is an error label for network errors.
NetworkError = "NetworkError"
// RetryableWriteError is an error lable for retryable write errors.
RetryableWriteError = "RetryableWriteError"
// ErrCursorNotFound is the cursor not found error for legacy find operations.
ErrCursorNotFound = errors.New("cursor not found")
// ErrUnacknowledgedWrite is returned from functions that have an unacknowledged
// write concern.
ErrUnacknowledgedWrite = errors.New("unacknowledged write")
// ErrUnsupportedStorageEngine is returned when a retryable write is attempted against a server
// that uses a storage engine that does not support retryable writes
ErrUnsupportedStorageEngine = errors.New("this MongoDB deployment does not support retryable writes. Please add retryWrites=false to your connection string")
// ErrDeadlineWouldBeExceeded is returned when a Timeout set on an operation would be exceeded
// if the operation were sent to the server.
ErrDeadlineWouldBeExceeded = errors.New("operation not sent to server, as Timeout would be exceeded")
)
// QueryFailureError is an error representing a command failure as a document.
type QueryFailureError struct {
Message string
Response bsoncore.Document
Wrapped error
}
// Error implements the error interface.
func (e QueryFailureError) Error() string {
return fmt.Sprintf("%s: %v", e.Message, e.Response)
}
// Unwrap returns the underlying error.
func (e QueryFailureError) Unwrap() error {
return e.Wrapped
}
// ResponseError is an error parsing the response to a command.
type ResponseError struct {
Message string
Wrapped error
}
// NewCommandResponseError creates a CommandResponseError.
func NewCommandResponseError(msg string, err error) ResponseError {
return ResponseError{Message: msg, Wrapped: err}
}
// Error implements the error interface.
func (e ResponseError) Error() string {
if e.Wrapped != nil {
return fmt.Sprintf("%s: %s", e.Message, e.Wrapped)
}
return e.Message
}
// WriteCommandError is an error for a write command.
type WriteCommandError struct {
WriteConcernError *WriteConcernError
WriteErrors WriteErrors
Labels []string
Raw bsoncore.Document
}
// UnsupportedStorageEngine returns whether or not the WriteCommandError comes from a retryable write being attempted
// against a server that has a storage engine where they are not supported
func (wce WriteCommandError) UnsupportedStorageEngine() bool {
for _, writeError := range wce.WriteErrors {
if writeError.Code == 20 && strings.HasPrefix(strings.ToLower(writeError.Message), "transaction numbers") {
return true
}
}
return false
}
func (wce WriteCommandError) Error() string {
var buf bytes.Buffer
fmt.Fprint(&buf, "write command error: [")
fmt.Fprintf(&buf, "{%s}, ", wce.WriteErrors)
fmt.Fprintf(&buf, "{%s}]", wce.WriteConcernError)
return buf.String()
}
// Retryable returns true if the error is retryable
func (wce WriteCommandError) Retryable(wireVersion *description.VersionRange) bool {
for _, label := range wce.Labels {
if label == RetryableWriteError {
return true
}
}
if wireVersion != nil && wireVersion.Max >= 9 {
return false
}
if wce.WriteConcernError == nil {
return false
}
return (*wce.WriteConcernError).Retryable()
}
// WriteConcernError is a write concern failure that occurred as a result of a
// write operation.
type WriteConcernError struct {
Name string
Code int64
Message string
Details bsoncore.Document
Labels []string
TopologyVersion *description.TopologyVersion
Raw bsoncore.Document
}
func (wce WriteConcernError) Error() string {
if wce.Name != "" {
return fmt.Sprintf("(%v) %v", wce.Name, wce.Message)
}
return wce.Message
}
// Retryable returns true if the error is retryable
func (wce WriteConcernError) Retryable() bool {
for _, code := range retryableCodes {
if wce.Code == int64(code) {
return true
}
}
return false
}
// NodeIsRecovering returns true if this error is a node is recovering error.
func (wce WriteConcernError) NodeIsRecovering() bool {
for _, code := range nodeIsRecoveringCodes {
if wce.Code == int64(code) {
return true
}
}
hasNoCode := wce.Code == 0
return hasNoCode && strings.Contains(wce.Message, "node is recovering")
}
// NodeIsShuttingDown returns true if this error is a node is shutting down error.
func (wce WriteConcernError) NodeIsShuttingDown() bool {
for _, code := range nodeIsShuttingDownCodes {
if wce.Code == int64(code) {
return true
}
}
hasNoCode := wce.Code == 0
return hasNoCode && strings.Contains(wce.Message, "node is shutting down")
}
// NotPrimary returns true if this error is a not primary error.
func (wce WriteConcernError) NotPrimary() bool {
for _, code := range notPrimaryCodes {
if wce.Code == int64(code) {
return true
}
}
hasNoCode := wce.Code == 0
return hasNoCode && strings.Contains(wce.Message, internal.LegacyNotPrimary)
}
// WriteError is a non-write concern failure that occurred as a result of a write
// operation.
type WriteError struct {
Index int64
Code int64
Message string
Details bsoncore.Document
Raw bsoncore.Document
}
func (we WriteError) Error() string { return we.Message }
// WriteErrors is a group of non-write concern failures that occurred as a result
// of a write operation.
type WriteErrors []WriteError
func (we WriteErrors) Error() string {
var buf bytes.Buffer
fmt.Fprint(&buf, "write errors: [")
for idx, err := range we {
if idx != 0 {
fmt.Fprintf(&buf, ", ")
}
fmt.Fprintf(&buf, "{%s}", err)
}
fmt.Fprint(&buf, "]")
return buf.String()
}
// Error is a command execution error from the database.
type Error struct {
Code int32
Message string
Labels []string
Name string
Wrapped error
TopologyVersion *description.TopologyVersion
Raw bsoncore.Document
}
// UnsupportedStorageEngine returns whether e came as a result of an unsupported storage engine
func (e Error) UnsupportedStorageEngine() bool {
return e.Code == 20 && strings.HasPrefix(strings.ToLower(e.Message), "transaction numbers")
}
// Error implements the error interface.
func (e Error) Error() string {
if e.Name != "" {
return fmt.Sprintf("(%v) %v", e.Name, e.Message)
}
return e.Message
}
// Unwrap returns the underlying error.
func (e Error) Unwrap() error {
return e.Wrapped
}
// HasErrorLabel returns true if the error contains the specified label.
func (e Error) HasErrorLabel(label string) bool {
if e.Labels != nil {
for _, l := range e.Labels {
if l == label {
return true
}
}
}
return false
}
// RetryableRead returns true if the error is retryable for a read operation
func (e Error) RetryableRead() bool {
for _, label := range e.Labels {
if label == NetworkError {
return true
}
}
for _, code := range retryableCodes {
if e.Code == code {
return true
}
}
return false
}
// RetryableWrite returns true if the error is retryable for a write operation
func (e Error) RetryableWrite(wireVersion *description.VersionRange) bool {
for _, label := range e.Labels {
if label == NetworkError || label == RetryableWriteError {
return true
}
}
if wireVersion != nil && wireVersion.Max >= 9 {
return false
}
for _, code := range retryableCodes {
if e.Code == code {
return true
}
}
return false
}
// NetworkError returns true if the error is a network error.
func (e Error) NetworkError() bool {
for _, label := range e.Labels {
if label == NetworkError {
return true
}
}
return false
}
// NodeIsRecovering returns true if this error is a node is recovering error.
func (e Error) NodeIsRecovering() bool {
for _, code := range nodeIsRecoveringCodes {
if e.Code == code {
return true
}
}
hasNoCode := e.Code == 0
return hasNoCode && strings.Contains(e.Message, "node is recovering")
}
// NodeIsShuttingDown returns true if this error is a node is shutting down error.
func (e Error) NodeIsShuttingDown() bool {
for _, code := range nodeIsShuttingDownCodes {
if e.Code == code {
return true
}
}
hasNoCode := e.Code == 0
return hasNoCode && strings.Contains(e.Message, "node is shutting down")
}
// NotPrimary returns true if this error is a not primary error.
func (e Error) NotPrimary() bool {
for _, code := range notPrimaryCodes {
if e.Code == code {
return true
}
}
hasNoCode := e.Code == 0
return hasNoCode && strings.Contains(e.Message, internal.LegacyNotPrimary)
}
// NamespaceNotFound returns true if this errors is a NamespaceNotFound error.
func (e Error) NamespaceNotFound() bool {
return e.Code == 26 || e.Message == "ns not found"
}
// ExtractErrorFromServerResponse extracts an error from a server response bsoncore.Document
// if there is one. Also used in testing for SDAM.
func ExtractErrorFromServerResponse(doc bsoncore.Document) error {
var errmsg, codeName string
var code int32
var labels []string
var ok bool
var tv *description.TopologyVersion
var wcError WriteCommandError
elems, err := doc.Elements()
if err != nil {
return err
}
for _, elem := range elems {
switch elem.Key() {
case "ok":
switch elem.Value().Type {
case bson.TypeInt32:
if elem.Value().Int32() == 1 {
ok = true
}
case bson.TypeInt64:
if elem.Value().Int64() == 1 {
ok = true
}
case bson.TypeDouble:
if elem.Value().Double() == 1 {
ok = true
}
}
case "errmsg":
if str, okay := elem.Value().StringValueOK(); okay {
errmsg = str
}
case "codeName":
if str, okay := elem.Value().StringValueOK(); okay {
codeName = str
}
case "code":
if c, okay := elem.Value().Int32OK(); okay {
code = c
}
case "errorLabels":
if arr, okay := elem.Value().ArrayOK(); okay {
vals, err := arr.Values()
if err != nil {
continue
}
for _, val := range vals {
if str, ok := val.StringValueOK(); ok {
labels = append(labels, str)
}
}
}
case "writeErrors":
arr, exists := elem.Value().ArrayOK()
if !exists {
break
}
vals, err := arr.Values()
if err != nil {
continue
}
for _, val := range vals {
var we WriteError
doc, exists := val.DocumentOK()
if !exists {
continue
}
if index, exists := doc.Lookup("index").AsInt64OK(); exists {
we.Index = index
}
if code, exists := doc.Lookup("code").AsInt64OK(); exists {
we.Code = code
}
if msg, exists := doc.Lookup("errmsg").StringValueOK(); exists {
we.Message = msg
}
if info, exists := doc.Lookup("errInfo").DocumentOK(); exists {
we.Details = make([]byte, len(info))
copy(we.Details, info)
}
we.Raw = doc
wcError.WriteErrors = append(wcError.WriteErrors, we)
}
case "writeConcernError":
doc, exists := elem.Value().DocumentOK()
if !exists {
break
}
wcError.WriteConcernError = new(WriteConcernError)
wcError.WriteConcernError.Raw = doc
if code, exists := doc.Lookup("code").AsInt64OK(); exists {
wcError.WriteConcernError.Code = code
}
if name, exists := doc.Lookup("codeName").StringValueOK(); exists {
wcError.WriteConcernError.Name = name
}
if msg, exists := doc.Lookup("errmsg").StringValueOK(); exists {
wcError.WriteConcernError.Message = msg
}
if info, exists := doc.Lookup("errInfo").DocumentOK(); exists {
wcError.WriteConcernError.Details = make([]byte, len(info))
copy(wcError.WriteConcernError.Details, info)
}
if errLabels, exists := doc.Lookup("errorLabels").ArrayOK(); exists {
vals, err := errLabels.Values()
if err != nil {
continue
}
for _, val := range vals {
if str, ok := val.StringValueOK(); ok {
labels = append(labels, str)
}
}
}
case "topologyVersion":
doc, ok := elem.Value().DocumentOK()
if !ok {
break
}
version, err := description.NewTopologyVersion(bson.Raw(doc))
if err == nil {
tv = version
}
}
}
if !ok {
if errmsg == "" {
errmsg = "command failed"
}
return Error{
Code: code,
Message: errmsg,
Name: codeName,
Labels: labels,
TopologyVersion: tv,
Raw: doc,
}
}
if len(wcError.WriteErrors) > 0 || wcError.WriteConcernError != nil {
wcError.Labels = labels
if wcError.WriteConcernError != nil {
wcError.WriteConcernError.TopologyVersion = tv
}
wcError.Raw = doc
return wcError
}
return nil
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver
// LegacyOperationKind indicates if an operation is a legacy find, getMore, or killCursors. This is used
// in Operation.Execute, which will create legacy OP_QUERY, OP_GET_MORE, or OP_KILL_CURSORS instead
// of sending them as a command.
type LegacyOperationKind uint
// These constants represent the three different kinds of legacy operations.
const (
LegacyNone LegacyOperationKind = iota
LegacyFind
LegacyGetMore
LegacyKillCursors
LegacyListCollections
LegacyListIndexes
)
@@ -0,0 +1,129 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver
import (
"context"
"errors"
"io"
"strings"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// ListCollectionsBatchCursor is a special batch cursor returned from ListCollections that properly
// handles current and legacy ListCollections operations.
type ListCollectionsBatchCursor struct {
legacy bool // server version < 3.0
bc *BatchCursor
currentBatch *bsoncore.DocumentSequence
err error
}
// NewListCollectionsBatchCursor creates a new non-legacy ListCollectionsCursor.
func NewListCollectionsBatchCursor(bc *BatchCursor) (*ListCollectionsBatchCursor, error) {
if bc == nil {
return nil, errors.New("batch cursor must not be nil")
}
return &ListCollectionsBatchCursor{bc: bc, currentBatch: new(bsoncore.DocumentSequence)}, nil
}
// NewLegacyListCollectionsBatchCursor creates a new legacy ListCollectionsCursor.
func NewLegacyListCollectionsBatchCursor(bc *BatchCursor) (*ListCollectionsBatchCursor, error) {
if bc == nil {
return nil, errors.New("batch cursor must not be nil")
}
return &ListCollectionsBatchCursor{legacy: true, bc: bc, currentBatch: new(bsoncore.DocumentSequence)}, nil
}
// ID returns the cursor ID for this batch cursor.
func (lcbc *ListCollectionsBatchCursor) ID() int64 {
return lcbc.bc.ID()
}
// Next indicates if there is another batch available. Returning false does not necessarily indicate
// that the cursor is closed. This method will return false when an empty batch is returned.
//
// If Next returns true, there is a valid batch of documents available. If Next returns false, there
// is not a valid batch of documents available.
func (lcbc *ListCollectionsBatchCursor) Next(ctx context.Context) bool {
if !lcbc.bc.Next(ctx) {
return false
}
if !lcbc.legacy {
lcbc.currentBatch.Style = lcbc.bc.currentBatch.Style
lcbc.currentBatch.Data = lcbc.bc.currentBatch.Data
lcbc.currentBatch.ResetIterator()
return true
}
lcbc.currentBatch.Style = bsoncore.SequenceStyle
lcbc.currentBatch.Data = lcbc.currentBatch.Data[:0]
var doc bsoncore.Document
for {
doc, lcbc.err = lcbc.bc.currentBatch.Next()
if lcbc.err != nil {
if lcbc.err == io.EOF {
lcbc.err = nil
break
}
return false
}
doc, lcbc.err = lcbc.projectNameElement(doc)
if lcbc.err != nil {
return false
}
lcbc.currentBatch.Data = append(lcbc.currentBatch.Data, doc...)
}
return true
}
// Batch will return a DocumentSequence for the current batch of documents. The returned
// DocumentSequence is only valid until the next call to Next or Close.
func (lcbc *ListCollectionsBatchCursor) Batch() *bsoncore.DocumentSequence { return lcbc.currentBatch }
// Server returns a pointer to the cursor's server.
func (lcbc *ListCollectionsBatchCursor) Server() Server { return lcbc.bc.server }
// Err returns the latest error encountered.
func (lcbc *ListCollectionsBatchCursor) Err() error {
if lcbc.err != nil {
return lcbc.err
}
return lcbc.bc.Err()
}
// Close closes this batch cursor.
func (lcbc *ListCollectionsBatchCursor) Close(ctx context.Context) error { return lcbc.bc.Close(ctx) }
// project out the database name for a legacy server
func (*ListCollectionsBatchCursor) projectNameElement(rawDoc bsoncore.Document) (bsoncore.Document, error) {
elems, err := rawDoc.Elements()
if err != nil {
return nil, err
}
var filteredElems []byte
for _, elem := range elems {
key := elem.Key()
if key != "name" {
filteredElems = append(filteredElems, elem...)
continue
}
name := elem.Value().StringValue()
collName := name[strings.Index(name, ".")+1:]
filteredElems = bsoncore.AppendStringElement(filteredElems, "name", collName)
}
var filteredDoc []byte
filteredDoc = bsoncore.BuildDocument(filteredDoc, filteredElems)
return filteredDoc, nil
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build cse
// +build cse
package mongocrypt
// #include <mongocrypt.h>
import "C"
import (
"unsafe"
)
// binary is a wrapper type around a mongocrypt_binary_t*
type binary struct {
wrapped *C.mongocrypt_binary_t
}
// newBinary creates an empty binary instance.
func newBinary() *binary {
return &binary{
wrapped: C.mongocrypt_binary_new(),
}
}
// newBinaryFromBytes creates a binary instance from a byte buffer.
func newBinaryFromBytes(data []byte) *binary {
if len(data) == 0 {
return newBinary()
}
// We don't need C.CBytes here because data cannot go out of scope. Any mongocrypt function that takes a
// mongocrypt_binary_t will make a copy of the data so the data can be garbage collected after calling.
addr := (*C.uint8_t)(unsafe.Pointer(&data[0])) // uint8_t*
dataLen := C.uint32_t(len(data)) // uint32_t
return &binary{
wrapped: C.mongocrypt_binary_new_from_data(addr, dataLen),
}
}
// toBytes converts the given binary instance to []byte.
func (b *binary) toBytes() []byte {
dataPtr := C.mongocrypt_binary_data(b.wrapped) // C.uint8_t*
dataLen := C.mongocrypt_binary_len(b.wrapped) // C.uint32_t
return C.GoBytes(unsafe.Pointer(dataPtr), C.int(dataLen))
}
// close cleans up any resources associated with the given binary instance.
func (b *binary) close() {
C.mongocrypt_binary_destroy(b.wrapped)
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build cse
// +build cse
package mongocrypt
// #include <mongocrypt.h>
import "C"
import (
"fmt"
)
// Error represents an error from an operation on a MongoCrypt instance.
type Error struct {
Code int32
Message string
}
// Error implements the error interface.
func (e Error) Error() string {
return fmt.Sprintf("mongocrypt error %d: %v", e.Code, e.Message)
}
// errorFromStatus builds a Error from a mongocrypt_status_t object.
func errorFromStatus(status *C.mongocrypt_status_t) error {
cCode := C.mongocrypt_status_code(status) // uint32_t
// mongocrypt_status_message takes uint32_t* as its second param to store the length of the returned string.
// pass nil because the length is handled by C.GoString
cMsg := C.mongocrypt_status_message(status, nil) // const char*
var msg string
if cMsg != nil {
msg = C.GoString(cMsg)
}
return Error{
Code: int32(cCode),
Message: msg,
}
}
@@ -0,0 +1,21 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build !cse
// +build !cse
package mongocrypt
// Error represents an error from an operation on a MongoCrypt instance.
type Error struct {
Code int32
Message string
}
// Error implements the error interface
func (Error) Error() string {
panic(cseNotSupportedMsg)
}
@@ -0,0 +1,407 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build cse
// +build cse
package mongocrypt
// #cgo linux solaris darwin pkg-config: libmongocrypt
// #cgo windows CFLAGS: -I"c:/libmongocrypt/include"
// #cgo windows LDFLAGS: -lmongocrypt -Lc:/libmongocrypt/bin
// #include <mongocrypt.h>
// #include <stdlib.h>
import "C"
import (
"errors"
"fmt"
"unsafe"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver/mongocrypt/options"
)
type MongoCrypt struct {
wrapped *C.mongocrypt_t
}
// Version returns the version string for the loaded libmongocrypt, or an empty string
// if libmongocrypt was not loaded.
func Version() string {
str := C.GoString(C.mongocrypt_version(nil))
return str
}
// NewMongoCrypt constructs a new MongoCrypt instance configured using the provided MongoCryptOptions.
func NewMongoCrypt(opts *options.MongoCryptOptions) (*MongoCrypt, error) {
// create mongocrypt_t handle
wrapped := C.mongocrypt_new()
if wrapped == nil {
return nil, errors.New("could not create new mongocrypt object")
}
crypt := &MongoCrypt{
wrapped: wrapped,
}
// set options in mongocrypt
if err := crypt.setProviderOptions(opts.KmsProviders); err != nil {
return nil, err
}
if err := crypt.setLocalSchemaMap(opts.LocalSchemaMap); err != nil {
return nil, err
}
if err := crypt.setEncryptedFieldsMap(opts.EncryptedFieldsMap); err != nil {
return nil, err
}
if opts.BypassQueryAnalysis {
C.mongocrypt_setopt_bypass_query_analysis(wrapped)
}
// If loading the crypt_shared library isn't disabled, set the default library search path "$SYSTEM"
// and set a library override path if one was provided.
if !opts.CryptSharedLibDisabled {
systemStr := C.CString("$SYSTEM")
defer C.free(unsafe.Pointer(systemStr))
C.mongocrypt_setopt_append_crypt_shared_lib_search_path(crypt.wrapped, systemStr)
if opts.CryptSharedLibOverridePath != "" {
cryptSharedLibOverridePathStr := C.CString(opts.CryptSharedLibOverridePath)
defer C.free(unsafe.Pointer(cryptSharedLibOverridePathStr))
C.mongocrypt_setopt_set_crypt_shared_lib_path_override(crypt.wrapped, cryptSharedLibOverridePathStr)
}
}
// initialize handle
if !C.mongocrypt_init(crypt.wrapped) {
return nil, crypt.createErrorFromStatus()
}
return crypt, nil
}
// CreateEncryptionContext creates a Context to use for encryption.
func (m *MongoCrypt) CreateEncryptionContext(db string, cmd bsoncore.Document) (*Context, error) {
ctx := newContext(C.mongocrypt_ctx_new(m.wrapped))
if ctx.wrapped == nil {
return nil, m.createErrorFromStatus()
}
cmdBinary := newBinaryFromBytes(cmd)
defer cmdBinary.close()
dbStr := C.CString(db)
defer C.free(unsafe.Pointer(dbStr))
if ok := C.mongocrypt_ctx_encrypt_init(ctx.wrapped, dbStr, C.int32_t(-1), cmdBinary.wrapped); !ok {
return nil, ctx.createErrorFromStatus()
}
return ctx, nil
}
// CreateDecryptionContext creates a Context to use for decryption.
func (m *MongoCrypt) CreateDecryptionContext(cmd bsoncore.Document) (*Context, error) {
ctx := newContext(C.mongocrypt_ctx_new(m.wrapped))
if ctx.wrapped == nil {
return nil, m.createErrorFromStatus()
}
cmdBinary := newBinaryFromBytes(cmd)
defer cmdBinary.close()
if ok := C.mongocrypt_ctx_decrypt_init(ctx.wrapped, cmdBinary.wrapped); !ok {
return nil, ctx.createErrorFromStatus()
}
return ctx, nil
}
// lookupString returns a string for the value corresponding to the given key in the document.
// if the key does not exist or the value is not a string, the empty string is returned.
func lookupString(doc bsoncore.Document, key string) string {
strVal, _ := doc.Lookup(key).StringValueOK()
return strVal
}
func setAltName(ctx *Context, altName string) error {
// create document {"keyAltName": keyAltName}
idx, doc := bsoncore.AppendDocumentStart(nil)
doc = bsoncore.AppendStringElement(doc, "keyAltName", altName)
doc, _ = bsoncore.AppendDocumentEnd(doc, idx)
keyAltBinary := newBinaryFromBytes(doc)
defer keyAltBinary.close()
if ok := C.mongocrypt_ctx_setopt_key_alt_name(ctx.wrapped, keyAltBinary.wrapped); !ok {
return ctx.createErrorFromStatus()
}
return nil
}
func setKeyMaterial(ctx *Context, keyMaterial []byte) error {
// Create document {"keyMaterial": keyMaterial} using the generic binary sybtype 0x00.
idx, doc := bsoncore.AppendDocumentStart(nil)
doc = bsoncore.AppendBinaryElement(doc, "keyMaterial", 0x00, keyMaterial)
doc, err := bsoncore.AppendDocumentEnd(doc, idx)
if err != nil {
return err
}
keyMaterialBinary := newBinaryFromBytes(doc)
defer keyMaterialBinary.close()
if ok := C.mongocrypt_ctx_setopt_key_material(ctx.wrapped, keyMaterialBinary.wrapped); !ok {
return ctx.createErrorFromStatus()
}
return nil
}
func rewrapDataKey(ctx *Context, filter []byte) error {
filterBinary := newBinaryFromBytes(filter)
defer filterBinary.close()
if ok := C.mongocrypt_ctx_rewrap_many_datakey_init(ctx.wrapped, filterBinary.wrapped); !ok {
return ctx.createErrorFromStatus()
}
return nil
}
// CreateDataKeyContext creates a Context to use for creating a data key.
func (m *MongoCrypt) CreateDataKeyContext(kmsProvider string, opts *options.DataKeyOptions) (*Context, error) {
ctx := newContext(C.mongocrypt_ctx_new(m.wrapped))
if ctx.wrapped == nil {
return nil, m.createErrorFromStatus()
}
// Create a masterKey document of the form { "provider": <provider string>, other options... }.
var masterKey bsoncore.Document
switch {
case opts.MasterKey != nil:
// The original key passed into the top-level API was already transformed into a raw BSON document and passed
// down to here, so we can modify it without copying. Remove the terminating byte to add the "provider" field.
masterKey = opts.MasterKey[:len(opts.MasterKey)-1]
masterKey = bsoncore.AppendStringElement(masterKey, "provider", kmsProvider)
masterKey, _ = bsoncore.AppendDocumentEnd(masterKey, 0)
default:
masterKey = bsoncore.NewDocumentBuilder().AppendString("provider", kmsProvider).Build()
}
masterKeyBinary := newBinaryFromBytes(masterKey)
defer masterKeyBinary.close()
if ok := C.mongocrypt_ctx_setopt_key_encryption_key(ctx.wrapped, masterKeyBinary.wrapped); !ok {
return nil, ctx.createErrorFromStatus()
}
for _, altName := range opts.KeyAltNames {
if err := setAltName(ctx, altName); err != nil {
return nil, err
}
}
if opts.KeyMaterial != nil {
if err := setKeyMaterial(ctx, opts.KeyMaterial); err != nil {
return nil, err
}
}
if ok := C.mongocrypt_ctx_datakey_init(ctx.wrapped); !ok {
return nil, ctx.createErrorFromStatus()
}
return ctx, nil
}
const (
IndexTypeUnindexed = 1
IndexTypeIndexed = 2
)
// CreateExplicitEncryptionContext creates a Context to use for explicit encryption.
func (m *MongoCrypt) CreateExplicitEncryptionContext(doc bsoncore.Document, opts *options.ExplicitEncryptionOptions) (*Context, error) {
ctx := newContext(C.mongocrypt_ctx_new(m.wrapped))
if ctx.wrapped == nil {
return nil, m.createErrorFromStatus()
}
if opts.KeyID != nil {
keyIDBinary := newBinaryFromBytes(opts.KeyID.Data)
defer keyIDBinary.close()
if ok := C.mongocrypt_ctx_setopt_key_id(ctx.wrapped, keyIDBinary.wrapped); !ok {
return nil, ctx.createErrorFromStatus()
}
}
if opts.KeyAltName != nil {
if err := setAltName(ctx, *opts.KeyAltName); err != nil {
return nil, err
}
}
algoStr := C.CString(opts.Algorithm)
defer C.free(unsafe.Pointer(algoStr))
if ok := C.mongocrypt_ctx_setopt_algorithm(ctx.wrapped, algoStr, -1); !ok {
return nil, ctx.createErrorFromStatus()
}
if opts.QueryType != "" {
queryStr := C.CString(opts.QueryType)
defer C.free(unsafe.Pointer(queryStr))
if ok := C.mongocrypt_ctx_setopt_query_type(ctx.wrapped, queryStr, -1); !ok {
return nil, ctx.createErrorFromStatus()
}
}
if opts.ContentionFactor != nil {
if ok := C.mongocrypt_ctx_setopt_contention_factor(ctx.wrapped, C.int64_t(*opts.ContentionFactor)); !ok {
return nil, ctx.createErrorFromStatus()
}
}
docBinary := newBinaryFromBytes(doc)
defer docBinary.close()
if ok := C.mongocrypt_ctx_explicit_encrypt_init(ctx.wrapped, docBinary.wrapped); !ok {
return nil, ctx.createErrorFromStatus()
}
return ctx, nil
}
// CreateExplicitDecryptionContext creates a Context to use for explicit decryption.
func (m *MongoCrypt) CreateExplicitDecryptionContext(doc bsoncore.Document) (*Context, error) {
ctx := newContext(C.mongocrypt_ctx_new(m.wrapped))
if ctx.wrapped == nil {
return nil, m.createErrorFromStatus()
}
docBinary := newBinaryFromBytes(doc)
defer docBinary.close()
if ok := C.mongocrypt_ctx_explicit_decrypt_init(ctx.wrapped, docBinary.wrapped); !ok {
return nil, ctx.createErrorFromStatus()
}
return ctx, nil
}
// CryptSharedLibVersion returns the version number for the loaded crypt_shared library, or 0 if the
// crypt_shared library was not loaded.
func (m *MongoCrypt) CryptSharedLibVersion() uint64 {
return uint64(C.mongocrypt_crypt_shared_lib_version(m.wrapped))
}
// CryptSharedLibVersionString returns the version string for the loaded crypt_shared library, or an
// empty string if the crypt_shared library was not loaded.
func (m *MongoCrypt) CryptSharedLibVersionString() string {
// Pass in a pointer for "len", but ignore the value because C.GoString can determine the string
// length without it.
len := C.uint(0)
str := C.GoString(C.mongocrypt_crypt_shared_lib_version_string(m.wrapped, &len))
return str
}
// Close cleans up any resources associated with the given MongoCrypt instance.
func (m *MongoCrypt) Close() {
C.mongocrypt_destroy(m.wrapped)
}
// RewrapDataKeyContext create a Context to use for rewrapping a data key.
func (m *MongoCrypt) RewrapDataKeyContext(filter []byte, opts *options.RewrapManyDataKeyOptions) (*Context, error) {
const masterKey = "masterKey"
const providerKey = "provider"
ctx := newContext(C.mongocrypt_ctx_new(m.wrapped))
if ctx.wrapped == nil {
return nil, m.createErrorFromStatus()
}
if opts.Provider != nil {
// If a provider has been specified, create an encryption key document for creating a data key or for rewrapping
// datakeys. If a new provider is not specified, then the filter portion of this logic returns the data as it
// exists in the collection.
idx, mongocryptDoc := bsoncore.AppendDocumentStart(nil)
mongocryptDoc = bsoncore.AppendStringElement(mongocryptDoc, providerKey, *opts.Provider)
if opts.MasterKey != nil {
mongocryptDoc = opts.MasterKey[:len(opts.MasterKey)-1]
mongocryptDoc = bsoncore.AppendStringElement(mongocryptDoc, providerKey, *opts.Provider)
}
mongocryptDoc, err := bsoncore.AppendDocumentEnd(mongocryptDoc, idx)
if err != nil {
return nil, err
}
mongocryptBinary := newBinaryFromBytes(mongocryptDoc)
defer mongocryptBinary.close()
// Add new masterKey to the mongocrypt context.
if ok := C.mongocrypt_ctx_setopt_key_encryption_key(ctx.wrapped, mongocryptBinary.wrapped); !ok {
return nil, ctx.createErrorFromStatus()
}
}
return ctx, rewrapDataKey(ctx, filter)
}
func (m *MongoCrypt) setProviderOptions(kmsProviders bsoncore.Document) error {
providersBinary := newBinaryFromBytes(kmsProviders)
defer providersBinary.close()
if ok := C.mongocrypt_setopt_kms_providers(m.wrapped, providersBinary.wrapped); !ok {
return m.createErrorFromStatus()
}
return nil
}
// setLocalSchemaMap sets the local schema map in mongocrypt.
func (m *MongoCrypt) setLocalSchemaMap(schemaMap map[string]bsoncore.Document) error {
if len(schemaMap) == 0 {
return nil
}
// convert schema map to BSON document
schemaMapBSON, err := bson.Marshal(schemaMap)
if err != nil {
return fmt.Errorf("error marshalling SchemaMap: %v", err)
}
schemaMapBinary := newBinaryFromBytes(schemaMapBSON)
defer schemaMapBinary.close()
if ok := C.mongocrypt_setopt_schema_map(m.wrapped, schemaMapBinary.wrapped); !ok {
return m.createErrorFromStatus()
}
return nil
}
// setEncryptedFieldsMap sets the encryptedfields map in mongocrypt.
func (m *MongoCrypt) setEncryptedFieldsMap(encryptedfieldsMap map[string]bsoncore.Document) error {
if len(encryptedfieldsMap) == 0 {
return nil
}
// convert encryptedfields map to BSON document
encryptedfieldsMapBSON, err := bson.Marshal(encryptedfieldsMap)
if err != nil {
return fmt.Errorf("error marshalling EncryptedFieldsMap: %v", err)
}
encryptedfieldsMapBinary := newBinaryFromBytes(encryptedfieldsMapBSON)
defer encryptedfieldsMapBinary.close()
if ok := C.mongocrypt_setopt_encrypted_field_config_map(m.wrapped, encryptedfieldsMapBinary.wrapped); !ok {
return m.createErrorFromStatus()
}
return nil
}
// createErrorFromStatus creates a new Error based on the status of the MongoCrypt instance.
func (m *MongoCrypt) createErrorFromStatus() error {
status := C.mongocrypt_status_new()
defer C.mongocrypt_status_destroy(status)
C.mongocrypt_status(m.wrapped, status)
return errorFromStatus(status)
}
@@ -0,0 +1,104 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build cse
// +build cse
package mongocrypt
// #include <mongocrypt.h>
import "C"
import (
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// Context represents a mongocrypt_ctx_t handle
type Context struct {
wrapped *C.mongocrypt_ctx_t
}
// newContext creates a Context wrapper around the given C type.
func newContext(wrapped *C.mongocrypt_ctx_t) *Context {
return &Context{
wrapped: wrapped,
}
}
// State returns the current State of the Context.
func (c *Context) State() State {
return State(int(C.mongocrypt_ctx_state(c.wrapped)))
}
// NextOperation gets the document for the next database operation to run.
func (c *Context) NextOperation() (bsoncore.Document, error) {
opDocBinary := newBinary() // out param for mongocrypt_ctx_mongo_op to fill in operation
defer opDocBinary.close()
if ok := C.mongocrypt_ctx_mongo_op(c.wrapped, opDocBinary.wrapped); !ok {
return nil, c.createErrorFromStatus()
}
return opDocBinary.toBytes(), nil
}
// AddOperationResult feeds the result of a database operation to mongocrypt.
func (c *Context) AddOperationResult(result bsoncore.Document) error {
resultBinary := newBinaryFromBytes(result)
defer resultBinary.close()
if ok := C.mongocrypt_ctx_mongo_feed(c.wrapped, resultBinary.wrapped); !ok {
return c.createErrorFromStatus()
}
return nil
}
// CompleteOperation signals a database operation has been completed.
func (c *Context) CompleteOperation() error {
if ok := C.mongocrypt_ctx_mongo_done(c.wrapped); !ok {
return c.createErrorFromStatus()
}
return nil
}
// NextKmsContext returns the next KmsContext, or nil if there are no more.
func (c *Context) NextKmsContext() *KmsContext {
ctx := C.mongocrypt_ctx_next_kms_ctx(c.wrapped)
if ctx == nil {
return nil
}
return newKmsContext(ctx)
}
// FinishKmsContexts signals that all KMS contexts have been completed.
func (c *Context) FinishKmsContexts() error {
if ok := C.mongocrypt_ctx_kms_done(c.wrapped); !ok {
return c.createErrorFromStatus()
}
return nil
}
// Finish performs the final operations for the context and returns the resulting document.
func (c *Context) Finish() (bsoncore.Document, error) {
docBinary := newBinary() // out param for mongocrypt_ctx_finalize to fill in resulting document
defer docBinary.close()
if ok := C.mongocrypt_ctx_finalize(c.wrapped, docBinary.wrapped); !ok {
return nil, c.createErrorFromStatus()
}
return docBinary.toBytes(), nil
}
// Close cleans up any resources associated with the given Context instance.
func (c *Context) Close() {
C.mongocrypt_ctx_destroy(c.wrapped)
}
// createErrorFromStatus creates a new Error based on the status of the MongoCrypt instance.
func (c *Context) createErrorFromStatus() error {
status := C.mongocrypt_status_new()
defer C.mongocrypt_status_destroy(status)
C.mongocrypt_ctx_status(c.wrapped, status)
return errorFromStatus(status)
}
@@ -0,0 +1,57 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build !cse
// +build !cse
package mongocrypt
import (
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// Context represents a mongocrypt_ctx_t handle
type Context struct{}
// State returns the current State of the Context.
func (c *Context) State() State {
panic(cseNotSupportedMsg)
}
// NextOperation gets the document for the next database operation to run.
func (c *Context) NextOperation() (bsoncore.Document, error) {
panic(cseNotSupportedMsg)
}
// AddOperationResult feeds the result of a database operation to mongocrypt.
func (c *Context) AddOperationResult(result bsoncore.Document) error {
panic(cseNotSupportedMsg)
}
// CompleteOperation signals a database operation has been completed.
func (c *Context) CompleteOperation() error {
panic(cseNotSupportedMsg)
}
// NextKmsContext returns the next KmsContext, or nil if there are no more.
func (c *Context) NextKmsContext() *KmsContext {
panic(cseNotSupportedMsg)
}
// FinishKmsContexts signals that all KMS contexts have been completed.
func (c *Context) FinishKmsContexts() error {
panic(cseNotSupportedMsg)
}
// Finish performs the final operations for the context and returns the resulting document.
func (c *Context) Finish() (bsoncore.Document, error) {
panic(cseNotSupportedMsg)
}
// Close cleans up any resources associated with the given Context instance.
func (c *Context) Close() {
panic(cseNotSupportedMsg)
}
@@ -0,0 +1,76 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build cse
// +build cse
package mongocrypt
// #include <mongocrypt.h>
import "C"
// KmsContext represents a mongocrypt_kms_ctx_t handle.
type KmsContext struct {
wrapped *C.mongocrypt_kms_ctx_t
}
// newKmsContext creates a KmsContext wrapper around the given C type.
func newKmsContext(wrapped *C.mongocrypt_kms_ctx_t) *KmsContext {
return &KmsContext{
wrapped: wrapped,
}
}
// HostName gets the host name of the KMS.
func (kc *KmsContext) HostName() (string, error) {
var hostname *C.char // out param for mongocrypt function to fill in hostname
if ok := C.mongocrypt_kms_ctx_endpoint(kc.wrapped, &hostname); !ok {
return "", kc.createErrorFromStatus()
}
return C.GoString(hostname), nil
}
// KMSProvider gets the KMS provider of the KMS context.
func (kc *KmsContext) KMSProvider() string {
kmsProvider := C.mongocrypt_kms_ctx_get_kms_provider(kc.wrapped, nil)
return C.GoString(kmsProvider)
}
// Message returns the message to send to the KMS.
func (kc *KmsContext) Message() ([]byte, error) {
msgBinary := newBinary()
defer msgBinary.close()
if ok := C.mongocrypt_kms_ctx_message(kc.wrapped, msgBinary.wrapped); !ok {
return nil, kc.createErrorFromStatus()
}
return msgBinary.toBytes(), nil
}
// BytesNeeded returns the number of bytes that should be received from the KMS.
// After sending the message to the KMS, this message should be called in a loop until the number returned is 0.
func (kc *KmsContext) BytesNeeded() int32 {
return int32(C.mongocrypt_kms_ctx_bytes_needed(kc.wrapped))
}
// FeedResponse feeds the bytes received from the KMS to mongocrypt.
func (kc *KmsContext) FeedResponse(response []byte) error {
responseBinary := newBinaryFromBytes(response)
defer responseBinary.close()
if ok := C.mongocrypt_kms_ctx_feed(kc.wrapped, responseBinary.wrapped); !ok {
return kc.createErrorFromStatus()
}
return nil
}
// createErrorFromStatus creates a new Error from the status of the KmsContext instance.
func (kc *KmsContext) createErrorFromStatus() error {
status := C.mongocrypt_status_new()
defer C.mongocrypt_status_destroy(status)
C.mongocrypt_kms_ctx_status(kc.wrapped, status)
return errorFromStatus(status)
}
@@ -0,0 +1,39 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build !cse
// +build !cse
package mongocrypt
// KmsContext represents a mongocrypt_kms_ctx_t handle.
type KmsContext struct{}
// HostName gets the host name of the KMS.
func (kc *KmsContext) HostName() (string, error) {
panic(cseNotSupportedMsg)
}
// Message returns the message to send to the KMS.
func (kc *KmsContext) Message() ([]byte, error) {
panic(cseNotSupportedMsg)
}
// KMSProvider gets the KMS provider of the KMS context.
func (kc *KmsContext) KMSProvider() string {
panic(cseNotSupportedMsg)
}
// BytesNeeded returns the number of bytes that should be received from the KMS.
// After sending the message to the KMS, this message should be called in a loop until the number returned is 0.
func (kc *KmsContext) BytesNeeded() int32 {
panic(cseNotSupportedMsg)
}
// FeedResponse feeds the bytes received from the KMS to mongocrypt.
func (kc *KmsContext) FeedResponse(response []byte) error {
panic(cseNotSupportedMsg)
}
@@ -0,0 +1,78 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//go:build !cse
// +build !cse
package mongocrypt
import (
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver/mongocrypt/options"
)
const cseNotSupportedMsg = "client-side encryption not enabled. add the cse build tag to support"
// MongoCrypt represents a mongocrypt_t handle.
type MongoCrypt struct{}
// Version returns the version string for the loaded libmongocrypt, or an empty string
// if libmongocrypt was not loaded.
func Version() string {
return ""
}
// NewMongoCrypt constructs a new MongoCrypt instance configured using the provided MongoCryptOptions.
func NewMongoCrypt(opts *options.MongoCryptOptions) (*MongoCrypt, error) {
panic(cseNotSupportedMsg)
}
// CreateEncryptionContext creates a Context to use for encryption.
func (m *MongoCrypt) CreateEncryptionContext(db string, cmd bsoncore.Document) (*Context, error) {
panic(cseNotSupportedMsg)
}
// CreateDecryptionContext creates a Context to use for decryption.
func (m *MongoCrypt) CreateDecryptionContext(cmd bsoncore.Document) (*Context, error) {
panic(cseNotSupportedMsg)
}
// CreateDataKeyContext creates a Context to use for creating a data key.
func (m *MongoCrypt) CreateDataKeyContext(kmsProvider string, opts *options.DataKeyOptions) (*Context, error) {
panic(cseNotSupportedMsg)
}
// CreateExplicitEncryptionContext creates a Context to use for explicit encryption.
func (m *MongoCrypt) CreateExplicitEncryptionContext(doc bsoncore.Document, opts *options.ExplicitEncryptionOptions) (*Context, error) {
panic(cseNotSupportedMsg)
}
// RewrapDataKeyContext creates a Context to use for rewrapping a data key.
func (m *MongoCrypt) RewrapDataKeyContext(filter []byte, opts *options.RewrapManyDataKeyOptions) (*Context, error) {
panic(cseNotSupportedMsg)
}
// CreateExplicitDecryptionContext creates a Context to use for explicit decryption.
func (m *MongoCrypt) CreateExplicitDecryptionContext(doc bsoncore.Document) (*Context, error) {
panic(cseNotSupportedMsg)
}
// CryptSharedLibVersion returns the version number for the loaded crypt_shared library, or 0 if the
// crypt_shared library was not loaded.
func (m *MongoCrypt) CryptSharedLibVersion() uint64 {
panic(cseNotSupportedMsg)
}
// CryptSharedLibVersionString returns the version string for the loaded crypt_shared library, or an
// empty string if the crypt_shared library was not loaded.
func (m *MongoCrypt) CryptSharedLibVersionString() string {
panic(cseNotSupportedMsg)
}
// Close cleans up any resources associated with the given MongoCrypt instance.
func (m *MongoCrypt) Close() {
panic(cseNotSupportedMsg)
}
@@ -0,0 +1,139 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package options
import (
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// DataKeyOptions specifies options for creating a new data key.
type DataKeyOptions struct {
KeyAltNames []string
KeyMaterial []byte
MasterKey bsoncore.Document
}
// DataKey creates a new DataKeyOptions instance.
func DataKey() *DataKeyOptions {
return &DataKeyOptions{}
}
// SetKeyAltNames specifies alternate key names.
func (dko *DataKeyOptions) SetKeyAltNames(names []string) *DataKeyOptions {
dko.KeyAltNames = names
return dko
}
// SetMasterKey specifies the master key.
func (dko *DataKeyOptions) SetMasterKey(key bsoncore.Document) *DataKeyOptions {
dko.MasterKey = key
return dko
}
// SetKeyMaterial specifies the key material.
func (dko *DataKeyOptions) SetKeyMaterial(keyMaterial []byte) *DataKeyOptions {
dko.KeyMaterial = keyMaterial
return dko
}
// QueryType describes the type of query the result of Encrypt is used for.
type QueryType int
// These constants specify valid values for QueryType
const (
QueryTypeEquality QueryType = 1
)
// ExplicitEncryptionOptions specifies options for configuring an explicit encryption context.
type ExplicitEncryptionOptions struct {
KeyID *primitive.Binary
KeyAltName *string
Algorithm string
QueryType string
ContentionFactor *int64
}
// ExplicitEncryption creates a new ExplicitEncryptionOptions instance.
func ExplicitEncryption() *ExplicitEncryptionOptions {
return &ExplicitEncryptionOptions{}
}
// SetKeyID sets the key identifier.
func (eeo *ExplicitEncryptionOptions) SetKeyID(keyID primitive.Binary) *ExplicitEncryptionOptions {
eeo.KeyID = &keyID
return eeo
}
// SetKeyAltName sets the key alternative name.
func (eeo *ExplicitEncryptionOptions) SetKeyAltName(keyAltName string) *ExplicitEncryptionOptions {
eeo.KeyAltName = &keyAltName
return eeo
}
// SetAlgorithm specifies an encryption algorithm.
func (eeo *ExplicitEncryptionOptions) SetAlgorithm(algorithm string) *ExplicitEncryptionOptions {
eeo.Algorithm = algorithm
return eeo
}
// SetQueryType specifies the query type.
func (eeo *ExplicitEncryptionOptions) SetQueryType(queryType string) *ExplicitEncryptionOptions {
eeo.QueryType = queryType
return eeo
}
// SetContentionFactor specifies the contention factor.
func (eeo *ExplicitEncryptionOptions) SetContentionFactor(contentionFactor int64) *ExplicitEncryptionOptions {
eeo.ContentionFactor = &contentionFactor
return eeo
}
// RewrapManyDataKeyOptions represents all possible options used to decrypt and encrypt all matching data keys with a
// possibly new masterKey.
type RewrapManyDataKeyOptions struct {
// Provider identifies the new KMS provider. If omitted, encrypting uses the current KMS provider.
Provider *string
// MasterKey identifies the new masterKey. If omitted, rewraps with the current masterKey.
MasterKey bsoncore.Document
}
// RewrapManyDataKey creates a new RewrapManyDataKeyOptions instance.
func RewrapManyDataKey() *RewrapManyDataKeyOptions {
return new(RewrapManyDataKeyOptions)
}
// SetProvider sets the value for the Provider field.
func (rmdko *RewrapManyDataKeyOptions) SetProvider(provider string) *RewrapManyDataKeyOptions {
rmdko.Provider = &provider
return rmdko
}
// SetMasterKey sets the value for the MasterKey field.
func (rmdko *RewrapManyDataKeyOptions) SetMasterKey(masterKey bsoncore.Document) *RewrapManyDataKeyOptions {
rmdko.MasterKey = masterKey
return rmdko
}
// MergeRewrapManyDataKeyOptions combines the given RewrapManyDataKeyOptions instances into a single
// RewrapManyDataKeyOptions in a last one wins fashion.
func MergeRewrapManyDataKeyOptions(opts ...*RewrapManyDataKeyOptions) *RewrapManyDataKeyOptions {
rmdkOpts := RewrapManyDataKey()
for _, rmdko := range opts {
if rmdko == nil {
continue
}
if provider := rmdko.Provider; provider != nil {
rmdkOpts.Provider = provider
}
if masterKey := rmdko.MasterKey; masterKey != nil {
rmdkOpts.MasterKey = masterKey
}
}
return rmdkOpts
}
@@ -0,0 +1,63 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package options
import (
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// MongoCryptOptions specifies options to configure a MongoCrypt instance.
type MongoCryptOptions struct {
KmsProviders bsoncore.Document
LocalSchemaMap map[string]bsoncore.Document
BypassQueryAnalysis bool
EncryptedFieldsMap map[string]bsoncore.Document
CryptSharedLibDisabled bool
CryptSharedLibOverridePath string
}
// MongoCrypt creates a new MongoCryptOptions instance.
func MongoCrypt() *MongoCryptOptions {
return &MongoCryptOptions{}
}
// SetKmsProviders specifies the KMS providers map.
func (mo *MongoCryptOptions) SetKmsProviders(kmsProviders bsoncore.Document) *MongoCryptOptions {
mo.KmsProviders = kmsProviders
return mo
}
// SetLocalSchemaMap specifies the local schema map.
func (mo *MongoCryptOptions) SetLocalSchemaMap(localSchemaMap map[string]bsoncore.Document) *MongoCryptOptions {
mo.LocalSchemaMap = localSchemaMap
return mo
}
// SetBypassQueryAnalysis skips the NeedMongoMarkings state.
func (mo *MongoCryptOptions) SetBypassQueryAnalysis(bypassQueryAnalysis bool) *MongoCryptOptions {
mo.BypassQueryAnalysis = bypassQueryAnalysis
return mo
}
// SetEncryptedFieldsMap specifies the encrypted fields map.
func (mo *MongoCryptOptions) SetEncryptedFieldsMap(efcMap map[string]bsoncore.Document) *MongoCryptOptions {
mo.EncryptedFieldsMap = efcMap
return mo
}
// SetCryptSharedLibDisabled explicitly disables loading the crypt_shared library if set to true.
func (mo *MongoCryptOptions) SetCryptSharedLibDisabled(disabled bool) *MongoCryptOptions {
mo.CryptSharedLibDisabled = disabled
return mo
}
// SetCryptSharedLibOverridePath sets the override path to the crypt_shared library file. Setting
// an override path disables the default operating system dynamic library search path.
func (mo *MongoCryptOptions) SetCryptSharedLibOverridePath(path string) *MongoCryptOptions {
mo.CryptSharedLibOverridePath = path
return mo
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package mongocrypt
// State represents a state that a MongocryptContext can be in.
type State int
// These constants are valid values for the State type.
const (
StateError State = iota
NeedMongoCollInfo
NeedMongoMarkings
NeedMongoKeys
NeedKms
Ready
Done
)
// String implements the Stringer interface.
func (s State) String() string {
switch s {
case StateError:
return "Error"
case NeedMongoCollInfo:
return "NeedMongoCollInfo"
case NeedMongoMarkings:
return "NeedMongoMarkings"
case NeedMongoKeys:
return "NeedMongoKeys"
case NeedKms:
return "NeedKms"
case Ready:
return "Ready"
case Done:
return "Done"
default:
return "Unknown State"
}
}
+121
View File
@@ -0,0 +1,121 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package ocsp
import (
"crypto"
"sync"
"time"
"golang.org/x/crypto/ocsp"
)
type cacheKey struct {
HashAlgorithm crypto.Hash
IssuerNameHash string
IssuerKeyHash string
SerialNumber string
}
// Cache represents an OCSP cache.
type Cache interface {
Update(*ocsp.Request, *ResponseDetails) *ResponseDetails
Get(request *ocsp.Request) *ResponseDetails
}
// ConcurrentCache is an implementation of ocsp.Cache that's safe for concurrent use.
type ConcurrentCache struct {
cache map[cacheKey]*ResponseDetails
sync.Mutex
}
var _ Cache = (*ConcurrentCache)(nil)
// NewCache creates an empty OCSP cache.
func NewCache() *ConcurrentCache {
return &ConcurrentCache{
cache: make(map[cacheKey]*ResponseDetails),
}
}
// Update updates the cache entry for the provided request. The provided response will only be cached if it has a
// status that is not ocsp.Unknown and has a non-zero NextUpdate time. If there is an existing cache entry for request,
// it will be overwritten by response if response.NextUpdate is further ahead in the future than the existing entry's
// NextUpdate.
//
// This function returns the most up-to-date response corresponding to the request.
func (c *ConcurrentCache) Update(request *ocsp.Request, response *ResponseDetails) *ResponseDetails {
unknown := response.Status == ocsp.Unknown
hasUpdateTime := !response.NextUpdate.IsZero()
canBeCached := !unknown && hasUpdateTime
key := createCacheKey(request)
c.Lock()
defer c.Unlock()
current, ok := c.cache[key]
if !ok {
if canBeCached {
c.cache[key] = response
}
// Return the provided response even though it might not have been cached because it's the most up-to-date
// response available.
return response
}
// If the new response is Unknown, we can't cache it. Return the existing cached response.
if unknown {
return current
}
// If a response has no nextUpdate set, the responder is telling us that newer information is always available.
// In this case, remove the existing cache entry because it is stale and return the new response because it is
// more up-to-date.
if !hasUpdateTime {
delete(c.cache, key)
return response
}
// If we get here, the new response is conclusive and has a non-empty nextUpdate so it can be cached. Overwrite
// the existing cache entry if the new one will be valid for longer.
newest := current
if response.NextUpdate.After(current.NextUpdate) {
c.cache[key] = response
newest = response
}
return newest
}
// Get returns the cached response for the request, or nil if there is no cached response. If the cached response has
// expired, it will be removed from the cache and nil will be returned.
func (c *ConcurrentCache) Get(request *ocsp.Request) *ResponseDetails {
key := createCacheKey(request)
c.Lock()
defer c.Unlock()
response, ok := c.cache[key]
if !ok {
return nil
}
if time.Now().UTC().Before(response.NextUpdate) {
return response
}
delete(c.cache, key)
return nil
}
func createCacheKey(request *ocsp.Request) cacheKey {
return cacheKey{
HashAlgorithm: request.HashAlgorithm,
IssuerNameHash: string(request.IssuerNameHash),
IssuerKeyHash: string(request.IssuerKeyHash),
SerialNumber: request.SerialNumber.String(),
}
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package ocsp
import (
"crypto/x509"
"errors"
"fmt"
"golang.org/x/crypto/ocsp"
)
type config struct {
serverCert, issuer *x509.Certificate
cache Cache
disableEndpointChecking bool
ocspRequest *ocsp.Request
ocspRequestBytes []byte
}
func newConfig(certChain []*x509.Certificate, opts *VerifyOptions) (config, error) {
cfg := config{
cache: opts.Cache,
disableEndpointChecking: opts.DisableEndpointChecking,
}
if len(certChain) == 0 {
return cfg, errors.New("verified certificate chain contained no certificates")
}
// In the case where the leaf certificate and CA are the same, the chain may only contain one certificate.
cfg.serverCert = certChain[0]
cfg.issuer = certChain[0]
if len(certChain) > 1 {
// If the chain has multiple certificates, the one directly after the leaf should be the issuer. Use
// CheckSignatureFrom to verify that it is the issuer.
cfg.issuer = certChain[1]
if err := cfg.serverCert.CheckSignatureFrom(cfg.issuer); err != nil {
errString := "error checking if server certificate is signed by the issuer in the verified chain: %v"
return cfg, fmt.Errorf(errString, err)
}
}
var err error
cfg.ocspRequestBytes, err = ocsp.CreateRequest(cfg.serverCert, cfg.issuer, nil)
if err != nil {
return cfg, fmt.Errorf("error creating OCSP request: %v", err)
}
cfg.ocspRequest, err = ocsp.ParseRequest(cfg.ocspRequestBytes)
if err != nil {
return cfg, fmt.Errorf("error parsing OCSP request bytes: %v", err)
}
return cfg, nil
}
+321
View File
@@ -0,0 +1,321 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package ocsp
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"time"
"golang.org/x/crypto/ocsp"
"golang.org/x/sync/errgroup"
)
var (
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
mustStapleFeatureValue = big.NewInt(5)
)
// Error represents an OCSP verification error
type Error struct {
wrapped error
}
// Error implements the error interface
func (e *Error) Error() string {
return fmt.Sprintf("OCSP verification failed: %v", e.wrapped)
}
// Unwrap returns the underlying error.
func (e *Error) Unwrap() error {
return e.wrapped
}
func newOCSPError(wrapped error) error {
return &Error{wrapped: wrapped}
}
// ResponseDetails contains a subset of the details needed from an OCSP response after the original response has been
// validated.
type ResponseDetails struct {
Status int
NextUpdate time.Time
}
func extractResponseDetails(res *ocsp.Response) *ResponseDetails {
return &ResponseDetails{
Status: res.Status,
NextUpdate: res.NextUpdate,
}
}
// Verify performs OCSP verification for the provided ConnectionState instance.
func Verify(ctx context.Context, connState tls.ConnectionState, opts *VerifyOptions) error {
if opts.Cache == nil {
// There should always be an OCSP cache. Even if the user has specified the URI option to disable communication
// with OCSP responders, the driver will cache any stapled responses. Requiring that the cache is non-nil
// allows us to confirm that the cache is correctly being passed down from a higher level.
return newOCSPError(errors.New("no OCSP cache provided"))
}
if len(connState.VerifiedChains) == 0 {
return newOCSPError(errors.New("no verified certificate chains reported after TLS handshake"))
}
certChain := connState.VerifiedChains[0]
if numCerts := len(certChain); numCerts == 0 {
return newOCSPError(errors.New("verified chain contained no certificates"))
}
ocspCfg, err := newConfig(certChain, opts)
if err != nil {
return newOCSPError(err)
}
res, err := getParsedResponse(ctx, ocspCfg, connState)
if err != nil {
return err
}
if res == nil {
// If no response was parsed from the staple and responders, the status of the certificate is unknown, so don't
// error.
return nil
}
if res.Status == ocsp.Revoked {
return newOCSPError(errors.New("certificate is revoked"))
}
return nil
}
// getParsedResponse attempts to parse a response from the stapled OCSP data or by contacting OCSP responders if no
// staple is present.
func getParsedResponse(ctx context.Context, cfg config, connState tls.ConnectionState) (*ResponseDetails, error) {
stapledResponse, err := processStaple(cfg, connState.OCSPResponse)
if err != nil {
return nil, err
}
if stapledResponse != nil {
// If there is a staple, attempt to cache it. The cache.Update call will resolve conflicts with an existing
// cache enry if necessary.
return cfg.cache.Update(cfg.ocspRequest, stapledResponse), nil
}
if cachedResponse := cfg.cache.Get(cfg.ocspRequest); cachedResponse != nil {
return cachedResponse, nil
}
// If there is no stapled or cached response, fall back to querying the responders if that functionality has not
// been disabled.
if cfg.disableEndpointChecking {
return nil, nil
}
externalResponse := contactResponders(ctx, cfg)
if externalResponse == nil {
// None of the responders were available.
return nil, nil
}
// Similar to the stapled response case above, unconditionally call Update and it will either cache the response
// or resolve conflicts if a different connection has cached a response since the previous call to Get.
return cfg.cache.Update(cfg.ocspRequest, externalResponse), nil
}
// processStaple returns the OCSP response from the provided staple. An error will be returned if any of the following
// are true:
//
// 1. cfg.serverCert has the Must-Staple extension but the staple is empty.
// 2. The staple is malformed.
// 3. The staple does not cover cfg.serverCert.
// 4. The OCSP response has an error status.
func processStaple(cfg config, staple []byte) (*ResponseDetails, error) {
mustStaple, err := isMustStapleCertificate(cfg.serverCert)
if err != nil {
return nil, err
}
// If the server has a Must-Staple certificate and the server does not present a stapled OCSP response, error.
if mustStaple && len(staple) == 0 {
return nil, errors.New("server provided a certificate with the Must-Staple extension but did not " +
"provde a stapled OCSP response")
}
if len(staple) == 0 {
return nil, nil
}
parsedResponse, err := ocsp.ParseResponseForCert(staple, cfg.serverCert, cfg.issuer)
if err != nil {
// If the stapled response could not be parsed correctly, error. This can happen if the response is malformed,
// the response does not cover the certificate presented by the server, or if the response contains an error
// status.
return nil, fmt.Errorf("error parsing stapled response: %v", err)
}
if err = verifyResponse(cfg, parsedResponse); err != nil {
return nil, fmt.Errorf("error validating stapled response: %v", err)
}
return extractResponseDetails(parsedResponse), nil
}
// isMustStapleCertificate determines whether or not an X509 certificate is a must-staple certificate.
func isMustStapleCertificate(cert *x509.Certificate) (bool, error) {
var featureExtension pkix.Extension
var foundExtension bool
for _, ext := range cert.Extensions {
if ext.Id.Equal(tlsFeatureExtensionOID) {
featureExtension = ext
foundExtension = true
break
}
}
if !foundExtension {
return false, nil
}
// The value for the TLS feature extension is a sequence of integers. Per the asn1.Unmarshal documentation, an
// integer can be unmarshalled into an int, int32, int64, or *big.Int and unmarshalling will error if the integer
// cannot be encoded into the target type.
//
// Use []*big.Int to ensure that all values in the sequence can be successfully unmarshalled.
var featureValues []*big.Int
if _, err := asn1.Unmarshal(featureExtension.Value, &featureValues); err != nil {
return false, fmt.Errorf("error unmarshalling TLS feature extension values: %v", err)
}
for _, value := range featureValues {
if value.Cmp(mustStapleFeatureValue) == 0 {
return true, nil
}
}
return false, nil
}
// contactResponders will send a request to all OCSP responders reported by cfg.serverCert. The
// first response that conclusively identifies cfg.serverCert as good or revoked will be returned.
// If all responders are unavailable or no responder returns a conclusive status, it returns nil.
// contactResponders will wait for up to 5 seconds to get a certificate status response.
func contactResponders(ctx context.Context, cfg config) *ResponseDetails {
if len(cfg.serverCert.OCSPServer) == 0 {
return nil
}
// Limit all OCSP responder calls to a maximum of 5 seconds or when the passed-in context expires,
// whichever happens first.
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
group, ctx := errgroup.WithContext(ctx)
ocspResponses := make(chan *ocsp.Response, len(cfg.serverCert.OCSPServer))
defer close(ocspResponses)
for _, endpoint := range cfg.serverCert.OCSPServer {
// Re-assign endpoint so it gets re-scoped rather than using the iteration variable in the goroutine. See
// https://golang.org/doc/faq#closures_and_goroutines.
endpoint := endpoint
// Start a group of goroutines that each attempt to request the certificate status from one
// of the OCSP endpoints listed in the server certificate. We want to "soft fail" on all
// errors, so this function never returns actual errors. Only a "done" error is returned
// when a response is received so the errgroup cancels any other in-progress requests.
group.Go(func() error {
// Use bytes.NewReader instead of bytes.NewBuffer because a bytes.Buffer is an owning representation and the
// docs recommend not using the underlying []byte after creating the buffer, so a new copy of the request
// bytes would be needed for each request.
request, err := http.NewRequest("POST", endpoint, bytes.NewReader(cfg.ocspRequestBytes))
if err != nil {
return nil
}
request = request.WithContext(ctx)
httpResponse, err := http.DefaultClient.Do(request)
if err != nil {
return nil
}
defer func() {
_ = httpResponse.Body.Close()
}()
if httpResponse.StatusCode != 200 {
return nil
}
httpBytes, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
return nil
}
ocspResponse, err := ocsp.ParseResponseForCert(httpBytes, cfg.serverCert, cfg.issuer)
if err != nil || verifyResponse(cfg, ocspResponse) != nil || ocspResponse.Status == ocsp.Unknown {
// If there was an error parsing/validating the response or the response was
// inconclusive, suppress the error because we want to ignore this responder.
return nil
}
// Send the conclusive response on the response channel and return a "done" error that
// will cause the errgroup to cancel all other in-progress requests.
ocspResponses <- ocspResponse
return errors.New("done")
})
}
_ = group.Wait()
select {
case res := <-ocspResponses:
return extractResponseDetails(res)
default:
// If there is no OCSP response on the response channel, all OCSP calls either failed or
// were inconclusive. Return nil.
return nil
}
}
// verifyResponse checks that the provided OCSP response is valid.
func verifyResponse(cfg config, res *ocsp.Response) error {
if err := verifyExtendedKeyUsage(cfg, res); err != nil {
return err
}
currTime := time.Now().UTC()
if res.ThisUpdate.After(currTime) {
return fmt.Errorf("reported thisUpdate time %s is after current time %s", res.ThisUpdate, currTime)
}
if !res.NextUpdate.IsZero() && res.NextUpdate.Before(currTime) {
return fmt.Errorf("reported nextUpdate time %s is before current time %s", res.NextUpdate, currTime)
}
return nil
}
func verifyExtendedKeyUsage(cfg config, res *ocsp.Response) error {
if res.Certificate == nil {
return nil
}
namesMatch := res.RawResponderName != nil && bytes.Equal(res.RawResponderName, cfg.issuer.RawSubject)
keyHashesMatch := res.ResponderKeyHash != nil && bytes.Equal(res.ResponderKeyHash, cfg.ocspRequest.IssuerKeyHash)
if namesMatch || keyHashesMatch {
// The responder certificate is the same as the issuer certificate.
return nil
}
// There is a delegate.
for _, extKeyUsage := range res.Certificate.ExtKeyUsage {
if extKeyUsage == x509.ExtKeyUsageOCSPSigning {
return nil
}
}
return errors.New("delegate responder certificate is missing the OCSP signing extended key usage")
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package ocsp
// VerifyOptions specifies options to configure OCSP verification.
type VerifyOptions struct {
Cache Cache
DisableEndpointChecking bool
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,199 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// AbortTransaction performs an abortTransaction operation.
type AbortTransaction struct {
recoveryToken bsoncore.Document
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
retry *driver.RetryMode
serverAPI *driver.ServerAPIOptions
}
// NewAbortTransaction constructs and returns a new AbortTransaction.
func NewAbortTransaction() *AbortTransaction {
return &AbortTransaction{}
}
func (at *AbortTransaction) processResponse(driver.ResponseInfo) error {
var err error
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (at *AbortTransaction) Execute(ctx context.Context) error {
if at.deployment == nil {
return errors.New("the AbortTransaction operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: at.command,
ProcessResponseFn: at.processResponse,
RetryMode: at.retry,
Type: driver.Write,
Client: at.session,
Clock: at.clock,
CommandMonitor: at.monitor,
Crypt: at.crypt,
Database: at.database,
Deployment: at.deployment,
Selector: at.selector,
WriteConcern: at.writeConcern,
ServerAPI: at.serverAPI,
}.Execute(ctx, nil)
}
func (at *AbortTransaction) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendInt32Element(dst, "abortTransaction", 1)
if at.recoveryToken != nil {
dst = bsoncore.AppendDocumentElement(dst, "recoveryToken", at.recoveryToken)
}
return dst, nil
}
// RecoveryToken sets the recovery token to use when committing or aborting a sharded transaction.
func (at *AbortTransaction) RecoveryToken(recoveryToken bsoncore.Document) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.recoveryToken = recoveryToken
return at
}
// Session sets the session for this operation.
func (at *AbortTransaction) Session(session *session.Client) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.session = session
return at
}
// ClusterClock sets the cluster clock for this operation.
func (at *AbortTransaction) ClusterClock(clock *session.ClusterClock) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.clock = clock
return at
}
// Collection sets the collection that this command will run against.
func (at *AbortTransaction) Collection(collection string) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.collection = collection
return at
}
// CommandMonitor sets the monitor to use for APM events.
func (at *AbortTransaction) CommandMonitor(monitor *event.CommandMonitor) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.monitor = monitor
return at
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (at *AbortTransaction) Crypt(crypt driver.Crypt) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.crypt = crypt
return at
}
// Database sets the database to run this operation against.
func (at *AbortTransaction) Database(database string) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.database = database
return at
}
// Deployment sets the deployment to use for this operation.
func (at *AbortTransaction) Deployment(deployment driver.Deployment) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.deployment = deployment
return at
}
// ServerSelector sets the selector used to retrieve a server.
func (at *AbortTransaction) ServerSelector(selector description.ServerSelector) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.selector = selector
return at
}
// WriteConcern sets the write concern for this operation.
func (at *AbortTransaction) WriteConcern(writeConcern *writeconcern.WriteConcern) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.writeConcern = writeConcern
return at
}
// Retry enables retryable mode for this operation. Retries are handled automatically in driver.Operation.Execute based
// on how the operation is set.
func (at *AbortTransaction) Retry(retry driver.RetryMode) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.retry = &retry
return at
}
// ServerAPI sets the server API version for this operation.
func (at *AbortTransaction) ServerAPI(serverAPI *driver.ServerAPIOptions) *AbortTransaction {
if at == nil {
at = new(AbortTransaction)
}
at.serverAPI = serverAPI
return at
}
@@ -0,0 +1,424 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/readconcern"
"go.mongodb.org/mongo-driver/mongo/readpref"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Aggregate represents an aggregate operation.
type Aggregate struct {
allowDiskUse *bool
batchSize *int32
bypassDocumentValidation *bool
collation bsoncore.Document
comment *string
hint bsoncore.Value
maxTimeMS *int64
pipeline bsoncore.Document
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
database string
deployment driver.Deployment
readConcern *readconcern.ReadConcern
readPreference *readpref.ReadPref
retry *driver.RetryMode
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
crypt driver.Crypt
serverAPI *driver.ServerAPIOptions
let bsoncore.Document
hasOutputStage bool
customOptions map[string]bsoncore.Value
timeout *time.Duration
result driver.CursorResponse
}
// NewAggregate constructs and returns a new Aggregate.
func NewAggregate(pipeline bsoncore.Document) *Aggregate {
return &Aggregate{
pipeline: pipeline,
}
}
// Result returns the result of executing this operation.
func (a *Aggregate) Result(opts driver.CursorOptions) (*driver.BatchCursor, error) {
clientSession := a.session
clock := a.clock
opts.ServerAPI = a.serverAPI
return driver.NewBatchCursor(a.result, clientSession, clock, opts)
}
// ResultCursorResponse returns the underlying CursorResponse result of executing this
// operation.
func (a *Aggregate) ResultCursorResponse() driver.CursorResponse {
return a.result
}
func (a *Aggregate) processResponse(info driver.ResponseInfo) error {
var err error
a.result, err = driver.NewCursorResponse(info)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (a *Aggregate) Execute(ctx context.Context) error {
if a.deployment == nil {
return errors.New("the Aggregate operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: a.command,
ProcessResponseFn: a.processResponse,
Client: a.session,
Clock: a.clock,
CommandMonitor: a.monitor,
Database: a.database,
Deployment: a.deployment,
ReadConcern: a.readConcern,
ReadPreference: a.readPreference,
Type: driver.Read,
RetryMode: a.retry,
Selector: a.selector,
WriteConcern: a.writeConcern,
Crypt: a.crypt,
MinimumWriteConcernWireVersion: 5,
ServerAPI: a.serverAPI,
IsOutputAggregate: a.hasOutputStage,
Timeout: a.timeout,
}.Execute(ctx, nil)
}
func (a *Aggregate) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
header := bsoncore.Value{Type: bsontype.String, Data: bsoncore.AppendString(nil, a.collection)}
if a.collection == "" {
header = bsoncore.Value{Type: bsontype.Int32, Data: []byte{0x01, 0x00, 0x00, 0x00}}
}
dst = bsoncore.AppendValueElement(dst, "aggregate", header)
cursorIdx, cursorDoc := bsoncore.AppendDocumentStart(nil)
if a.allowDiskUse != nil {
dst = bsoncore.AppendBooleanElement(dst, "allowDiskUse", *a.allowDiskUse)
}
if a.batchSize != nil {
cursorDoc = bsoncore.AppendInt32Element(cursorDoc, "batchSize", *a.batchSize)
}
if a.bypassDocumentValidation != nil {
dst = bsoncore.AppendBooleanElement(dst, "bypassDocumentValidation", *a.bypassDocumentValidation)
}
if a.collation != nil {
if desc.WireVersion == nil || !desc.WireVersion.Includes(5) {
return nil, errors.New("the 'collation' command parameter requires a minimum server wire version of 5")
}
dst = bsoncore.AppendDocumentElement(dst, "collation", a.collation)
}
if a.comment != nil {
dst = bsoncore.AppendStringElement(dst, "comment", *a.comment)
}
if a.hint.Type != bsontype.Type(0) {
dst = bsoncore.AppendValueElement(dst, "hint", a.hint)
}
// Only append specified maxTimeMS if timeout is not also specified.
if a.maxTimeMS != nil && a.timeout == nil {
dst = bsoncore.AppendInt64Element(dst, "maxTimeMS", *a.maxTimeMS)
}
if a.pipeline != nil {
dst = bsoncore.AppendArrayElement(dst, "pipeline", a.pipeline)
}
if a.let != nil {
dst = bsoncore.AppendDocumentElement(dst, "let", a.let)
}
for optionName, optionValue := range a.customOptions {
dst = bsoncore.AppendValueElement(dst, optionName, optionValue)
}
cursorDoc, _ = bsoncore.AppendDocumentEnd(cursorDoc, cursorIdx)
dst = bsoncore.AppendDocumentElement(dst, "cursor", cursorDoc)
return dst, nil
}
// AllowDiskUse enables writing to temporary files. When true, aggregation stages can write to the dbPath/_tmp directory.
func (a *Aggregate) AllowDiskUse(allowDiskUse bool) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.allowDiskUse = &allowDiskUse
return a
}
// BatchSize specifies the number of documents to return in every batch.
func (a *Aggregate) BatchSize(batchSize int32) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.batchSize = &batchSize
return a
}
// BypassDocumentValidation allows the write to opt-out of document level validation. This only applies when the $out stage is specified.
func (a *Aggregate) BypassDocumentValidation(bypassDocumentValidation bool) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.bypassDocumentValidation = &bypassDocumentValidation
return a
}
// Collation specifies a collation. This option is only valid for server versions 3.4 and above.
func (a *Aggregate) Collation(collation bsoncore.Document) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.collation = collation
return a
}
// Comment specifies an arbitrary string to help trace the operation through the database profiler, currentOp, and logs.
func (a *Aggregate) Comment(comment string) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.comment = &comment
return a
}
// Hint specifies the index to use.
func (a *Aggregate) Hint(hint bsoncore.Value) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.hint = hint
return a
}
// MaxTimeMS specifies the maximum amount of time to allow the query to run.
func (a *Aggregate) MaxTimeMS(maxTimeMS int64) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.maxTimeMS = &maxTimeMS
return a
}
// Pipeline determines how data is transformed for an aggregation.
func (a *Aggregate) Pipeline(pipeline bsoncore.Document) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.pipeline = pipeline
return a
}
// Session sets the session for this operation.
func (a *Aggregate) Session(session *session.Client) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.session = session
return a
}
// ClusterClock sets the cluster clock for this operation.
func (a *Aggregate) ClusterClock(clock *session.ClusterClock) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.clock = clock
return a
}
// Collection sets the collection that this command will run against.
func (a *Aggregate) Collection(collection string) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.collection = collection
return a
}
// CommandMonitor sets the monitor to use for APM events.
func (a *Aggregate) CommandMonitor(monitor *event.CommandMonitor) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.monitor = monitor
return a
}
// Database sets the database to run this operation against.
func (a *Aggregate) Database(database string) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.database = database
return a
}
// Deployment sets the deployment to use for this operation.
func (a *Aggregate) Deployment(deployment driver.Deployment) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.deployment = deployment
return a
}
// ReadConcern specifies the read concern for this operation.
func (a *Aggregate) ReadConcern(readConcern *readconcern.ReadConcern) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.readConcern = readConcern
return a
}
// ReadPreference set the read preference used with this operation.
func (a *Aggregate) ReadPreference(readPreference *readpref.ReadPref) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.readPreference = readPreference
return a
}
// ServerSelector sets the selector used to retrieve a server.
func (a *Aggregate) ServerSelector(selector description.ServerSelector) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.selector = selector
return a
}
// WriteConcern sets the write concern for this operation.
func (a *Aggregate) WriteConcern(writeConcern *writeconcern.WriteConcern) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.writeConcern = writeConcern
return a
}
// Retry enables retryable writes for this operation. Retries are not handled automatically,
// instead a boolean is returned from Execute and SelectAndExecute that indicates if the
// operation can be retried. Retrying is handled by calling RetryExecute.
func (a *Aggregate) Retry(retry driver.RetryMode) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.retry = &retry
return a
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (a *Aggregate) Crypt(crypt driver.Crypt) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.crypt = crypt
return a
}
// ServerAPI sets the server API version for this operation.
func (a *Aggregate) ServerAPI(serverAPI *driver.ServerAPIOptions) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.serverAPI = serverAPI
return a
}
// Let specifies the let document to use. This option is only valid for server versions 5.0 and above.
func (a *Aggregate) Let(let bsoncore.Document) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.let = let
return a
}
// HasOutputStage specifies whether the aggregate contains an output stage. Used in determining when to
// append read preference at the operation level.
func (a *Aggregate) HasOutputStage(hos bool) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.hasOutputStage = hos
return a
}
// CustomOptions specifies extra options to use in the aggregate command.
func (a *Aggregate) CustomOptions(co map[string]bsoncore.Value) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.customOptions = co
return a
}
// Timeout sets the timeout for this operation.
func (a *Aggregate) Timeout(timeout *time.Duration) *Aggregate {
if a == nil {
a = new(Aggregate)
}
a.timeout = timeout
return a
}
+220
View File
@@ -0,0 +1,220 @@
// Copyright (C) MongoDB, Inc. 2021-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"time"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/readconcern"
"go.mongodb.org/mongo-driver/mongo/readpref"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Command is used to run a generic operation.
type Command struct {
command bsoncore.Document
readConcern *readconcern.ReadConcern
database string
deployment driver.Deployment
selector description.ServerSelector
readPreference *readpref.ReadPref
clock *session.ClusterClock
session *session.Client
monitor *event.CommandMonitor
resultResponse bsoncore.Document
resultCursor *driver.BatchCursor
crypt driver.Crypt
serverAPI *driver.ServerAPIOptions
createCursor bool
cursorOpts driver.CursorOptions
timeout *time.Duration
}
// NewCommand constructs and returns a new Command. Once the operation is executed, the result may only be accessed via
// the Result() function.
func NewCommand(command bsoncore.Document) *Command {
return &Command{
command: command,
}
}
// NewCursorCommand constructs a new Command. Once the operation is executed, the server response will be used to
// construct a cursor, which can be accessed via the ResultCursor() function.
func NewCursorCommand(command bsoncore.Document, cursorOpts driver.CursorOptions) *Command {
return &Command{
command: command,
cursorOpts: cursorOpts,
createCursor: true,
}
}
// Result returns the result of executing this operation.
func (c *Command) Result() bsoncore.Document { return c.resultResponse }
// ResultCursor returns the BatchCursor that was constructed using the command response. If the operation was not
// configured to create a cursor (i.e. it was created using NewCommand rather than NewCursorCommand), this function
// will return nil and an error.
func (c *Command) ResultCursor() (*driver.BatchCursor, error) {
if !c.createCursor {
return nil, errors.New("command operation was not configured to create a cursor, but a result cursor was requested")
}
return c.resultCursor, nil
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (c *Command) Execute(ctx context.Context) error {
if c.deployment == nil {
return errors.New("the Command operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: func(dst []byte, desc description.SelectedServer) ([]byte, error) {
return append(dst, c.command[4:len(c.command)-1]...), nil
},
ProcessResponseFn: func(info driver.ResponseInfo) error {
c.resultResponse = info.ServerResponse
if c.createCursor {
cursorRes, err := driver.NewCursorResponse(info)
if err != nil {
return err
}
c.resultCursor, err = driver.NewBatchCursor(cursorRes, c.session, c.clock, c.cursorOpts)
return err
}
return nil
},
Client: c.session,
Clock: c.clock,
CommandMonitor: c.monitor,
Database: c.database,
Deployment: c.deployment,
ReadPreference: c.readPreference,
Selector: c.selector,
Crypt: c.crypt,
ServerAPI: c.serverAPI,
Timeout: c.timeout,
}.Execute(ctx, nil)
}
// Session sets the session for this operation.
func (c *Command) Session(session *session.Client) *Command {
if c == nil {
c = new(Command)
}
c.session = session
return c
}
// ClusterClock sets the cluster clock for this operation.
func (c *Command) ClusterClock(clock *session.ClusterClock) *Command {
if c == nil {
c = new(Command)
}
c.clock = clock
return c
}
// CommandMonitor sets the monitor to use for APM events.
func (c *Command) CommandMonitor(monitor *event.CommandMonitor) *Command {
if c == nil {
c = new(Command)
}
c.monitor = monitor
return c
}
// Database sets the database to run this operation against.
func (c *Command) Database(database string) *Command {
if c == nil {
c = new(Command)
}
c.database = database
return c
}
// Deployment sets the deployment to use for this operation.
func (c *Command) Deployment(deployment driver.Deployment) *Command {
if c == nil {
c = new(Command)
}
c.deployment = deployment
return c
}
// ReadConcern specifies the read concern for this operation.
func (c *Command) ReadConcern(readConcern *readconcern.ReadConcern) *Command {
if c == nil {
c = new(Command)
}
c.readConcern = readConcern
return c
}
// ReadPreference set the read preference used with this operation.
func (c *Command) ReadPreference(readPreference *readpref.ReadPref) *Command {
if c == nil {
c = new(Command)
}
c.readPreference = readPreference
return c
}
// ServerSelector sets the selector used to retrieve a server.
func (c *Command) ServerSelector(selector description.ServerSelector) *Command {
if c == nil {
c = new(Command)
}
c.selector = selector
return c
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (c *Command) Crypt(crypt driver.Crypt) *Command {
if c == nil {
c = new(Command)
}
c.crypt = crypt
return c
}
// ServerAPI sets the server API version for this operation.
func (c *Command) ServerAPI(serverAPI *driver.ServerAPIOptions) *Command {
if c == nil {
c = new(Command)
}
c.serverAPI = serverAPI
return c
}
// Timeout sets the timeout for this operation.
func (c *Command) Timeout(timeout *time.Duration) *Command {
if c == nil {
c = new(Command)
}
c.timeout = timeout
return c
}
@@ -0,0 +1,202 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// CommitTransaction attempts to commit a transaction.
type CommitTransaction struct {
maxTimeMS *int64
recoveryToken bsoncore.Document
session *session.Client
clock *session.ClusterClock
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
retry *driver.RetryMode
serverAPI *driver.ServerAPIOptions
}
// NewCommitTransaction constructs and returns a new CommitTransaction.
func NewCommitTransaction() *CommitTransaction {
return &CommitTransaction{}
}
func (ct *CommitTransaction) processResponse(driver.ResponseInfo) error {
var err error
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (ct *CommitTransaction) Execute(ctx context.Context) error {
if ct.deployment == nil {
return errors.New("the CommitTransaction operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: ct.command,
ProcessResponseFn: ct.processResponse,
RetryMode: ct.retry,
Type: driver.Write,
Client: ct.session,
Clock: ct.clock,
CommandMonitor: ct.monitor,
Crypt: ct.crypt,
Database: ct.database,
Deployment: ct.deployment,
Selector: ct.selector,
WriteConcern: ct.writeConcern,
ServerAPI: ct.serverAPI,
}.Execute(ctx, nil)
}
func (ct *CommitTransaction) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendInt32Element(dst, "commitTransaction", 1)
if ct.maxTimeMS != nil {
dst = bsoncore.AppendInt64Element(dst, "maxTimeMS", *ct.maxTimeMS)
}
if ct.recoveryToken != nil {
dst = bsoncore.AppendDocumentElement(dst, "recoveryToken", ct.recoveryToken)
}
return dst, nil
}
// MaxTimeMS specifies the maximum amount of time to allow the query to run.
func (ct *CommitTransaction) MaxTimeMS(maxTimeMS int64) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.maxTimeMS = &maxTimeMS
return ct
}
// RecoveryToken sets the recovery token to use when committing or aborting a sharded transaction.
func (ct *CommitTransaction) RecoveryToken(recoveryToken bsoncore.Document) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.recoveryToken = recoveryToken
return ct
}
// Session sets the session for this operation.
func (ct *CommitTransaction) Session(session *session.Client) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.session = session
return ct
}
// ClusterClock sets the cluster clock for this operation.
func (ct *CommitTransaction) ClusterClock(clock *session.ClusterClock) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.clock = clock
return ct
}
// CommandMonitor sets the monitor to use for APM events.
func (ct *CommitTransaction) CommandMonitor(monitor *event.CommandMonitor) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.monitor = monitor
return ct
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (ct *CommitTransaction) Crypt(crypt driver.Crypt) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.crypt = crypt
return ct
}
// Database sets the database to run this operation against.
func (ct *CommitTransaction) Database(database string) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.database = database
return ct
}
// Deployment sets the deployment to use for this operation.
func (ct *CommitTransaction) Deployment(deployment driver.Deployment) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.deployment = deployment
return ct
}
// ServerSelector sets the selector used to retrieve a server.
func (ct *CommitTransaction) ServerSelector(selector description.ServerSelector) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.selector = selector
return ct
}
// WriteConcern sets the write concern for this operation.
func (ct *CommitTransaction) WriteConcern(writeConcern *writeconcern.WriteConcern) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.writeConcern = writeConcern
return ct
}
// Retry enables retryable mode for this operation. Retries are handled automatically in driver.Operation.Execute based
// on how the operation is set.
func (ct *CommitTransaction) Retry(retry driver.RetryMode) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.retry = &retry
return ct
}
// ServerAPI sets the server API version for this operation.
func (ct *CommitTransaction) ServerAPI(serverAPI *driver.ServerAPIOptions) *CommitTransaction {
if ct == nil {
ct = new(CommitTransaction)
}
ct.serverAPI = serverAPI
return ct
}
+315
View File
@@ -0,0 +1,315 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"fmt"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/readconcern"
"go.mongodb.org/mongo-driver/mongo/readpref"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Count represents a count operation.
type Count struct {
maxTimeMS *int64
query bsoncore.Document
session *session.Client
clock *session.ClusterClock
collection string
comment bsoncore.Value
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
readConcern *readconcern.ReadConcern
readPreference *readpref.ReadPref
selector description.ServerSelector
retry *driver.RetryMode
result CountResult
serverAPI *driver.ServerAPIOptions
timeout *time.Duration
}
// CountResult represents a count result returned by the server.
type CountResult struct {
// The number of documents found
N int64
}
func buildCountResult(response bsoncore.Document) (CountResult, error) {
elements, err := response.Elements()
if err != nil {
return CountResult{}, err
}
cr := CountResult{}
for _, element := range elements {
switch element.Key() {
case "n": // for count using original command
var ok bool
cr.N, ok = element.Value().AsInt64OK()
if !ok {
return cr, fmt.Errorf("response field 'n' is type int64, but received BSON type %s",
element.Value().Type)
}
case "cursor": // for count using aggregate with $collStats
firstBatch, err := element.Value().Document().LookupErr("firstBatch")
if err != nil {
return cr, err
}
// get count value from first batch
val := firstBatch.Array().Index(0)
count, err := val.Document().LookupErr("n")
if err != nil {
return cr, err
}
// use count as Int64 for result
var ok bool
cr.N, ok = count.AsInt64OK()
if !ok {
return cr, fmt.Errorf("response field 'n' is type int64, but received BSON type %s",
element.Value().Type)
}
}
}
return cr, nil
}
// NewCount constructs and returns a new Count.
func NewCount() *Count {
return &Count{}
}
// Result returns the result of executing this operation.
func (c *Count) Result() CountResult { return c.result }
func (c *Count) processResponse(info driver.ResponseInfo) error {
var err error
c.result, err = buildCountResult(info.ServerResponse)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (c *Count) Execute(ctx context.Context) error {
if c.deployment == nil {
return errors.New("the Count operation must have a Deployment set before Execute can be called")
}
err := driver.Operation{
CommandFn: c.command,
ProcessResponseFn: c.processResponse,
RetryMode: c.retry,
Type: driver.Read,
Client: c.session,
Clock: c.clock,
CommandMonitor: c.monitor,
Crypt: c.crypt,
Database: c.database,
Deployment: c.deployment,
ReadConcern: c.readConcern,
ReadPreference: c.readPreference,
Selector: c.selector,
ServerAPI: c.serverAPI,
Timeout: c.timeout,
}.Execute(ctx, nil)
// Swallow error if NamespaceNotFound(26) is returned from aggregate on non-existent namespace
if err != nil {
dErr, ok := err.(driver.Error)
if ok && dErr.Code == 26 {
err = nil
}
}
return err
}
func (c *Count) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "count", c.collection)
if c.query != nil {
dst = bsoncore.AppendDocumentElement(dst, "query", c.query)
}
// Only append specified maxTimeMS if timeout is not also specified.
if c.maxTimeMS != nil && c.timeout == nil {
dst = bsoncore.AppendInt64Element(dst, "maxTimeMS", *c.maxTimeMS)
}
if c.comment.Type != bsontype.Type(0) {
dst = bsoncore.AppendValueElement(dst, "comment", c.comment)
}
return dst, nil
}
// MaxTimeMS specifies the maximum amount of time to allow the query to run.
func (c *Count) MaxTimeMS(maxTimeMS int64) *Count {
if c == nil {
c = new(Count)
}
c.maxTimeMS = &maxTimeMS
return c
}
// Query determines what results are returned from find.
func (c *Count) Query(query bsoncore.Document) *Count {
if c == nil {
c = new(Count)
}
c.query = query
return c
}
// Session sets the session for this operation.
func (c *Count) Session(session *session.Client) *Count {
if c == nil {
c = new(Count)
}
c.session = session
return c
}
// ClusterClock sets the cluster clock for this operation.
func (c *Count) ClusterClock(clock *session.ClusterClock) *Count {
if c == nil {
c = new(Count)
}
c.clock = clock
return c
}
// Collection sets the collection that this command will run against.
func (c *Count) Collection(collection string) *Count {
if c == nil {
c = new(Count)
}
c.collection = collection
return c
}
// Comment sets a value to help trace an operation.
func (c *Count) Comment(comment bsoncore.Value) *Count {
if c == nil {
c = new(Count)
}
c.comment = comment
return c
}
// CommandMonitor sets the monitor to use for APM events.
func (c *Count) CommandMonitor(monitor *event.CommandMonitor) *Count {
if c == nil {
c = new(Count)
}
c.monitor = monitor
return c
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (c *Count) Crypt(crypt driver.Crypt) *Count {
if c == nil {
c = new(Count)
}
c.crypt = crypt
return c
}
// Database sets the database to run this operation against.
func (c *Count) Database(database string) *Count {
if c == nil {
c = new(Count)
}
c.database = database
return c
}
// Deployment sets the deployment to use for this operation.
func (c *Count) Deployment(deployment driver.Deployment) *Count {
if c == nil {
c = new(Count)
}
c.deployment = deployment
return c
}
// ReadConcern specifies the read concern for this operation.
func (c *Count) ReadConcern(readConcern *readconcern.ReadConcern) *Count {
if c == nil {
c = new(Count)
}
c.readConcern = readConcern
return c
}
// ReadPreference set the read preference used with this operation.
func (c *Count) ReadPreference(readPreference *readpref.ReadPref) *Count {
if c == nil {
c = new(Count)
}
c.readPreference = readPreference
return c
}
// ServerSelector sets the selector used to retrieve a server.
func (c *Count) ServerSelector(selector description.ServerSelector) *Count {
if c == nil {
c = new(Count)
}
c.selector = selector
return c
}
// Retry enables retryable mode for this operation. Retries are handled automatically in driver.Operation.Execute based
// on how the operation is set.
func (c *Count) Retry(retry driver.RetryMode) *Count {
if c == nil {
c = new(Count)
}
c.retry = &retry
return c
}
// ServerAPI sets the server API version for this operation.
func (c *Count) ServerAPI(serverAPI *driver.ServerAPIOptions) *Count {
if c == nil {
c = new(Count)
}
c.serverAPI = serverAPI
return c
}
// Timeout sets the timeout for this operation.
func (c *Count) Timeout(timeout *time.Duration) *Count {
if c == nil {
c = new(Count)
}
c.timeout = timeout
return c
}
+402
View File
@@ -0,0 +1,402 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Create represents a create operation.
type Create struct {
capped *bool
collation bsoncore.Document
changeStreamPreAndPostImages bsoncore.Document
collectionName *string
indexOptionDefaults bsoncore.Document
max *int64
pipeline bsoncore.Document
size *int64
storageEngine bsoncore.Document
validationAction *string
validationLevel *string
validator bsoncore.Document
viewOn *string
session *session.Client
clock *session.ClusterClock
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
serverAPI *driver.ServerAPIOptions
expireAfterSeconds *int64
timeSeries bsoncore.Document
encryptedFields bsoncore.Document
clusteredIndex bsoncore.Document
}
// NewCreate constructs and returns a new Create.
func NewCreate(collectionName string) *Create {
return &Create{
collectionName: &collectionName,
}
}
func (c *Create) processResponse(driver.ResponseInfo) error {
return nil
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (c *Create) Execute(ctx context.Context) error {
if c.deployment == nil {
return errors.New("the Create operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: c.command,
ProcessResponseFn: c.processResponse,
Client: c.session,
Clock: c.clock,
CommandMonitor: c.monitor,
Crypt: c.crypt,
Database: c.database,
Deployment: c.deployment,
Selector: c.selector,
WriteConcern: c.writeConcern,
ServerAPI: c.serverAPI,
}.Execute(ctx, nil)
}
func (c *Create) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
if c.collectionName != nil {
dst = bsoncore.AppendStringElement(dst, "create", *c.collectionName)
}
if c.capped != nil {
dst = bsoncore.AppendBooleanElement(dst, "capped", *c.capped)
}
if c.changeStreamPreAndPostImages != nil {
dst = bsoncore.AppendDocumentElement(dst, "changeStreamPreAndPostImages", c.changeStreamPreAndPostImages)
}
if c.collation != nil {
if desc.WireVersion == nil || !desc.WireVersion.Includes(5) {
return nil, errors.New("the 'collation' command parameter requires a minimum server wire version of 5")
}
dst = bsoncore.AppendDocumentElement(dst, "collation", c.collation)
}
if c.indexOptionDefaults != nil {
dst = bsoncore.AppendDocumentElement(dst, "indexOptionDefaults", c.indexOptionDefaults)
}
if c.max != nil {
dst = bsoncore.AppendInt64Element(dst, "max", *c.max)
}
if c.pipeline != nil {
dst = bsoncore.AppendArrayElement(dst, "pipeline", c.pipeline)
}
if c.size != nil {
dst = bsoncore.AppendInt64Element(dst, "size", *c.size)
}
if c.storageEngine != nil {
dst = bsoncore.AppendDocumentElement(dst, "storageEngine", c.storageEngine)
}
if c.validationAction != nil {
dst = bsoncore.AppendStringElement(dst, "validationAction", *c.validationAction)
}
if c.validationLevel != nil {
dst = bsoncore.AppendStringElement(dst, "validationLevel", *c.validationLevel)
}
if c.validator != nil {
dst = bsoncore.AppendDocumentElement(dst, "validator", c.validator)
}
if c.viewOn != nil {
dst = bsoncore.AppendStringElement(dst, "viewOn", *c.viewOn)
}
if c.expireAfterSeconds != nil {
dst = bsoncore.AppendInt64Element(dst, "expireAfterSeconds", *c.expireAfterSeconds)
}
if c.timeSeries != nil {
dst = bsoncore.AppendDocumentElement(dst, "timeseries", c.timeSeries)
}
if c.encryptedFields != nil {
dst = bsoncore.AppendDocumentElement(dst, "encryptedFields", c.encryptedFields)
}
if c.clusteredIndex != nil {
dst = bsoncore.AppendDocumentElement(dst, "clusteredIndex", c.clusteredIndex)
}
return dst, nil
}
// Capped specifies if the collection is capped.
func (c *Create) Capped(capped bool) *Create {
if c == nil {
c = new(Create)
}
c.capped = &capped
return c
}
// Collation specifies a collation. This option is only valid for server versions 3.4 and above.
func (c *Create) Collation(collation bsoncore.Document) *Create {
if c == nil {
c = new(Create)
}
c.collation = collation
return c
}
// ChangeStreamPreAndPostImages specifies how change streams opened against the collection can return pre-
// and post-images of updated documents. This option is only valid for server versions 6.0 and above.
func (c *Create) ChangeStreamPreAndPostImages(csppi bsoncore.Document) *Create {
if c == nil {
c = new(Create)
}
c.changeStreamPreAndPostImages = csppi
return c
}
// CollectionName specifies the name of the collection to create.
func (c *Create) CollectionName(collectionName string) *Create {
if c == nil {
c = new(Create)
}
c.collectionName = &collectionName
return c
}
// IndexOptionDefaults specifies a default configuration for indexes on the collection.
func (c *Create) IndexOptionDefaults(indexOptionDefaults bsoncore.Document) *Create {
if c == nil {
c = new(Create)
}
c.indexOptionDefaults = indexOptionDefaults
return c
}
// Max specifies the maximum number of documents allowed in a capped collection.
func (c *Create) Max(max int64) *Create {
if c == nil {
c = new(Create)
}
c.max = &max
return c
}
// Pipeline specifies the agggregtion pipeline to be run against the source to create the view.
func (c *Create) Pipeline(pipeline bsoncore.Document) *Create {
if c == nil {
c = new(Create)
}
c.pipeline = pipeline
return c
}
// Size specifies the maximum size in bytes for a capped collection.
func (c *Create) Size(size int64) *Create {
if c == nil {
c = new(Create)
}
c.size = &size
return c
}
// StorageEngine specifies the storage engine to use for the index.
func (c *Create) StorageEngine(storageEngine bsoncore.Document) *Create {
if c == nil {
c = new(Create)
}
c.storageEngine = storageEngine
return c
}
// ValidationAction specifies what should happen if a document being inserted does not pass validation.
func (c *Create) ValidationAction(validationAction string) *Create {
if c == nil {
c = new(Create)
}
c.validationAction = &validationAction
return c
}
// ValidationLevel specifies how strictly the server applies validation rules to existing documents in the collection
// during update operations.
func (c *Create) ValidationLevel(validationLevel string) *Create {
if c == nil {
c = new(Create)
}
c.validationLevel = &validationLevel
return c
}
// Validator specifies validation rules for the collection.
func (c *Create) Validator(validator bsoncore.Document) *Create {
if c == nil {
c = new(Create)
}
c.validator = validator
return c
}
// ViewOn specifies the name of the source collection or view on which the view will be created.
func (c *Create) ViewOn(viewOn string) *Create {
if c == nil {
c = new(Create)
}
c.viewOn = &viewOn
return c
}
// Session sets the session for this operation.
func (c *Create) Session(session *session.Client) *Create {
if c == nil {
c = new(Create)
}
c.session = session
return c
}
// ClusterClock sets the cluster clock for this operation.
func (c *Create) ClusterClock(clock *session.ClusterClock) *Create {
if c == nil {
c = new(Create)
}
c.clock = clock
return c
}
// CommandMonitor sets the monitor to use for APM events.
func (c *Create) CommandMonitor(monitor *event.CommandMonitor) *Create {
if c == nil {
c = new(Create)
}
c.monitor = monitor
return c
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (c *Create) Crypt(crypt driver.Crypt) *Create {
if c == nil {
c = new(Create)
}
c.crypt = crypt
return c
}
// Database sets the database to run this operation against.
func (c *Create) Database(database string) *Create {
if c == nil {
c = new(Create)
}
c.database = database
return c
}
// Deployment sets the deployment to use for this operation.
func (c *Create) Deployment(deployment driver.Deployment) *Create {
if c == nil {
c = new(Create)
}
c.deployment = deployment
return c
}
// ServerSelector sets the selector used to retrieve a server.
func (c *Create) ServerSelector(selector description.ServerSelector) *Create {
if c == nil {
c = new(Create)
}
c.selector = selector
return c
}
// WriteConcern sets the write concern for this operation.
func (c *Create) WriteConcern(writeConcern *writeconcern.WriteConcern) *Create {
if c == nil {
c = new(Create)
}
c.writeConcern = writeConcern
return c
}
// ServerAPI sets the server API version for this operation.
func (c *Create) ServerAPI(serverAPI *driver.ServerAPIOptions) *Create {
if c == nil {
c = new(Create)
}
c.serverAPI = serverAPI
return c
}
// ExpireAfterSeconds sets the seconds to wait before deleting old time-series data.
func (c *Create) ExpireAfterSeconds(eas int64) *Create {
if c == nil {
c = new(Create)
}
c.expireAfterSeconds = &eas
return c
}
// TimeSeries sets the time series options for this operation.
func (c *Create) TimeSeries(timeSeries bsoncore.Document) *Create {
if c == nil {
c = new(Create)
}
c.timeSeries = timeSeries
return c
}
// EncryptedFields sets the EncryptedFields for this operation.
func (c *Create) EncryptedFields(ef bsoncore.Document) *Create {
if c == nil {
c = new(Create)
}
c.encryptedFields = ef
return c
}
// ClusteredIndex sets the ClusteredIndex option for this operation.
func (c *Create) ClusteredIndex(ci bsoncore.Document) *Create {
if c == nil {
c = new(Create)
}
c.clusteredIndex = ci
return c
}
@@ -0,0 +1,281 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"fmt"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// CreateIndexes performs a createIndexes operation.
type CreateIndexes struct {
commitQuorum bsoncore.Value
indexes bsoncore.Document
maxTimeMS *int64
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
result CreateIndexesResult
serverAPI *driver.ServerAPIOptions
timeout *time.Duration
}
// CreateIndexesResult represents a createIndexes result returned by the server.
type CreateIndexesResult struct {
// If the collection was created automatically.
CreatedCollectionAutomatically bool
// The number of indexes existing after this command.
IndexesAfter int32
// The number of indexes existing before this command.
IndexesBefore int32
}
func buildCreateIndexesResult(response bsoncore.Document) (CreateIndexesResult, error) {
elements, err := response.Elements()
if err != nil {
return CreateIndexesResult{}, err
}
cir := CreateIndexesResult{}
for _, element := range elements {
switch element.Key() {
case "createdCollectionAutomatically":
var ok bool
cir.CreatedCollectionAutomatically, ok = element.Value().BooleanOK()
if !ok {
return cir, fmt.Errorf("response field 'createdCollectionAutomatically' is type bool, but received BSON type %s", element.Value().Type)
}
case "indexesAfter":
var ok bool
cir.IndexesAfter, ok = element.Value().AsInt32OK()
if !ok {
return cir, fmt.Errorf("response field 'indexesAfter' is type int32, but received BSON type %s", element.Value().Type)
}
case "indexesBefore":
var ok bool
cir.IndexesBefore, ok = element.Value().AsInt32OK()
if !ok {
return cir, fmt.Errorf("response field 'indexesBefore' is type int32, but received BSON type %s", element.Value().Type)
}
}
}
return cir, nil
}
// NewCreateIndexes constructs and returns a new CreateIndexes.
func NewCreateIndexes(indexes bsoncore.Document) *CreateIndexes {
return &CreateIndexes{
indexes: indexes,
}
}
// Result returns the result of executing this operation.
func (ci *CreateIndexes) Result() CreateIndexesResult { return ci.result }
func (ci *CreateIndexes) processResponse(info driver.ResponseInfo) error {
var err error
ci.result, err = buildCreateIndexesResult(info.ServerResponse)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (ci *CreateIndexes) Execute(ctx context.Context) error {
if ci.deployment == nil {
return errors.New("the CreateIndexes operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: ci.command,
ProcessResponseFn: ci.processResponse,
Client: ci.session,
Clock: ci.clock,
CommandMonitor: ci.monitor,
Crypt: ci.crypt,
Database: ci.database,
Deployment: ci.deployment,
Selector: ci.selector,
WriteConcern: ci.writeConcern,
ServerAPI: ci.serverAPI,
Timeout: ci.timeout,
}.Execute(ctx, nil)
}
func (ci *CreateIndexes) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "createIndexes", ci.collection)
if ci.commitQuorum.Type != bsontype.Type(0) {
if desc.WireVersion == nil || !desc.WireVersion.Includes(9) {
return nil, errors.New("the 'commitQuorum' command parameter requires a minimum server wire version of 9")
}
dst = bsoncore.AppendValueElement(dst, "commitQuorum", ci.commitQuorum)
}
if ci.indexes != nil {
dst = bsoncore.AppendArrayElement(dst, "indexes", ci.indexes)
}
// Only append specified maxTimeMS if timeout is not also specified.
if ci.maxTimeMS != nil && ci.timeout == nil {
dst = bsoncore.AppendInt64Element(dst, "maxTimeMS", *ci.maxTimeMS)
}
return dst, nil
}
// CommitQuorum specifies the number of data-bearing members of a replica set, including the primary, that must
// complete the index builds successfully before the primary marks the indexes as ready. This should either be a
// string or int32 value.
func (ci *CreateIndexes) CommitQuorum(commitQuorum bsoncore.Value) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.commitQuorum = commitQuorum
return ci
}
// Indexes specifies an array containing index specification documents for the indexes being created.
func (ci *CreateIndexes) Indexes(indexes bsoncore.Document) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.indexes = indexes
return ci
}
// MaxTimeMS specifies the maximum amount of time to allow the query to run.
func (ci *CreateIndexes) MaxTimeMS(maxTimeMS int64) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.maxTimeMS = &maxTimeMS
return ci
}
// Session sets the session for this operation.
func (ci *CreateIndexes) Session(session *session.Client) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.session = session
return ci
}
// ClusterClock sets the cluster clock for this operation.
func (ci *CreateIndexes) ClusterClock(clock *session.ClusterClock) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.clock = clock
return ci
}
// Collection sets the collection that this command will run against.
func (ci *CreateIndexes) Collection(collection string) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.collection = collection
return ci
}
// CommandMonitor sets the monitor to use for APM events.
func (ci *CreateIndexes) CommandMonitor(monitor *event.CommandMonitor) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.monitor = monitor
return ci
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (ci *CreateIndexes) Crypt(crypt driver.Crypt) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.crypt = crypt
return ci
}
// Database sets the database to run this operation against.
func (ci *CreateIndexes) Database(database string) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.database = database
return ci
}
// Deployment sets the deployment to use for this operation.
func (ci *CreateIndexes) Deployment(deployment driver.Deployment) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.deployment = deployment
return ci
}
// ServerSelector sets the selector used to retrieve a server.
func (ci *CreateIndexes) ServerSelector(selector description.ServerSelector) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.selector = selector
return ci
}
// WriteConcern sets the write concern for this operation.
func (ci *CreateIndexes) WriteConcern(writeConcern *writeconcern.WriteConcern) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.writeConcern = writeConcern
return ci
}
// ServerAPI sets the server API version for this operation.
func (ci *CreateIndexes) ServerAPI(serverAPI *driver.ServerAPIOptions) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.serverAPI = serverAPI
return ci
}
// Timeout sets the timeout for this operation.
func (ci *CreateIndexes) Timeout(timeout *time.Duration) *CreateIndexes {
if ci == nil {
ci = new(CreateIndexes)
}
ci.timeout = timeout
return ci
}
+314
View File
@@ -0,0 +1,314 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"fmt"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Delete performs a delete operation
type Delete struct {
comment bsoncore.Value
deletes []bsoncore.Document
ordered *bool
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
retry *driver.RetryMode
hint *bool
result DeleteResult
serverAPI *driver.ServerAPIOptions
let bsoncore.Document
timeout *time.Duration
}
// DeleteResult represents a delete result returned by the server.
type DeleteResult struct {
// Number of documents successfully deleted.
N int64
}
func buildDeleteResult(response bsoncore.Document) (DeleteResult, error) {
elements, err := response.Elements()
if err != nil {
return DeleteResult{}, err
}
dr := DeleteResult{}
for _, element := range elements {
switch element.Key() {
case "n":
var ok bool
dr.N, ok = element.Value().AsInt64OK()
if !ok {
return dr, fmt.Errorf("response field 'n' is type int32 or int64, but received BSON type %s", element.Value().Type)
}
}
}
return dr, nil
}
// NewDelete constructs and returns a new Delete.
func NewDelete(deletes ...bsoncore.Document) *Delete {
return &Delete{
deletes: deletes,
}
}
// Result returns the result of executing this operation.
func (d *Delete) Result() DeleteResult { return d.result }
func (d *Delete) processResponse(info driver.ResponseInfo) error {
dr, err := buildDeleteResult(info.ServerResponse)
d.result.N += dr.N
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (d *Delete) Execute(ctx context.Context) error {
if d.deployment == nil {
return errors.New("the Delete operation must have a Deployment set before Execute can be called")
}
batches := &driver.Batches{
Identifier: "deletes",
Documents: d.deletes,
Ordered: d.ordered,
}
return driver.Operation{
CommandFn: d.command,
ProcessResponseFn: d.processResponse,
Batches: batches,
RetryMode: d.retry,
Type: driver.Write,
Client: d.session,
Clock: d.clock,
CommandMonitor: d.monitor,
Crypt: d.crypt,
Database: d.database,
Deployment: d.deployment,
Selector: d.selector,
WriteConcern: d.writeConcern,
ServerAPI: d.serverAPI,
Timeout: d.timeout,
}.Execute(ctx, nil)
}
func (d *Delete) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "delete", d.collection)
if d.comment.Type != bsontype.Type(0) {
dst = bsoncore.AppendValueElement(dst, "comment", d.comment)
}
if d.ordered != nil {
dst = bsoncore.AppendBooleanElement(dst, "ordered", *d.ordered)
}
if d.hint != nil && *d.hint {
if desc.WireVersion == nil || !desc.WireVersion.Includes(5) {
return nil, errors.New("the 'hint' command parameter requires a minimum server wire version of 5")
}
if !d.writeConcern.Acknowledged() {
return nil, errUnacknowledgedHint
}
}
if d.let != nil {
dst = bsoncore.AppendDocumentElement(dst, "let", d.let)
}
return dst, nil
}
// Deletes adds documents to this operation that will be used to determine what documents to delete when this operation
// is executed. These documents should have the form {q: <query>, limit: <integer limit>, collation: <document>}. The
// collation field is optional. If limit is 0, there will be no limit on the number of documents deleted.
func (d *Delete) Deletes(deletes ...bsoncore.Document) *Delete {
if d == nil {
d = new(Delete)
}
d.deletes = deletes
return d
}
// Ordered sets ordered. If true, when a write fails, the operation will return the error, when
// false write failures do not stop execution of the operation.
func (d *Delete) Ordered(ordered bool) *Delete {
if d == nil {
d = new(Delete)
}
d.ordered = &ordered
return d
}
// Session sets the session for this operation.
func (d *Delete) Session(session *session.Client) *Delete {
if d == nil {
d = new(Delete)
}
d.session = session
return d
}
// ClusterClock sets the cluster clock for this operation.
func (d *Delete) ClusterClock(clock *session.ClusterClock) *Delete {
if d == nil {
d = new(Delete)
}
d.clock = clock
return d
}
// Collection sets the collection that this command will run against.
func (d *Delete) Collection(collection string) *Delete {
if d == nil {
d = new(Delete)
}
d.collection = collection
return d
}
// Comment sets a value to help trace an operation.
func (d *Delete) Comment(comment bsoncore.Value) *Delete {
if d == nil {
d = new(Delete)
}
d.comment = comment
return d
}
// CommandMonitor sets the monitor to use for APM events.
func (d *Delete) CommandMonitor(monitor *event.CommandMonitor) *Delete {
if d == nil {
d = new(Delete)
}
d.monitor = monitor
return d
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (d *Delete) Crypt(crypt driver.Crypt) *Delete {
if d == nil {
d = new(Delete)
}
d.crypt = crypt
return d
}
// Database sets the database to run this operation against.
func (d *Delete) Database(database string) *Delete {
if d == nil {
d = new(Delete)
}
d.database = database
return d
}
// Deployment sets the deployment to use for this operation.
func (d *Delete) Deployment(deployment driver.Deployment) *Delete {
if d == nil {
d = new(Delete)
}
d.deployment = deployment
return d
}
// ServerSelector sets the selector used to retrieve a server.
func (d *Delete) ServerSelector(selector description.ServerSelector) *Delete {
if d == nil {
d = new(Delete)
}
d.selector = selector
return d
}
// WriteConcern sets the write concern for this operation.
func (d *Delete) WriteConcern(writeConcern *writeconcern.WriteConcern) *Delete {
if d == nil {
d = new(Delete)
}
d.writeConcern = writeConcern
return d
}
// Retry enables retryable mode for this operation. Retries are handled automatically in driver.Operation.Execute based
// on how the operation is set.
func (d *Delete) Retry(retry driver.RetryMode) *Delete {
if d == nil {
d = new(Delete)
}
d.retry = &retry
return d
}
// Hint is a flag to indicate that the update document contains a hint. Hint is only supported by
// servers >= 4.4. Older servers >= 3.4 will report an error for using the hint option. For servers <
// 3.4, the driver will return an error if the hint option is used.
func (d *Delete) Hint(hint bool) *Delete {
if d == nil {
d = new(Delete)
}
d.hint = &hint
return d
}
// ServerAPI sets the server API version for this operation.
func (d *Delete) ServerAPI(serverAPI *driver.ServerAPIOptions) *Delete {
if d == nil {
d = new(Delete)
}
d.serverAPI = serverAPI
return d
}
// Let specifies the let document to use. This option is only valid for server versions 5.0 and above.
func (d *Delete) Let(let bsoncore.Document) *Delete {
if d == nil {
d = new(Delete)
}
d.let = let
return d
}
// Timeout sets the timeout for this operation.
func (d *Delete) Timeout(timeout *time.Duration) *Delete {
if d == nil {
d = new(Delete)
}
d.timeout = timeout
return d
}
+313
View File
@@ -0,0 +1,313 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/readconcern"
"go.mongodb.org/mongo-driver/mongo/readpref"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Distinct performs a distinct operation.
type Distinct struct {
collation bsoncore.Document
key *string
maxTimeMS *int64
query bsoncore.Document
session *session.Client
clock *session.ClusterClock
collection string
comment bsoncore.Value
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
readConcern *readconcern.ReadConcern
readPreference *readpref.ReadPref
selector description.ServerSelector
retry *driver.RetryMode
result DistinctResult
serverAPI *driver.ServerAPIOptions
timeout *time.Duration
}
// DistinctResult represents a distinct result returned by the server.
type DistinctResult struct {
// The distinct values for the field.
Values bsoncore.Value
}
func buildDistinctResult(response bsoncore.Document) (DistinctResult, error) {
elements, err := response.Elements()
if err != nil {
return DistinctResult{}, err
}
dr := DistinctResult{}
for _, element := range elements {
switch element.Key() {
case "values":
dr.Values = element.Value()
}
}
return dr, nil
}
// NewDistinct constructs and returns a new Distinct.
func NewDistinct(key string, query bsoncore.Document) *Distinct {
return &Distinct{
key: &key,
query: query,
}
}
// Result returns the result of executing this operation.
func (d *Distinct) Result() DistinctResult { return d.result }
func (d *Distinct) processResponse(info driver.ResponseInfo) error {
var err error
d.result, err = buildDistinctResult(info.ServerResponse)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (d *Distinct) Execute(ctx context.Context) error {
if d.deployment == nil {
return errors.New("the Distinct operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: d.command,
ProcessResponseFn: d.processResponse,
RetryMode: d.retry,
Type: driver.Read,
Client: d.session,
Clock: d.clock,
CommandMonitor: d.monitor,
Crypt: d.crypt,
Database: d.database,
Deployment: d.deployment,
ReadConcern: d.readConcern,
ReadPreference: d.readPreference,
Selector: d.selector,
ServerAPI: d.serverAPI,
Timeout: d.timeout,
}.Execute(ctx, nil)
}
func (d *Distinct) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "distinct", d.collection)
if d.collation != nil {
if desc.WireVersion == nil || !desc.WireVersion.Includes(5) {
return nil, errors.New("the 'collation' command parameter requires a minimum server wire version of 5")
}
dst = bsoncore.AppendDocumentElement(dst, "collation", d.collation)
}
if d.comment.Type != bsontype.Type(0) {
dst = bsoncore.AppendValueElement(dst, "comment", d.comment)
}
if d.key != nil {
dst = bsoncore.AppendStringElement(dst, "key", *d.key)
}
if d.maxTimeMS != nil {
dst = bsoncore.AppendInt64Element(dst, "maxTimeMS", *d.maxTimeMS)
}
if d.query != nil {
dst = bsoncore.AppendDocumentElement(dst, "query", d.query)
}
return dst, nil
}
// Collation specifies a collation to be used.
func (d *Distinct) Collation(collation bsoncore.Document) *Distinct {
if d == nil {
d = new(Distinct)
}
d.collation = collation
return d
}
// Key specifies which field to return distinct values for.
func (d *Distinct) Key(key string) *Distinct {
if d == nil {
d = new(Distinct)
}
d.key = &key
return d
}
// MaxTimeMS specifies the maximum amount of time to allow the query to run.
func (d *Distinct) MaxTimeMS(maxTimeMS int64) *Distinct {
if d == nil {
d = new(Distinct)
}
d.maxTimeMS = &maxTimeMS
return d
}
// Query specifies which documents to return distinct values from.
func (d *Distinct) Query(query bsoncore.Document) *Distinct {
if d == nil {
d = new(Distinct)
}
d.query = query
return d
}
// Session sets the session for this operation.
func (d *Distinct) Session(session *session.Client) *Distinct {
if d == nil {
d = new(Distinct)
}
d.session = session
return d
}
// ClusterClock sets the cluster clock for this operation.
func (d *Distinct) ClusterClock(clock *session.ClusterClock) *Distinct {
if d == nil {
d = new(Distinct)
}
d.clock = clock
return d
}
// Collection sets the collection that this command will run against.
func (d *Distinct) Collection(collection string) *Distinct {
if d == nil {
d = new(Distinct)
}
d.collection = collection
return d
}
// Comment sets a value to help trace an operation.
func (d *Distinct) Comment(comment bsoncore.Value) *Distinct {
if d == nil {
d = new(Distinct)
}
d.comment = comment
return d
}
// CommandMonitor sets the monitor to use for APM events.
func (d *Distinct) CommandMonitor(monitor *event.CommandMonitor) *Distinct {
if d == nil {
d = new(Distinct)
}
d.monitor = monitor
return d
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (d *Distinct) Crypt(crypt driver.Crypt) *Distinct {
if d == nil {
d = new(Distinct)
}
d.crypt = crypt
return d
}
// Database sets the database to run this operation against.
func (d *Distinct) Database(database string) *Distinct {
if d == nil {
d = new(Distinct)
}
d.database = database
return d
}
// Deployment sets the deployment to use for this operation.
func (d *Distinct) Deployment(deployment driver.Deployment) *Distinct {
if d == nil {
d = new(Distinct)
}
d.deployment = deployment
return d
}
// ReadConcern specifies the read concern for this operation.
func (d *Distinct) ReadConcern(readConcern *readconcern.ReadConcern) *Distinct {
if d == nil {
d = new(Distinct)
}
d.readConcern = readConcern
return d
}
// ReadPreference set the read preference used with this operation.
func (d *Distinct) ReadPreference(readPreference *readpref.ReadPref) *Distinct {
if d == nil {
d = new(Distinct)
}
d.readPreference = readPreference
return d
}
// ServerSelector sets the selector used to retrieve a server.
func (d *Distinct) ServerSelector(selector description.ServerSelector) *Distinct {
if d == nil {
d = new(Distinct)
}
d.selector = selector
return d
}
// Retry enables retryable mode for this operation. Retries are handled automatically in driver.Operation.Execute based
// on how the operation is set.
func (d *Distinct) Retry(retry driver.RetryMode) *Distinct {
if d == nil {
d = new(Distinct)
}
d.retry = &retry
return d
}
// ServerAPI sets the server API version for this operation.
func (d *Distinct) ServerAPI(serverAPI *driver.ServerAPIOptions) *Distinct {
if d == nil {
d = new(Distinct)
}
d.serverAPI = serverAPI
return d
}
// Timeout sets the timeout for this operation.
func (d *Distinct) Timeout(timeout *time.Duration) *Distinct {
if d == nil {
d = new(Distinct)
}
d.timeout = timeout
return d
}
@@ -0,0 +1,209 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"fmt"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// DropCollection performs a drop operation.
type DropCollection struct {
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
result DropCollectionResult
serverAPI *driver.ServerAPIOptions
}
// DropCollectionResult represents a dropCollection result returned by the server.
type DropCollectionResult struct {
// The number of indexes in the dropped collection.
NIndexesWas int32
// The namespace of the dropped collection.
Ns string
}
func buildDropCollectionResult(response bsoncore.Document) (DropCollectionResult, error) {
elements, err := response.Elements()
if err != nil {
return DropCollectionResult{}, err
}
dcr := DropCollectionResult{}
for _, element := range elements {
switch element.Key() {
case "nIndexesWas":
var ok bool
dcr.NIndexesWas, ok = element.Value().AsInt32OK()
if !ok {
return dcr, fmt.Errorf("response field 'nIndexesWas' is type int32, but received BSON type %s", element.Value().Type)
}
case "ns":
var ok bool
dcr.Ns, ok = element.Value().StringValueOK()
if !ok {
return dcr, fmt.Errorf("response field 'ns' is type string, but received BSON type %s", element.Value().Type)
}
}
}
return dcr, nil
}
// NewDropCollection constructs and returns a new DropCollection.
func NewDropCollection() *DropCollection {
return &DropCollection{}
}
// Result returns the result of executing this operation.
func (dc *DropCollection) Result() DropCollectionResult { return dc.result }
func (dc *DropCollection) processResponse(info driver.ResponseInfo) error {
var err error
dc.result, err = buildDropCollectionResult(info.ServerResponse)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (dc *DropCollection) Execute(ctx context.Context) error {
if dc.deployment == nil {
return errors.New("the DropCollection operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: dc.command,
ProcessResponseFn: dc.processResponse,
Client: dc.session,
Clock: dc.clock,
CommandMonitor: dc.monitor,
Crypt: dc.crypt,
Database: dc.database,
Deployment: dc.deployment,
Selector: dc.selector,
WriteConcern: dc.writeConcern,
ServerAPI: dc.serverAPI,
}.Execute(ctx, nil)
}
func (dc *DropCollection) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "drop", dc.collection)
return dst, nil
}
// Session sets the session for this operation.
func (dc *DropCollection) Session(session *session.Client) *DropCollection {
if dc == nil {
dc = new(DropCollection)
}
dc.session = session
return dc
}
// ClusterClock sets the cluster clock for this operation.
func (dc *DropCollection) ClusterClock(clock *session.ClusterClock) *DropCollection {
if dc == nil {
dc = new(DropCollection)
}
dc.clock = clock
return dc
}
// Collection sets the collection that this command will run against.
func (dc *DropCollection) Collection(collection string) *DropCollection {
if dc == nil {
dc = new(DropCollection)
}
dc.collection = collection
return dc
}
// CommandMonitor sets the monitor to use for APM events.
func (dc *DropCollection) CommandMonitor(monitor *event.CommandMonitor) *DropCollection {
if dc == nil {
dc = new(DropCollection)
}
dc.monitor = monitor
return dc
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (dc *DropCollection) Crypt(crypt driver.Crypt) *DropCollection {
if dc == nil {
dc = new(DropCollection)
}
dc.crypt = crypt
return dc
}
// Database sets the database to run this operation against.
func (dc *DropCollection) Database(database string) *DropCollection {
if dc == nil {
dc = new(DropCollection)
}
dc.database = database
return dc
}
// Deployment sets the deployment to use for this operation.
func (dc *DropCollection) Deployment(deployment driver.Deployment) *DropCollection {
if dc == nil {
dc = new(DropCollection)
}
dc.deployment = deployment
return dc
}
// ServerSelector sets the selector used to retrieve a server.
func (dc *DropCollection) ServerSelector(selector description.ServerSelector) *DropCollection {
if dc == nil {
dc = new(DropCollection)
}
dc.selector = selector
return dc
}
// WriteConcern sets the write concern for this operation.
func (dc *DropCollection) WriteConcern(writeConcern *writeconcern.WriteConcern) *DropCollection {
if dc == nil {
dc = new(DropCollection)
}
dc.writeConcern = writeConcern
return dc
}
// ServerAPI sets the server API version for this operation.
func (dc *DropCollection) ServerAPI(serverAPI *driver.ServerAPIOptions) *DropCollection {
if dc == nil {
dc = new(DropCollection)
}
dc.serverAPI = serverAPI
return dc
}
@@ -0,0 +1,154 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// DropDatabase performs a dropDatabase operation
type DropDatabase struct {
session *session.Client
clock *session.ClusterClock
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
serverAPI *driver.ServerAPIOptions
}
// NewDropDatabase constructs and returns a new DropDatabase.
func NewDropDatabase() *DropDatabase {
return &DropDatabase{}
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (dd *DropDatabase) Execute(ctx context.Context) error {
if dd.deployment == nil {
return errors.New("the DropDatabase operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: dd.command,
Client: dd.session,
Clock: dd.clock,
CommandMonitor: dd.monitor,
Crypt: dd.crypt,
Database: dd.database,
Deployment: dd.deployment,
Selector: dd.selector,
WriteConcern: dd.writeConcern,
ServerAPI: dd.serverAPI,
}.Execute(ctx, nil)
}
func (dd *DropDatabase) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendInt32Element(dst, "dropDatabase", 1)
return dst, nil
}
// Session sets the session for this operation.
func (dd *DropDatabase) Session(session *session.Client) *DropDatabase {
if dd == nil {
dd = new(DropDatabase)
}
dd.session = session
return dd
}
// ClusterClock sets the cluster clock for this operation.
func (dd *DropDatabase) ClusterClock(clock *session.ClusterClock) *DropDatabase {
if dd == nil {
dd = new(DropDatabase)
}
dd.clock = clock
return dd
}
// CommandMonitor sets the monitor to use for APM events.
func (dd *DropDatabase) CommandMonitor(monitor *event.CommandMonitor) *DropDatabase {
if dd == nil {
dd = new(DropDatabase)
}
dd.monitor = monitor
return dd
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (dd *DropDatabase) Crypt(crypt driver.Crypt) *DropDatabase {
if dd == nil {
dd = new(DropDatabase)
}
dd.crypt = crypt
return dd
}
// Database sets the database to run this operation against.
func (dd *DropDatabase) Database(database string) *DropDatabase {
if dd == nil {
dd = new(DropDatabase)
}
dd.database = database
return dd
}
// Deployment sets the deployment to use for this operation.
func (dd *DropDatabase) Deployment(deployment driver.Deployment) *DropDatabase {
if dd == nil {
dd = new(DropDatabase)
}
dd.deployment = deployment
return dd
}
// ServerSelector sets the selector used to retrieve a server.
func (dd *DropDatabase) ServerSelector(selector description.ServerSelector) *DropDatabase {
if dd == nil {
dd = new(DropDatabase)
}
dd.selector = selector
return dd
}
// WriteConcern sets the write concern for this operation.
func (dd *DropDatabase) WriteConcern(writeConcern *writeconcern.WriteConcern) *DropDatabase {
if dd == nil {
dd = new(DropDatabase)
}
dd.writeConcern = writeConcern
return dd
}
// ServerAPI sets the server API version for this operation.
func (dd *DropDatabase) ServerAPI(serverAPI *driver.ServerAPIOptions) *DropDatabase {
if dd == nil {
dd = new(DropDatabase)
}
dd.serverAPI = serverAPI
return dd
}
@@ -0,0 +1,246 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"fmt"
"time"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// DropIndexes performs an dropIndexes operation.
type DropIndexes struct {
index *string
maxTimeMS *int64
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
result DropIndexesResult
serverAPI *driver.ServerAPIOptions
timeout *time.Duration
}
// DropIndexesResult represents a dropIndexes result returned by the server.
type DropIndexesResult struct {
// Number of indexes that existed before the drop was executed.
NIndexesWas int32
}
func buildDropIndexesResult(response bsoncore.Document) (DropIndexesResult, error) {
elements, err := response.Elements()
if err != nil {
return DropIndexesResult{}, err
}
dir := DropIndexesResult{}
for _, element := range elements {
switch element.Key() {
case "nIndexesWas":
var ok bool
dir.NIndexesWas, ok = element.Value().AsInt32OK()
if !ok {
return dir, fmt.Errorf("response field 'nIndexesWas' is type int32, but received BSON type %s", element.Value().Type)
}
}
}
return dir, nil
}
// NewDropIndexes constructs and returns a new DropIndexes.
func NewDropIndexes(index string) *DropIndexes {
return &DropIndexes{
index: &index,
}
}
// Result returns the result of executing this operation.
func (di *DropIndexes) Result() DropIndexesResult { return di.result }
func (di *DropIndexes) processResponse(info driver.ResponseInfo) error {
var err error
di.result, err = buildDropIndexesResult(info.ServerResponse)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (di *DropIndexes) Execute(ctx context.Context) error {
if di.deployment == nil {
return errors.New("the DropIndexes operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: di.command,
ProcessResponseFn: di.processResponse,
Client: di.session,
Clock: di.clock,
CommandMonitor: di.monitor,
Crypt: di.crypt,
Database: di.database,
Deployment: di.deployment,
Selector: di.selector,
WriteConcern: di.writeConcern,
ServerAPI: di.serverAPI,
Timeout: di.timeout,
}.Execute(ctx, nil)
}
func (di *DropIndexes) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "dropIndexes", di.collection)
if di.index != nil {
dst = bsoncore.AppendStringElement(dst, "index", *di.index)
}
// Only append specified maxTimeMS if timeout is not also specified.
if di.maxTimeMS != nil && di.timeout == nil {
dst = bsoncore.AppendInt64Element(dst, "maxTimeMS", *di.maxTimeMS)
}
return dst, nil
}
// Index specifies the name of the index to drop. If '*' is specified, all indexes will be dropped.
//
func (di *DropIndexes) Index(index string) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.index = &index
return di
}
// MaxTimeMS specifies the maximum amount of time to allow the query to run.
func (di *DropIndexes) MaxTimeMS(maxTimeMS int64) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.maxTimeMS = &maxTimeMS
return di
}
// Session sets the session for this operation.
func (di *DropIndexes) Session(session *session.Client) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.session = session
return di
}
// ClusterClock sets the cluster clock for this operation.
func (di *DropIndexes) ClusterClock(clock *session.ClusterClock) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.clock = clock
return di
}
// Collection sets the collection that this command will run against.
func (di *DropIndexes) Collection(collection string) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.collection = collection
return di
}
// CommandMonitor sets the monitor to use for APM events.
func (di *DropIndexes) CommandMonitor(monitor *event.CommandMonitor) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.monitor = monitor
return di
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (di *DropIndexes) Crypt(crypt driver.Crypt) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.crypt = crypt
return di
}
// Database sets the database to run this operation against.
func (di *DropIndexes) Database(database string) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.database = database
return di
}
// Deployment sets the deployment to use for this operation.
func (di *DropIndexes) Deployment(deployment driver.Deployment) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.deployment = deployment
return di
}
// ServerSelector sets the selector used to retrieve a server.
func (di *DropIndexes) ServerSelector(selector description.ServerSelector) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.selector = selector
return di
}
// WriteConcern sets the write concern for this operation.
func (di *DropIndexes) WriteConcern(writeConcern *writeconcern.WriteConcern) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.writeConcern = writeConcern
return di
}
// ServerAPI sets the server API version for this operation.
func (di *DropIndexes) ServerAPI(serverAPI *driver.ServerAPIOptions) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.serverAPI = serverAPI
return di
}
// Timeout sets the timeout for this operation.
func (di *DropIndexes) Timeout(timeout *time.Duration) *DropIndexes {
if di == nil {
di = new(DropIndexes)
}
di.timeout = timeout
return di
}
@@ -0,0 +1,161 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// EndSessions performs an endSessions operation.
type EndSessions struct {
sessionIDs bsoncore.Document
session *session.Client
clock *session.ClusterClock
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
selector description.ServerSelector
serverAPI *driver.ServerAPIOptions
}
// NewEndSessions constructs and returns a new EndSessions.
func NewEndSessions(sessionIDs bsoncore.Document) *EndSessions {
return &EndSessions{
sessionIDs: sessionIDs,
}
}
func (es *EndSessions) processResponse(driver.ResponseInfo) error {
var err error
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (es *EndSessions) Execute(ctx context.Context) error {
if es.deployment == nil {
return errors.New("the EndSessions operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: es.command,
ProcessResponseFn: es.processResponse,
Client: es.session,
Clock: es.clock,
CommandMonitor: es.monitor,
Crypt: es.crypt,
Database: es.database,
Deployment: es.deployment,
Selector: es.selector,
ServerAPI: es.serverAPI,
}.Execute(ctx, nil)
}
func (es *EndSessions) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
if es.sessionIDs != nil {
dst = bsoncore.AppendArrayElement(dst, "endSessions", es.sessionIDs)
}
return dst, nil
}
// SessionIDs specifies the sessions to be expired.
func (es *EndSessions) SessionIDs(sessionIDs bsoncore.Document) *EndSessions {
if es == nil {
es = new(EndSessions)
}
es.sessionIDs = sessionIDs
return es
}
// Session sets the session for this operation.
func (es *EndSessions) Session(session *session.Client) *EndSessions {
if es == nil {
es = new(EndSessions)
}
es.session = session
return es
}
// ClusterClock sets the cluster clock for this operation.
func (es *EndSessions) ClusterClock(clock *session.ClusterClock) *EndSessions {
if es == nil {
es = new(EndSessions)
}
es.clock = clock
return es
}
// CommandMonitor sets the monitor to use for APM events.
func (es *EndSessions) CommandMonitor(monitor *event.CommandMonitor) *EndSessions {
if es == nil {
es = new(EndSessions)
}
es.monitor = monitor
return es
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (es *EndSessions) Crypt(crypt driver.Crypt) *EndSessions {
if es == nil {
es = new(EndSessions)
}
es.crypt = crypt
return es
}
// Database sets the database to run this operation against.
func (es *EndSessions) Database(database string) *EndSessions {
if es == nil {
es = new(EndSessions)
}
es.database = database
return es
}
// Deployment sets the deployment to use for this operation.
func (es *EndSessions) Deployment(deployment driver.Deployment) *EndSessions {
if es == nil {
es = new(EndSessions)
}
es.deployment = deployment
return es
}
// ServerSelector sets the selector used to retrieve a server.
func (es *EndSessions) ServerSelector(selector description.ServerSelector) *EndSessions {
if es == nil {
es = new(EndSessions)
}
es.selector = selector
return es
}
// ServerAPI sets the server API version for this operation.
func (es *EndSessions) ServerAPI(serverAPI *driver.ServerAPIOptions) *EndSessions {
if es == nil {
es = new(EndSessions)
}
es.serverAPI = serverAPI
return es
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import "errors"
var (
errUnacknowledgedHint = errors.New("the 'hint' command parameter cannot be used with unacknowledged writes")
)
+551
View File
@@ -0,0 +1,551 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/readconcern"
"go.mongodb.org/mongo-driver/mongo/readpref"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Find performs a find operation.
type Find struct {
allowDiskUse *bool
allowPartialResults *bool
awaitData *bool
batchSize *int32
collation bsoncore.Document
comment *string
filter bsoncore.Document
hint bsoncore.Value
let bsoncore.Document
limit *int64
max bsoncore.Document
maxTimeMS *int64
min bsoncore.Document
noCursorTimeout *bool
oplogReplay *bool
projection bsoncore.Document
returnKey *bool
showRecordID *bool
singleBatch *bool
skip *int64
snapshot *bool
sort bsoncore.Document
tailable *bool
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
readConcern *readconcern.ReadConcern
readPreference *readpref.ReadPref
selector description.ServerSelector
retry *driver.RetryMode
result driver.CursorResponse
serverAPI *driver.ServerAPIOptions
timeout *time.Duration
}
// NewFind constructs and returns a new Find.
func NewFind(filter bsoncore.Document) *Find {
return &Find{
filter: filter,
}
}
// Result returns the result of executing this operation.
func (f *Find) Result(opts driver.CursorOptions) (*driver.BatchCursor, error) {
opts.ServerAPI = f.serverAPI
return driver.NewBatchCursor(f.result, f.session, f.clock, opts)
}
func (f *Find) processResponse(info driver.ResponseInfo) error {
var err error
f.result, err = driver.NewCursorResponse(info)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (f *Find) Execute(ctx context.Context) error {
if f.deployment == nil {
return errors.New("the Find operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: f.command,
ProcessResponseFn: f.processResponse,
RetryMode: f.retry,
Type: driver.Read,
Client: f.session,
Clock: f.clock,
CommandMonitor: f.monitor,
Crypt: f.crypt,
Database: f.database,
Deployment: f.deployment,
ReadConcern: f.readConcern,
ReadPreference: f.readPreference,
Selector: f.selector,
Legacy: driver.LegacyFind,
ServerAPI: f.serverAPI,
Timeout: f.timeout,
}.Execute(ctx, nil)
}
func (f *Find) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "find", f.collection)
if f.allowDiskUse != nil {
if desc.WireVersion == nil || !desc.WireVersion.Includes(4) {
return nil, errors.New("the 'allowDiskUse' command parameter requires a minimum server wire version of 4")
}
dst = bsoncore.AppendBooleanElement(dst, "allowDiskUse", *f.allowDiskUse)
}
if f.allowPartialResults != nil {
dst = bsoncore.AppendBooleanElement(dst, "allowPartialResults", *f.allowPartialResults)
}
if f.awaitData != nil {
dst = bsoncore.AppendBooleanElement(dst, "awaitData", *f.awaitData)
}
if f.batchSize != nil {
dst = bsoncore.AppendInt32Element(dst, "batchSize", *f.batchSize)
}
if f.collation != nil {
if desc.WireVersion == nil || !desc.WireVersion.Includes(5) {
return nil, errors.New("the 'collation' command parameter requires a minimum server wire version of 5")
}
dst = bsoncore.AppendDocumentElement(dst, "collation", f.collation)
}
if f.comment != nil {
dst = bsoncore.AppendStringElement(dst, "comment", *f.comment)
}
if f.filter != nil {
dst = bsoncore.AppendDocumentElement(dst, "filter", f.filter)
}
if f.hint.Type != bsontype.Type(0) {
dst = bsoncore.AppendValueElement(dst, "hint", f.hint)
}
if f.let != nil {
dst = bsoncore.AppendDocumentElement(dst, "let", f.let)
}
if f.limit != nil {
dst = bsoncore.AppendInt64Element(dst, "limit", *f.limit)
}
if f.max != nil {
dst = bsoncore.AppendDocumentElement(dst, "max", f.max)
}
// Only append specified maxTimeMS if timeout is not also specified.
if f.maxTimeMS != nil && f.timeout == nil {
dst = bsoncore.AppendInt64Element(dst, "maxTimeMS", *f.maxTimeMS)
}
if f.min != nil {
dst = bsoncore.AppendDocumentElement(dst, "min", f.min)
}
if f.noCursorTimeout != nil {
dst = bsoncore.AppendBooleanElement(dst, "noCursorTimeout", *f.noCursorTimeout)
}
if f.oplogReplay != nil {
dst = bsoncore.AppendBooleanElement(dst, "oplogReplay", *f.oplogReplay)
}
if f.projection != nil {
dst = bsoncore.AppendDocumentElement(dst, "projection", f.projection)
}
if f.returnKey != nil {
dst = bsoncore.AppendBooleanElement(dst, "returnKey", *f.returnKey)
}
if f.showRecordID != nil {
dst = bsoncore.AppendBooleanElement(dst, "showRecordId", *f.showRecordID)
}
if f.singleBatch != nil {
dst = bsoncore.AppendBooleanElement(dst, "singleBatch", *f.singleBatch)
}
if f.skip != nil {
dst = bsoncore.AppendInt64Element(dst, "skip", *f.skip)
}
if f.snapshot != nil {
dst = bsoncore.AppendBooleanElement(dst, "snapshot", *f.snapshot)
}
if f.sort != nil {
dst = bsoncore.AppendDocumentElement(dst, "sort", f.sort)
}
if f.tailable != nil {
dst = bsoncore.AppendBooleanElement(dst, "tailable", *f.tailable)
}
return dst, nil
}
// AllowDiskUse when true allows temporary data to be written to disk during the find command."
func (f *Find) AllowDiskUse(allowDiskUse bool) *Find {
if f == nil {
f = new(Find)
}
f.allowDiskUse = &allowDiskUse
return f
}
// AllowPartialResults when true allows partial results to be returned if some shards are down.
func (f *Find) AllowPartialResults(allowPartialResults bool) *Find {
if f == nil {
f = new(Find)
}
f.allowPartialResults = &allowPartialResults
return f
}
// AwaitData when true makes a cursor block before returning when no data is available.
func (f *Find) AwaitData(awaitData bool) *Find {
if f == nil {
f = new(Find)
}
f.awaitData = &awaitData
return f
}
// BatchSize specifies the number of documents to return in every batch.
func (f *Find) BatchSize(batchSize int32) *Find {
if f == nil {
f = new(Find)
}
f.batchSize = &batchSize
return f
}
// Collation specifies a collation to be used.
func (f *Find) Collation(collation bsoncore.Document) *Find {
if f == nil {
f = new(Find)
}
f.collation = collation
return f
}
// Comment sets a string to help trace an operation.
func (f *Find) Comment(comment string) *Find {
if f == nil {
f = new(Find)
}
f.comment = &comment
return f
}
// Filter determines what results are returned from find.
func (f *Find) Filter(filter bsoncore.Document) *Find {
if f == nil {
f = new(Find)
}
f.filter = filter
return f
}
// Hint specifies the index to use.
func (f *Find) Hint(hint bsoncore.Value) *Find {
if f == nil {
f = new(Find)
}
f.hint = hint
return f
}
// Let specifies the let document to use. This option is only valid for server versions 5.0 and above.
func (f *Find) Let(let bsoncore.Document) *Find {
if f == nil {
f = new(Find)
}
f.let = let
return f
}
// Limit sets a limit on the number of documents to return.
func (f *Find) Limit(limit int64) *Find {
if f == nil {
f = new(Find)
}
f.limit = &limit
return f
}
// Max sets an exclusive upper bound for a specific index.
func (f *Find) Max(max bsoncore.Document) *Find {
if f == nil {
f = new(Find)
}
f.max = max
return f
}
// MaxTimeMS specifies the maximum amount of time to allow the query to run.
func (f *Find) MaxTimeMS(maxTimeMS int64) *Find {
if f == nil {
f = new(Find)
}
f.maxTimeMS = &maxTimeMS
return f
}
// Min sets an inclusive lower bound for a specific index.
func (f *Find) Min(min bsoncore.Document) *Find {
if f == nil {
f = new(Find)
}
f.min = min
return f
}
// NoCursorTimeout when true prevents cursor from timing out after an inactivity period.
func (f *Find) NoCursorTimeout(noCursorTimeout bool) *Find {
if f == nil {
f = new(Find)
}
f.noCursorTimeout = &noCursorTimeout
return f
}
// OplogReplay when true replays a replica set's oplog.
func (f *Find) OplogReplay(oplogReplay bool) *Find {
if f == nil {
f = new(Find)
}
f.oplogReplay = &oplogReplay
return f
}
// Projection limits the fields returned for all documents.
func (f *Find) Projection(projection bsoncore.Document) *Find {
if f == nil {
f = new(Find)
}
f.projection = projection
return f
}
// ReturnKey when true returns index keys for all result documents.
func (f *Find) ReturnKey(returnKey bool) *Find {
if f == nil {
f = new(Find)
}
f.returnKey = &returnKey
return f
}
// ShowRecordID when true adds a $recordId field with the record identifier to returned documents.
func (f *Find) ShowRecordID(showRecordID bool) *Find {
if f == nil {
f = new(Find)
}
f.showRecordID = &showRecordID
return f
}
// SingleBatch specifies whether the results should be returned in a single batch.
func (f *Find) SingleBatch(singleBatch bool) *Find {
if f == nil {
f = new(Find)
}
f.singleBatch = &singleBatch
return f
}
// Skip specifies the number of documents to skip before returning.
func (f *Find) Skip(skip int64) *Find {
if f == nil {
f = new(Find)
}
f.skip = &skip
return f
}
// Snapshot prevents the cursor from returning a document more than once because of an intervening write operation.
func (f *Find) Snapshot(snapshot bool) *Find {
if f == nil {
f = new(Find)
}
f.snapshot = &snapshot
return f
}
// Sort specifies the order in which to return results.
func (f *Find) Sort(sort bsoncore.Document) *Find {
if f == nil {
f = new(Find)
}
f.sort = sort
return f
}
// Tailable keeps a cursor open and resumable after the last data has been retrieved.
func (f *Find) Tailable(tailable bool) *Find {
if f == nil {
f = new(Find)
}
f.tailable = &tailable
return f
}
// Session sets the session for this operation.
func (f *Find) Session(session *session.Client) *Find {
if f == nil {
f = new(Find)
}
f.session = session
return f
}
// ClusterClock sets the cluster clock for this operation.
func (f *Find) ClusterClock(clock *session.ClusterClock) *Find {
if f == nil {
f = new(Find)
}
f.clock = clock
return f
}
// Collection sets the collection that this command will run against.
func (f *Find) Collection(collection string) *Find {
if f == nil {
f = new(Find)
}
f.collection = collection
return f
}
// CommandMonitor sets the monitor to use for APM events.
func (f *Find) CommandMonitor(monitor *event.CommandMonitor) *Find {
if f == nil {
f = new(Find)
}
f.monitor = monitor
return f
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (f *Find) Crypt(crypt driver.Crypt) *Find {
if f == nil {
f = new(Find)
}
f.crypt = crypt
return f
}
// Database sets the database to run this operation against.
func (f *Find) Database(database string) *Find {
if f == nil {
f = new(Find)
}
f.database = database
return f
}
// Deployment sets the deployment to use for this operation.
func (f *Find) Deployment(deployment driver.Deployment) *Find {
if f == nil {
f = new(Find)
}
f.deployment = deployment
return f
}
// ReadConcern specifies the read concern for this operation.
func (f *Find) ReadConcern(readConcern *readconcern.ReadConcern) *Find {
if f == nil {
f = new(Find)
}
f.readConcern = readConcern
return f
}
// ReadPreference set the read preference used with this operation.
func (f *Find) ReadPreference(readPreference *readpref.ReadPref) *Find {
if f == nil {
f = new(Find)
}
f.readPreference = readPreference
return f
}
// ServerSelector sets the selector used to retrieve a server.
func (f *Find) ServerSelector(selector description.ServerSelector) *Find {
if f == nil {
f = new(Find)
}
f.selector = selector
return f
}
// Retry enables retryable mode for this operation. Retries are handled automatically in driver.Operation.Execute based
// on how the operation is set.
func (f *Find) Retry(retry driver.RetryMode) *Find {
if f == nil {
f = new(Find)
}
f.retry = &retry
return f
}
// ServerAPI sets the server API version for this operation.
func (f *Find) ServerAPI(serverAPI *driver.ServerAPIOptions) *Find {
if f == nil {
f = new(Find)
}
f.serverAPI = serverAPI
return f
}
// Timeout sets the timeout for this operation.
func (f *Find) Timeout(timeout *time.Duration) *Find {
if f == nil {
f = new(Find)
}
f.timeout = timeout
return f
}
@@ -0,0 +1,483 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"fmt"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// FindAndModify performs a findAndModify operation.
type FindAndModify struct {
arrayFilters bsoncore.Document
bypassDocumentValidation *bool
collation bsoncore.Document
comment bsoncore.Value
fields bsoncore.Document
maxTimeMS *int64
newDocument *bool
query bsoncore.Document
remove *bool
sort bsoncore.Document
update bsoncore.Value
upsert *bool
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
database string
deployment driver.Deployment
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
retry *driver.RetryMode
crypt driver.Crypt
hint bsoncore.Value
serverAPI *driver.ServerAPIOptions
let bsoncore.Document
timeout *time.Duration
result FindAndModifyResult
}
// LastErrorObject represents information about updates and upserts returned by the server.
type LastErrorObject struct {
// True if an update modified an existing document
UpdatedExisting bool
// Object ID of the upserted document.
Upserted interface{}
}
// FindAndModifyResult represents a findAndModify result returned by the server.
type FindAndModifyResult struct {
// Either the old or modified document, depending on the value of the new parameter.
Value bsoncore.Document
// Contains information about updates and upserts.
LastErrorObject LastErrorObject
}
func buildFindAndModifyResult(response bsoncore.Document) (FindAndModifyResult, error) {
elements, err := response.Elements()
if err != nil {
return FindAndModifyResult{}, err
}
famr := FindAndModifyResult{}
for _, element := range elements {
switch element.Key() {
case "value":
var ok bool
famr.Value, ok = element.Value().DocumentOK()
// The 'value' field returned by a FindAndModify can be null in the case that no document was found.
if element.Value().Type != bsontype.Null && !ok {
return famr, fmt.Errorf("response field 'value' is type document or null, but received BSON type %s", element.Value().Type)
}
case "lastErrorObject":
valDoc, ok := element.Value().DocumentOK()
if !ok {
return famr, fmt.Errorf("response field 'lastErrorObject' is type document, but received BSON type %s", element.Value().Type)
}
var leo LastErrorObject
if err = bson.Unmarshal(valDoc, &leo); err != nil {
return famr, err
}
famr.LastErrorObject = leo
}
}
return famr, nil
}
// NewFindAndModify constructs and returns a new FindAndModify.
func NewFindAndModify(query bsoncore.Document) *FindAndModify {
return &FindAndModify{
query: query,
}
}
// Result returns the result of executing this operation.
func (fam *FindAndModify) Result() FindAndModifyResult { return fam.result }
func (fam *FindAndModify) processResponse(info driver.ResponseInfo) error {
var err error
fam.result, err = buildFindAndModifyResult(info.ServerResponse)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (fam *FindAndModify) Execute(ctx context.Context) error {
if fam.deployment == nil {
return errors.New("the FindAndModify operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: fam.command,
ProcessResponseFn: fam.processResponse,
RetryMode: fam.retry,
Type: driver.Write,
Client: fam.session,
Clock: fam.clock,
CommandMonitor: fam.monitor,
Database: fam.database,
Deployment: fam.deployment,
Selector: fam.selector,
WriteConcern: fam.writeConcern,
Crypt: fam.crypt,
ServerAPI: fam.serverAPI,
Timeout: fam.timeout,
}.Execute(ctx, nil)
}
func (fam *FindAndModify) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "findAndModify", fam.collection)
if fam.arrayFilters != nil {
if desc.WireVersion == nil || !desc.WireVersion.Includes(6) {
return nil, errors.New("the 'arrayFilters' command parameter requires a minimum server wire version of 6")
}
dst = bsoncore.AppendArrayElement(dst, "arrayFilters", fam.arrayFilters)
}
if fam.bypassDocumentValidation != nil {
dst = bsoncore.AppendBooleanElement(dst, "bypassDocumentValidation", *fam.bypassDocumentValidation)
}
if fam.collation != nil {
if desc.WireVersion == nil || !desc.WireVersion.Includes(5) {
return nil, errors.New("the 'collation' command parameter requires a minimum server wire version of 5")
}
dst = bsoncore.AppendDocumentElement(dst, "collation", fam.collation)
}
if fam.comment.Type != bsontype.Type(0) {
dst = bsoncore.AppendValueElement(dst, "comment", fam.comment)
}
if fam.fields != nil {
dst = bsoncore.AppendDocumentElement(dst, "fields", fam.fields)
}
// Only append specified maxTimeMS if timeout is not also specified.
if fam.maxTimeMS != nil && fam.timeout == nil {
dst = bsoncore.AppendInt64Element(dst, "maxTimeMS", *fam.maxTimeMS)
}
if fam.newDocument != nil {
dst = bsoncore.AppendBooleanElement(dst, "new", *fam.newDocument)
}
if fam.query != nil {
dst = bsoncore.AppendDocumentElement(dst, "query", fam.query)
}
if fam.remove != nil {
dst = bsoncore.AppendBooleanElement(dst, "remove", *fam.remove)
}
if fam.sort != nil {
dst = bsoncore.AppendDocumentElement(dst, "sort", fam.sort)
}
if fam.update.Data != nil {
dst = bsoncore.AppendValueElement(dst, "update", fam.update)
}
if fam.upsert != nil {
dst = bsoncore.AppendBooleanElement(dst, "upsert", *fam.upsert)
}
if fam.hint.Type != bsontype.Type(0) {
if desc.WireVersion == nil || !desc.WireVersion.Includes(8) {
return nil, errors.New("the 'hint' command parameter requires a minimum server wire version of 8")
}
if !fam.writeConcern.Acknowledged() {
return nil, errUnacknowledgedHint
}
dst = bsoncore.AppendValueElement(dst, "hint", fam.hint)
}
if fam.let != nil {
dst = bsoncore.AppendDocumentElement(dst, "let", fam.let)
}
return dst, nil
}
// ArrayFilters specifies an array of filter documents that determines which array elements to modify for an update operation on an array field.
func (fam *FindAndModify) ArrayFilters(arrayFilters bsoncore.Document) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.arrayFilters = arrayFilters
return fam
}
// BypassDocumentValidation specifies if document validation can be skipped when executing the operation.
func (fam *FindAndModify) BypassDocumentValidation(bypassDocumentValidation bool) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.bypassDocumentValidation = &bypassDocumentValidation
return fam
}
// Collation specifies a collation to be used.
func (fam *FindAndModify) Collation(collation bsoncore.Document) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.collation = collation
return fam
}
// Comment sets a value to help trace an operation.
func (fam *FindAndModify) Comment(comment bsoncore.Value) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.comment = comment
return fam
}
// Fields specifies a subset of fields to return.
func (fam *FindAndModify) Fields(fields bsoncore.Document) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.fields = fields
return fam
}
// MaxTimeMS specifies the maximum amount of time to allow the operation to run.
func (fam *FindAndModify) MaxTimeMS(maxTimeMS int64) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.maxTimeMS = &maxTimeMS
return fam
}
// NewDocument specifies whether to return the modified document or the original. Defaults to false (return original).
func (fam *FindAndModify) NewDocument(newDocument bool) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.newDocument = &newDocument
return fam
}
// Query specifies the selection criteria for the modification.
func (fam *FindAndModify) Query(query bsoncore.Document) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.query = query
return fam
}
// Remove specifies that the matched document should be removed. Defaults to false.
func (fam *FindAndModify) Remove(remove bool) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.remove = &remove
return fam
}
// Sort determines which document the operation modifies if the query matches multiple documents.The first document matched by the sort order will be modified.
//
func (fam *FindAndModify) Sort(sort bsoncore.Document) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.sort = sort
return fam
}
// Update specifies the update document to perform on the matched document.
func (fam *FindAndModify) Update(update bsoncore.Value) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.update = update
return fam
}
// Upsert specifies whether or not to create a new document if no documents match the query when doing an update. Defaults to false.
func (fam *FindAndModify) Upsert(upsert bool) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.upsert = &upsert
return fam
}
// Session sets the session for this operation.
func (fam *FindAndModify) Session(session *session.Client) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.session = session
return fam
}
// ClusterClock sets the cluster clock for this operation.
func (fam *FindAndModify) ClusterClock(clock *session.ClusterClock) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.clock = clock
return fam
}
// Collection sets the collection that this command will run against.
func (fam *FindAndModify) Collection(collection string) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.collection = collection
return fam
}
// CommandMonitor sets the monitor to use for APM events.
func (fam *FindAndModify) CommandMonitor(monitor *event.CommandMonitor) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.monitor = monitor
return fam
}
// Database sets the database to run this operation against.
func (fam *FindAndModify) Database(database string) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.database = database
return fam
}
// Deployment sets the deployment to use for this operation.
func (fam *FindAndModify) Deployment(deployment driver.Deployment) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.deployment = deployment
return fam
}
// ServerSelector sets the selector used to retrieve a server.
func (fam *FindAndModify) ServerSelector(selector description.ServerSelector) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.selector = selector
return fam
}
// WriteConcern sets the write concern for this operation.
func (fam *FindAndModify) WriteConcern(writeConcern *writeconcern.WriteConcern) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.writeConcern = writeConcern
return fam
}
// Retry enables retryable writes for this operation. Retries are not handled automatically,
// instead a boolean is returned from Execute and SelectAndExecute that indicates if the
// operation can be retried. Retrying is handled by calling RetryExecute.
func (fam *FindAndModify) Retry(retry driver.RetryMode) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.retry = &retry
return fam
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (fam *FindAndModify) Crypt(crypt driver.Crypt) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.crypt = crypt
return fam
}
// Hint specifies the index to use.
func (fam *FindAndModify) Hint(hint bsoncore.Value) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.hint = hint
return fam
}
// ServerAPI sets the server API version for this operation.
func (fam *FindAndModify) ServerAPI(serverAPI *driver.ServerAPIOptions) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.serverAPI = serverAPI
return fam
}
// Let specifies the let document to use. This option is only valid for server versions 5.0 and above.
func (fam *FindAndModify) Let(let bsoncore.Document) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.let = let
return fam
}
// Timeout sets the timeout for this operation.
func (fam *FindAndModify) Timeout(timeout *time.Duration) *FindAndModify {
if fam == nil {
fam = new(FindAndModify)
}
fam.timeout = timeout
return fam
}
+258
View File
@@ -0,0 +1,258 @@
// Copyright (C) MongoDB, Inc. 2021-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"runtime"
"strconv"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/internal"
"go.mongodb.org/mongo-driver/mongo/address"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/version"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Hello is used to run the handshake operation.
type Hello struct {
appname string
compressors []string
saslSupportedMechs string
d driver.Deployment
clock *session.ClusterClock
speculativeAuth bsoncore.Document
topologyVersion *description.TopologyVersion
maxAwaitTimeMS *int64
serverAPI *driver.ServerAPIOptions
loadBalanced bool
res bsoncore.Document
}
var _ driver.Handshaker = (*Hello)(nil)
// NewHello constructs a Hello.
func NewHello() *Hello { return &Hello{} }
// AppName sets the application name in the client metadata sent in this operation.
func (h *Hello) AppName(appname string) *Hello {
h.appname = appname
return h
}
// ClusterClock sets the cluster clock for this operation.
func (h *Hello) ClusterClock(clock *session.ClusterClock) *Hello {
if h == nil {
h = new(Hello)
}
h.clock = clock
return h
}
// Compressors sets the compressors that can be used.
func (h *Hello) Compressors(compressors []string) *Hello {
h.compressors = compressors
return h
}
// SASLSupportedMechs retrieves the supported SASL mechanism for the given user when this operation
// is run.
func (h *Hello) SASLSupportedMechs(username string) *Hello {
h.saslSupportedMechs = username
return h
}
// Deployment sets the Deployment for this operation.
func (h *Hello) Deployment(d driver.Deployment) *Hello {
h.d = d
return h
}
// SpeculativeAuthenticate sets the document to be used for speculative authentication.
func (h *Hello) SpeculativeAuthenticate(doc bsoncore.Document) *Hello {
h.speculativeAuth = doc
return h
}
// TopologyVersion sets the TopologyVersion to be used for heartbeats.
func (h *Hello) TopologyVersion(tv *description.TopologyVersion) *Hello {
h.topologyVersion = tv
return h
}
// MaxAwaitTimeMS sets the maximum time for the server to wait for topology changes during a heartbeat.
func (h *Hello) MaxAwaitTimeMS(awaitTime int64) *Hello {
h.maxAwaitTimeMS = &awaitTime
return h
}
// ServerAPI sets the server API version for this operation.
func (h *Hello) ServerAPI(serverAPI *driver.ServerAPIOptions) *Hello {
h.serverAPI = serverAPI
return h
}
// LoadBalanced specifies whether or not this operation is being sent over a connection to a load balanced cluster.
func (h *Hello) LoadBalanced(lb bool) *Hello {
h.loadBalanced = lb
return h
}
// Result returns the result of executing this operation.
func (h *Hello) Result(addr address.Address) description.Server {
return description.NewServer(addr, bson.Raw(h.res))
}
// handshakeCommand appends all necessary command fields as well as client metadata, SASL supported mechs, and compression.
func (h *Hello) handshakeCommand(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst, err := h.command(dst, desc)
if err != nil {
return dst, err
}
if h.saslSupportedMechs != "" {
dst = bsoncore.AppendStringElement(dst, "saslSupportedMechs", h.saslSupportedMechs)
}
if h.speculativeAuth != nil {
dst = bsoncore.AppendDocumentElement(dst, "speculativeAuthenticate", h.speculativeAuth)
}
var idx int32
idx, dst = bsoncore.AppendArrayElementStart(dst, "compression")
for i, compressor := range h.compressors {
dst = bsoncore.AppendStringElement(dst, strconv.Itoa(i), compressor)
}
dst, _ = bsoncore.AppendArrayEnd(dst, idx)
// append client metadata
idx, dst = bsoncore.AppendDocumentElementStart(dst, "client")
didx, dst := bsoncore.AppendDocumentElementStart(dst, "driver")
dst = bsoncore.AppendStringElement(dst, "name", "mongo-go-driver")
dst = bsoncore.AppendStringElement(dst, "version", version.Driver)
dst, _ = bsoncore.AppendDocumentEnd(dst, didx)
didx, dst = bsoncore.AppendDocumentElementStart(dst, "os")
dst = bsoncore.AppendStringElement(dst, "type", runtime.GOOS)
dst = bsoncore.AppendStringElement(dst, "architecture", runtime.GOARCH)
dst, _ = bsoncore.AppendDocumentEnd(dst, didx)
dst = bsoncore.AppendStringElement(dst, "platform", runtime.Version())
if h.appname != "" {
didx, dst = bsoncore.AppendDocumentElementStart(dst, "application")
dst = bsoncore.AppendStringElement(dst, "name", h.appname)
dst, _ = bsoncore.AppendDocumentEnd(dst, didx)
}
dst, _ = bsoncore.AppendDocumentEnd(dst, idx)
return dst, nil
}
// command appends all necessary command fields.
func (h *Hello) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
// Use "hello" if topology is LoadBalanced, API version is declared or server
// has responded with "helloOk". Otherwise, use legacy hello.
if desc.Kind == description.LoadBalanced || h.serverAPI != nil || desc.Server.HelloOK {
dst = bsoncore.AppendInt32Element(dst, "hello", 1)
} else {
dst = bsoncore.AppendInt32Element(dst, internal.LegacyHello, 1)
}
dst = bsoncore.AppendBooleanElement(dst, "helloOk", true)
if tv := h.topologyVersion; tv != nil {
var tvIdx int32
tvIdx, dst = bsoncore.AppendDocumentElementStart(dst, "topologyVersion")
dst = bsoncore.AppendObjectIDElement(dst, "processId", tv.ProcessID)
dst = bsoncore.AppendInt64Element(dst, "counter", tv.Counter)
dst, _ = bsoncore.AppendDocumentEnd(dst, tvIdx)
}
if h.maxAwaitTimeMS != nil {
dst = bsoncore.AppendInt64Element(dst, "maxAwaitTimeMS", *h.maxAwaitTimeMS)
}
if h.loadBalanced {
// The loadBalanced parameter should only be added if it's true. We should never explicitly send
// loadBalanced=false per the load balancing spec.
dst = bsoncore.AppendBooleanElement(dst, "loadBalanced", true)
}
return dst, nil
}
// Execute runs this operation.
func (h *Hello) Execute(ctx context.Context) error {
if h.d == nil {
return errors.New("a Hello must have a Deployment set before Execute can be called")
}
return h.createOperation().Execute(ctx, nil)
}
// StreamResponse gets the next streaming Hello response from the server.
func (h *Hello) StreamResponse(ctx context.Context, conn driver.StreamerConnection) error {
return h.createOperation().ExecuteExhaust(ctx, conn, nil)
}
func (h *Hello) createOperation() driver.Operation {
return driver.Operation{
Clock: h.clock,
CommandFn: h.command,
Database: "admin",
Deployment: h.d,
ProcessResponseFn: func(info driver.ResponseInfo) error {
h.res = info.ServerResponse
return nil
},
ServerAPI: h.serverAPI,
}
}
// GetHandshakeInformation performs the MongoDB handshake for the provided connection and returns the relevant
// information about the server. This function implements the driver.Handshaker interface.
func (h *Hello) GetHandshakeInformation(ctx context.Context, _ address.Address, c driver.Connection) (driver.HandshakeInformation, error) {
err := driver.Operation{
Clock: h.clock,
CommandFn: h.handshakeCommand,
Deployment: driver.SingleConnectionDeployment{c},
Database: "admin",
ProcessResponseFn: func(info driver.ResponseInfo) error {
h.res = info.ServerResponse
return nil
},
ServerAPI: h.serverAPI,
}.Execute(ctx, nil)
if err != nil {
return driver.HandshakeInformation{}, err
}
info := driver.HandshakeInformation{
Description: h.Result(c.Address()),
}
if speculativeAuthenticate, ok := h.res.Lookup("speculativeAuthenticate").DocumentOK(); ok {
info.SpeculativeAuthenticate = speculativeAuthenticate
}
if serverConnectionID, ok := h.res.Lookup("connectionId").Int32OK(); ok {
info.ServerConnectionID = &serverConnectionID
}
// Cast to bson.Raw to lookup saslSupportedMechs to avoid converting from bsoncore.Value to bson.RawValue for the
// StringSliceFromRawValue call.
if saslSupportedMechs, lookupErr := bson.Raw(h.res).LookupErr("saslSupportedMechs"); lookupErr == nil {
info.SaslSupportedMechs, err = internal.StringSliceFromRawValue("saslSupportedMechs", saslSupportedMechs)
}
return info, err
}
// FinishHandshake implements the Handshaker interface. This is a no-op function because a non-authenticated connection
// does not do anything besides the initial Hello for a handshake.
func (h *Hello) FinishHandshake(context.Context, driver.Connection) error {
return nil
}
+293
View File
@@ -0,0 +1,293 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"fmt"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Insert performs an insert operation.
type Insert struct {
bypassDocumentValidation *bool
comment bsoncore.Value
documents []bsoncore.Document
ordered *bool
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
retry *driver.RetryMode
result InsertResult
serverAPI *driver.ServerAPIOptions
timeout *time.Duration
}
// InsertResult represents an insert result returned by the server.
type InsertResult struct {
// Number of documents successfully inserted.
N int64
}
func buildInsertResult(response bsoncore.Document) (InsertResult, error) {
elements, err := response.Elements()
if err != nil {
return InsertResult{}, err
}
ir := InsertResult{}
for _, element := range elements {
switch element.Key() {
case "n":
var ok bool
ir.N, ok = element.Value().AsInt64OK()
if !ok {
return ir, fmt.Errorf("response field 'n' is type int32 or int64, but received BSON type %s", element.Value().Type)
}
}
}
return ir, nil
}
// NewInsert constructs and returns a new Insert.
func NewInsert(documents ...bsoncore.Document) *Insert {
return &Insert{
documents: documents,
}
}
// Result returns the result of executing this operation.
func (i *Insert) Result() InsertResult { return i.result }
func (i *Insert) processResponse(info driver.ResponseInfo) error {
ir, err := buildInsertResult(info.ServerResponse)
i.result.N += ir.N
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (i *Insert) Execute(ctx context.Context) error {
if i.deployment == nil {
return errors.New("the Insert operation must have a Deployment set before Execute can be called")
}
batches := &driver.Batches{
Identifier: "documents",
Documents: i.documents,
Ordered: i.ordered,
}
return driver.Operation{
CommandFn: i.command,
ProcessResponseFn: i.processResponse,
Batches: batches,
RetryMode: i.retry,
Type: driver.Write,
Client: i.session,
Clock: i.clock,
CommandMonitor: i.monitor,
Crypt: i.crypt,
Database: i.database,
Deployment: i.deployment,
Selector: i.selector,
WriteConcern: i.writeConcern,
ServerAPI: i.serverAPI,
Timeout: i.timeout,
}.Execute(ctx, nil)
}
func (i *Insert) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "insert", i.collection)
if i.bypassDocumentValidation != nil && (desc.WireVersion != nil && desc.WireVersion.Includes(4)) {
dst = bsoncore.AppendBooleanElement(dst, "bypassDocumentValidation", *i.bypassDocumentValidation)
}
if i.comment.Type != bsontype.Type(0) {
dst = bsoncore.AppendValueElement(dst, "comment", i.comment)
}
if i.ordered != nil {
dst = bsoncore.AppendBooleanElement(dst, "ordered", *i.ordered)
}
return dst, nil
}
// BypassDocumentValidation allows the operation to opt-out of document level validation. Valid
// for server versions >= 3.2. For servers < 3.2, this setting is ignored.
func (i *Insert) BypassDocumentValidation(bypassDocumentValidation bool) *Insert {
if i == nil {
i = new(Insert)
}
i.bypassDocumentValidation = &bypassDocumentValidation
return i
}
// Comment sets a value to help trace an operation.
func (i *Insert) Comment(comment bsoncore.Value) *Insert {
if i == nil {
i = new(Insert)
}
i.comment = comment
return i
}
// Documents adds documents to this operation that will be inserted when this operation is
// executed.
func (i *Insert) Documents(documents ...bsoncore.Document) *Insert {
if i == nil {
i = new(Insert)
}
i.documents = documents
return i
}
// Ordered sets ordered. If true, when a write fails, the operation will return the error, when
// false write failures do not stop execution of the operation.
func (i *Insert) Ordered(ordered bool) *Insert {
if i == nil {
i = new(Insert)
}
i.ordered = &ordered
return i
}
// Session sets the session for this operation.
func (i *Insert) Session(session *session.Client) *Insert {
if i == nil {
i = new(Insert)
}
i.session = session
return i
}
// ClusterClock sets the cluster clock for this operation.
func (i *Insert) ClusterClock(clock *session.ClusterClock) *Insert {
if i == nil {
i = new(Insert)
}
i.clock = clock
return i
}
// Collection sets the collection that this command will run against.
func (i *Insert) Collection(collection string) *Insert {
if i == nil {
i = new(Insert)
}
i.collection = collection
return i
}
// CommandMonitor sets the monitor to use for APM events.
func (i *Insert) CommandMonitor(monitor *event.CommandMonitor) *Insert {
if i == nil {
i = new(Insert)
}
i.monitor = monitor
return i
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (i *Insert) Crypt(crypt driver.Crypt) *Insert {
if i == nil {
i = new(Insert)
}
i.crypt = crypt
return i
}
// Database sets the database to run this operation against.
func (i *Insert) Database(database string) *Insert {
if i == nil {
i = new(Insert)
}
i.database = database
return i
}
// Deployment sets the deployment to use for this operation.
func (i *Insert) Deployment(deployment driver.Deployment) *Insert {
if i == nil {
i = new(Insert)
}
i.deployment = deployment
return i
}
// ServerSelector sets the selector used to retrieve a server.
func (i *Insert) ServerSelector(selector description.ServerSelector) *Insert {
if i == nil {
i = new(Insert)
}
i.selector = selector
return i
}
// WriteConcern sets the write concern for this operation.
func (i *Insert) WriteConcern(writeConcern *writeconcern.WriteConcern) *Insert {
if i == nil {
i = new(Insert)
}
i.writeConcern = writeConcern
return i
}
// Retry enables retryable mode for this operation. Retries are handled automatically in driver.Operation.Execute based
// on how the operation is set.
func (i *Insert) Retry(retry driver.RetryMode) *Insert {
if i == nil {
i = new(Insert)
}
i.retry = &retry
return i
}
// ServerAPI sets the server API version for this operation.
func (i *Insert) ServerAPI(serverAPI *driver.ServerAPIOptions) *Insert {
if i == nil {
i = new(Insert)
}
i.serverAPI = serverAPI
return i
}
// Timeout sets the timeout for this operation.
func (i *Insert) Timeout(timeout *time.Duration) *Insert {
if i == nil {
i = new(Insert)
}
i.timeout = timeout
return i
}
@@ -0,0 +1,327 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"fmt"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/readpref"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// ListDatabases performs a listDatabases operation.
type ListDatabases struct {
filter bsoncore.Document
authorizedDatabases *bool
nameOnly *bool
session *session.Client
clock *session.ClusterClock
monitor *event.CommandMonitor
database string
deployment driver.Deployment
readPreference *readpref.ReadPref
retry *driver.RetryMode
selector description.ServerSelector
crypt driver.Crypt
serverAPI *driver.ServerAPIOptions
timeout *time.Duration
result ListDatabasesResult
}
// ListDatabasesResult represents a listDatabases result returned by the server.
type ListDatabasesResult struct {
// An array of documents, one document for each database
Databases []databaseRecord
// The sum of the size of all the database files on disk in bytes.
TotalSize int64
}
type databaseRecord struct {
Name string
SizeOnDisk int64 `bson:"sizeOnDisk"`
Empty bool
}
func buildListDatabasesResult(response bsoncore.Document) (ListDatabasesResult, error) {
elements, err := response.Elements()
if err != nil {
return ListDatabasesResult{}, err
}
ir := ListDatabasesResult{}
for _, element := range elements {
switch element.Key() {
case "totalSize":
var ok bool
ir.TotalSize, ok = element.Value().AsInt64OK()
if !ok {
return ir, fmt.Errorf("response field 'totalSize' is type int64, but received BSON type %s: %s", element.Value().Type, element.Value())
}
case "databases":
arr, ok := element.Value().ArrayOK()
if !ok {
return ir, fmt.Errorf("response field 'databases' is type array, but received BSON type %s", element.Value().Type)
}
var tmp bsoncore.Document
err := bson.Unmarshal(arr, &tmp)
if err != nil {
return ir, err
}
records, err := tmp.Elements()
if err != nil {
return ir, err
}
ir.Databases = make([]databaseRecord, len(records))
for i, val := range records {
valueDoc, ok := val.Value().DocumentOK()
if !ok {
return ir, fmt.Errorf("'databases' element is type document, but received BSON type %s", val.Value().Type)
}
elems, err := valueDoc.Elements()
if err != nil {
return ir, err
}
for _, elem := range elems {
switch elem.Key() {
case "name":
ir.Databases[i].Name, ok = elem.Value().StringValueOK()
if !ok {
return ir, fmt.Errorf("response field 'name' is type string, but received BSON type %s", elem.Value().Type)
}
case "sizeOnDisk":
ir.Databases[i].SizeOnDisk, ok = elem.Value().AsInt64OK()
if !ok {
return ir, fmt.Errorf("response field 'sizeOnDisk' is type int64, but received BSON type %s", elem.Value().Type)
}
case "empty":
ir.Databases[i].Empty, ok = elem.Value().BooleanOK()
if !ok {
return ir, fmt.Errorf("response field 'empty' is type bool, but received BSON type %s", elem.Value().Type)
}
}
}
}
}
}
return ir, nil
}
// NewListDatabases constructs and returns a new ListDatabases.
func NewListDatabases(filter bsoncore.Document) *ListDatabases {
return &ListDatabases{
filter: filter,
}
}
// Result returns the result of executing this operation.
func (ld *ListDatabases) Result() ListDatabasesResult { return ld.result }
func (ld *ListDatabases) processResponse(info driver.ResponseInfo) error {
var err error
ld.result, err = buildListDatabasesResult(info.ServerResponse)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (ld *ListDatabases) Execute(ctx context.Context) error {
if ld.deployment == nil {
return errors.New("the ListDatabases operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: ld.command,
ProcessResponseFn: ld.processResponse,
Client: ld.session,
Clock: ld.clock,
CommandMonitor: ld.monitor,
Database: ld.database,
Deployment: ld.deployment,
ReadPreference: ld.readPreference,
RetryMode: ld.retry,
Type: driver.Read,
Selector: ld.selector,
Crypt: ld.crypt,
ServerAPI: ld.serverAPI,
Timeout: ld.timeout,
}.Execute(ctx, nil)
}
func (ld *ListDatabases) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendInt32Element(dst, "listDatabases", 1)
if ld.filter != nil {
dst = bsoncore.AppendDocumentElement(dst, "filter", ld.filter)
}
if ld.nameOnly != nil {
dst = bsoncore.AppendBooleanElement(dst, "nameOnly", *ld.nameOnly)
}
if ld.authorizedDatabases != nil {
dst = bsoncore.AppendBooleanElement(dst, "authorizedDatabases", *ld.authorizedDatabases)
}
return dst, nil
}
// Filter determines what results are returned from listDatabases.
func (ld *ListDatabases) Filter(filter bsoncore.Document) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.filter = filter
return ld
}
// NameOnly specifies whether to only return database names.
func (ld *ListDatabases) NameOnly(nameOnly bool) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.nameOnly = &nameOnly
return ld
}
// AuthorizedDatabases specifies whether to only return databases which the user is authorized to use."
func (ld *ListDatabases) AuthorizedDatabases(authorizedDatabases bool) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.authorizedDatabases = &authorizedDatabases
return ld
}
// Session sets the session for this operation.
func (ld *ListDatabases) Session(session *session.Client) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.session = session
return ld
}
// ClusterClock sets the cluster clock for this operation.
func (ld *ListDatabases) ClusterClock(clock *session.ClusterClock) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.clock = clock
return ld
}
// CommandMonitor sets the monitor to use for APM events.
func (ld *ListDatabases) CommandMonitor(monitor *event.CommandMonitor) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.monitor = monitor
return ld
}
// Database sets the database to run this operation against.
func (ld *ListDatabases) Database(database string) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.database = database
return ld
}
// Deployment sets the deployment to use for this operation.
func (ld *ListDatabases) Deployment(deployment driver.Deployment) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.deployment = deployment
return ld
}
// ReadPreference set the read preference used with this operation.
func (ld *ListDatabases) ReadPreference(readPreference *readpref.ReadPref) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.readPreference = readPreference
return ld
}
// ServerSelector sets the selector used to retrieve a server.
func (ld *ListDatabases) ServerSelector(selector description.ServerSelector) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.selector = selector
return ld
}
// Retry enables retryable mode for this operation. Retries are handled automatically in driver.Operation.Execute based
// on how the operation is set.
func (ld *ListDatabases) Retry(retry driver.RetryMode) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.retry = &retry
return ld
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (ld *ListDatabases) Crypt(crypt driver.Crypt) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.crypt = crypt
return ld
}
// ServerAPI sets the server API version for this operation.
func (ld *ListDatabases) ServerAPI(serverAPI *driver.ServerAPIOptions) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.serverAPI = serverAPI
return ld
}
// Timeout sets the timeout for this operation.
func (ld *ListDatabases) Timeout(timeout *time.Duration) *ListDatabases {
if ld == nil {
ld = new(ListDatabases)
}
ld.timeout = timeout
return ld
}
@@ -0,0 +1,266 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"time"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/readpref"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// ListCollections performs a listCollections operation.
type ListCollections struct {
filter bsoncore.Document
nameOnly *bool
authorizedCollections *bool
session *session.Client
clock *session.ClusterClock
monitor *event.CommandMonitor
crypt driver.Crypt
database string
deployment driver.Deployment
readPreference *readpref.ReadPref
selector description.ServerSelector
retry *driver.RetryMode
result driver.CursorResponse
batchSize *int32
serverAPI *driver.ServerAPIOptions
timeout *time.Duration
}
// NewListCollections constructs and returns a new ListCollections.
func NewListCollections(filter bsoncore.Document) *ListCollections {
return &ListCollections{
filter: filter,
}
}
// Result returns the result of executing this operation.
func (lc *ListCollections) Result(opts driver.CursorOptions) (*driver.ListCollectionsBatchCursor, error) {
opts.ServerAPI = lc.serverAPI
bc, err := driver.NewBatchCursor(lc.result, lc.session, lc.clock, opts)
if err != nil {
return nil, err
}
desc := lc.result.Desc
if desc.WireVersion == nil || desc.WireVersion.Max < 3 {
return driver.NewLegacyListCollectionsBatchCursor(bc)
}
return driver.NewListCollectionsBatchCursor(bc)
}
func (lc *ListCollections) processResponse(info driver.ResponseInfo) error {
var err error
lc.result, err = driver.NewCursorResponse(info)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (lc *ListCollections) Execute(ctx context.Context) error {
if lc.deployment == nil {
return errors.New("the ListCollections operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: lc.command,
ProcessResponseFn: lc.processResponse,
RetryMode: lc.retry,
Type: driver.Read,
Client: lc.session,
Clock: lc.clock,
CommandMonitor: lc.monitor,
Crypt: lc.crypt,
Database: lc.database,
Deployment: lc.deployment,
ReadPreference: lc.readPreference,
Selector: lc.selector,
Legacy: driver.LegacyListCollections,
ServerAPI: lc.serverAPI,
Timeout: lc.timeout,
}.Execute(ctx, nil)
}
func (lc *ListCollections) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendInt32Element(dst, "listCollections", 1)
if lc.filter != nil {
dst = bsoncore.AppendDocumentElement(dst, "filter", lc.filter)
}
if lc.nameOnly != nil {
dst = bsoncore.AppendBooleanElement(dst, "nameOnly", *lc.nameOnly)
}
if lc.authorizedCollections != nil {
dst = bsoncore.AppendBooleanElement(dst, "authorizedCollections", *lc.authorizedCollections)
}
cursorDoc := bsoncore.NewDocumentBuilder()
if lc.batchSize != nil {
cursorDoc.AppendInt32("batchSize", *lc.batchSize)
}
dst = bsoncore.AppendDocumentElement(dst, "cursor", cursorDoc.Build())
return dst, nil
}
// Filter determines what results are returned from listCollections.
func (lc *ListCollections) Filter(filter bsoncore.Document) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.filter = filter
return lc
}
// NameOnly specifies whether to only return collection names.
func (lc *ListCollections) NameOnly(nameOnly bool) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.nameOnly = &nameOnly
return lc
}
// AuthorizedCollections specifies whether to only return collections the user
// is authorized to use.
func (lc *ListCollections) AuthorizedCollections(authorizedCollections bool) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.authorizedCollections = &authorizedCollections
return lc
}
// Session sets the session for this operation.
func (lc *ListCollections) Session(session *session.Client) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.session = session
return lc
}
// ClusterClock sets the cluster clock for this operation.
func (lc *ListCollections) ClusterClock(clock *session.ClusterClock) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.clock = clock
return lc
}
// CommandMonitor sets the monitor to use for APM events.
func (lc *ListCollections) CommandMonitor(monitor *event.CommandMonitor) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.monitor = monitor
return lc
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (lc *ListCollections) Crypt(crypt driver.Crypt) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.crypt = crypt
return lc
}
// Database sets the database to run this operation against.
func (lc *ListCollections) Database(database string) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.database = database
return lc
}
// Deployment sets the deployment to use for this operation.
func (lc *ListCollections) Deployment(deployment driver.Deployment) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.deployment = deployment
return lc
}
// ReadPreference set the read preference used with this operation.
func (lc *ListCollections) ReadPreference(readPreference *readpref.ReadPref) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.readPreference = readPreference
return lc
}
// ServerSelector sets the selector used to retrieve a server.
func (lc *ListCollections) ServerSelector(selector description.ServerSelector) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.selector = selector
return lc
}
// Retry enables retryable mode for this operation. Retries are handled automatically in driver.Operation.Execute based
// on how the operation is set.
func (lc *ListCollections) Retry(retry driver.RetryMode) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.retry = &retry
return lc
}
// BatchSize specifies the number of documents to return in every batch.
func (lc *ListCollections) BatchSize(batchSize int32) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.batchSize = &batchSize
return lc
}
// ServerAPI sets the server API version for this operation.
func (lc *ListCollections) ServerAPI(serverAPI *driver.ServerAPIOptions) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.serverAPI = serverAPI
return lc
}
// Timeout sets the timeout for this operation.
func (lc *ListCollections) Timeout(timeout *time.Duration) *ListCollections {
if lc == nil {
lc = new(ListCollections)
}
lc.timeout = timeout
return lc
}
@@ -0,0 +1,238 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"time"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// ListIndexes performs a listIndexes operation.
type ListIndexes struct {
batchSize *int32
maxTimeMS *int64
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
database string
deployment driver.Deployment
selector description.ServerSelector
retry *driver.RetryMode
crypt driver.Crypt
serverAPI *driver.ServerAPIOptions
timeout *time.Duration
result driver.CursorResponse
}
// NewListIndexes constructs and returns a new ListIndexes.
func NewListIndexes() *ListIndexes {
return &ListIndexes{}
}
// Result returns the result of executing this operation.
func (li *ListIndexes) Result(opts driver.CursorOptions) (*driver.BatchCursor, error) {
clientSession := li.session
clock := li.clock
opts.ServerAPI = li.serverAPI
return driver.NewBatchCursor(li.result, clientSession, clock, opts)
}
func (li *ListIndexes) processResponse(info driver.ResponseInfo) error {
var err error
li.result, err = driver.NewCursorResponse(info)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (li *ListIndexes) Execute(ctx context.Context) error {
if li.deployment == nil {
return errors.New("the ListIndexes operation must have a Deployment set before Execute can be called")
}
return driver.Operation{
CommandFn: li.command,
ProcessResponseFn: li.processResponse,
Client: li.session,
Clock: li.clock,
CommandMonitor: li.monitor,
Database: li.database,
Deployment: li.deployment,
Selector: li.selector,
Crypt: li.crypt,
Legacy: driver.LegacyListIndexes,
RetryMode: li.retry,
Type: driver.Read,
ServerAPI: li.serverAPI,
Timeout: li.timeout,
}.Execute(ctx, nil)
}
func (li *ListIndexes) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "listIndexes", li.collection)
cursorIdx, cursorDoc := bsoncore.AppendDocumentStart(nil)
if li.batchSize != nil {
cursorDoc = bsoncore.AppendInt32Element(cursorDoc, "batchSize", *li.batchSize)
}
// Only append specified maxTimeMS if timeout is not also specified.
if li.maxTimeMS != nil && li.timeout == nil {
dst = bsoncore.AppendInt64Element(dst, "maxTimeMS", *li.maxTimeMS)
}
cursorDoc, _ = bsoncore.AppendDocumentEnd(cursorDoc, cursorIdx)
dst = bsoncore.AppendDocumentElement(dst, "cursor", cursorDoc)
return dst, nil
}
// BatchSize specifies the number of documents to return in every batch.
func (li *ListIndexes) BatchSize(batchSize int32) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.batchSize = &batchSize
return li
}
// MaxTimeMS specifies the maximum amount of time to allow the query to run.
func (li *ListIndexes) MaxTimeMS(maxTimeMS int64) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.maxTimeMS = &maxTimeMS
return li
}
// Session sets the session for this operation.
func (li *ListIndexes) Session(session *session.Client) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.session = session
return li
}
// ClusterClock sets the cluster clock for this operation.
func (li *ListIndexes) ClusterClock(clock *session.ClusterClock) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.clock = clock
return li
}
// Collection sets the collection that this command will run against.
func (li *ListIndexes) Collection(collection string) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.collection = collection
return li
}
// CommandMonitor sets the monitor to use for APM events.
func (li *ListIndexes) CommandMonitor(monitor *event.CommandMonitor) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.monitor = monitor
return li
}
// Database sets the database to run this operation against.
func (li *ListIndexes) Database(database string) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.database = database
return li
}
// Deployment sets the deployment to use for this operation.
func (li *ListIndexes) Deployment(deployment driver.Deployment) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.deployment = deployment
return li
}
// ServerSelector sets the selector used to retrieve a server.
func (li *ListIndexes) ServerSelector(selector description.ServerSelector) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.selector = selector
return li
}
// Retry enables retryable mode for this operation. Retries are handled automatically in driver.Operation.Execute based
// on how the operation is set.
func (li *ListIndexes) Retry(retry driver.RetryMode) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.retry = &retry
return li
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (li *ListIndexes) Crypt(crypt driver.Crypt) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.crypt = crypt
return li
}
// ServerAPI sets the server API version for this operation.
func (li *ListIndexes) ServerAPI(serverAPI *driver.ServerAPIOptions) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.serverAPI = serverAPI
return li
}
// Timeout sets the timeout for this operation.
func (li *ListIndexes) Timeout(timeout *time.Duration) *ListIndexes {
if li == nil {
li = new(ListIndexes)
}
li.timeout = timeout
return li
}
+401
View File
@@ -0,0 +1,401 @@
// Copyright (C) MongoDB, Inc. 2019-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package operation
import (
"context"
"errors"
"fmt"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
// Update performs an update operation.
type Update struct {
bypassDocumentValidation *bool
comment bsoncore.Value
ordered *bool
updates []bsoncore.Document
session *session.Client
clock *session.ClusterClock
collection string
monitor *event.CommandMonitor
database string
deployment driver.Deployment
hint *bool
arrayFilters *bool
selector description.ServerSelector
writeConcern *writeconcern.WriteConcern
retry *driver.RetryMode
result UpdateResult
crypt driver.Crypt
serverAPI *driver.ServerAPIOptions
let bsoncore.Document
timeout *time.Duration
}
// Upsert contains the information for an upsert in an Update operation.
type Upsert struct {
Index int64
ID interface{} `bson:"_id"`
}
// UpdateResult contains information for the result of an Update operation.
type UpdateResult struct {
// Number of documents matched.
N int64
// Number of documents modified.
NModified int64
// Information about upserted documents.
Upserted []Upsert
}
func buildUpdateResult(response bsoncore.Document) (UpdateResult, error) {
elements, err := response.Elements()
if err != nil {
return UpdateResult{}, err
}
ur := UpdateResult{}
for _, element := range elements {
switch element.Key() {
case "nModified":
var ok bool
ur.NModified, ok = element.Value().AsInt64OK()
if !ok {
return ur, fmt.Errorf("response field 'nModified' is type int32 or int64, but received BSON type %s", element.Value().Type)
}
case "n":
var ok bool
ur.N, ok = element.Value().AsInt64OK()
if !ok {
return ur, fmt.Errorf("response field 'n' is type int32 or int64, but received BSON type %s", element.Value().Type)
}
case "upserted":
arr, ok := element.Value().ArrayOK()
if !ok {
return ur, fmt.Errorf("response field 'upserted' is type array, but received BSON type %s", element.Value().Type)
}
var values []bsoncore.Value
values, err = arr.Values()
if err != nil {
break
}
for _, val := range values {
valDoc, ok := val.DocumentOK()
if !ok {
return ur, fmt.Errorf("upserted value is type document, but received BSON type %s", val.Type)
}
var upsert Upsert
if err = bson.Unmarshal(valDoc, &upsert); err != nil {
return ur, err
}
ur.Upserted = append(ur.Upserted, upsert)
}
}
}
return ur, nil
}
// NewUpdate constructs and returns a new Update.
func NewUpdate(updates ...bsoncore.Document) *Update {
return &Update{
updates: updates,
}
}
// Result returns the result of executing this operation.
func (u *Update) Result() UpdateResult { return u.result }
func (u *Update) processResponse(info driver.ResponseInfo) error {
ur, err := buildUpdateResult(info.ServerResponse)
u.result.N += ur.N
u.result.NModified += ur.NModified
if info.CurrentIndex > 0 {
for ind := range ur.Upserted {
ur.Upserted[ind].Index += int64(info.CurrentIndex)
}
}
u.result.Upserted = append(u.result.Upserted, ur.Upserted...)
return err
}
// Execute runs this operations and returns an error if the operation did not execute successfully.
func (u *Update) Execute(ctx context.Context) error {
if u.deployment == nil {
return errors.New("the Update operation must have a Deployment set before Execute can be called")
}
batches := &driver.Batches{
Identifier: "updates",
Documents: u.updates,
Ordered: u.ordered,
}
return driver.Operation{
CommandFn: u.command,
ProcessResponseFn: u.processResponse,
Batches: batches,
RetryMode: u.retry,
Type: driver.Write,
Client: u.session,
Clock: u.clock,
CommandMonitor: u.monitor,
Database: u.database,
Deployment: u.deployment,
Selector: u.selector,
WriteConcern: u.writeConcern,
Crypt: u.crypt,
ServerAPI: u.serverAPI,
Timeout: u.timeout,
}.Execute(ctx, nil)
}
func (u *Update) command(dst []byte, desc description.SelectedServer) ([]byte, error) {
dst = bsoncore.AppendStringElement(dst, "update", u.collection)
if u.bypassDocumentValidation != nil &&
(desc.WireVersion != nil && desc.WireVersion.Includes(4)) {
dst = bsoncore.AppendBooleanElement(dst, "bypassDocumentValidation", *u.bypassDocumentValidation)
}
if u.comment.Type != bsontype.Type(0) {
dst = bsoncore.AppendValueElement(dst, "comment", u.comment)
}
if u.ordered != nil {
dst = bsoncore.AppendBooleanElement(dst, "ordered", *u.ordered)
}
if u.hint != nil && *u.hint {
if desc.WireVersion == nil || !desc.WireVersion.Includes(5) {
return nil, errors.New("the 'hint' command parameter requires a minimum server wire version of 5")
}
if !u.writeConcern.Acknowledged() {
return nil, errUnacknowledgedHint
}
}
if u.arrayFilters != nil && *u.arrayFilters {
if desc.WireVersion == nil || !desc.WireVersion.Includes(6) {
return nil, errors.New("the 'arrayFilters' command parameter requires a minimum server wire version of 6")
}
}
if u.let != nil {
dst = bsoncore.AppendDocumentElement(dst, "let", u.let)
}
return dst, nil
}
// BypassDocumentValidation allows the operation to opt-out of document level validation. Valid
// for server versions >= 3.2. For servers < 3.2, this setting is ignored.
func (u *Update) BypassDocumentValidation(bypassDocumentValidation bool) *Update {
if u == nil {
u = new(Update)
}
u.bypassDocumentValidation = &bypassDocumentValidation
return u
}
// Hint is a flag to indicate that the update document contains a hint. Hint is only supported by
// servers >= 4.2. Older servers >= 3.4 will report an error for using the hint option. For servers <
// 3.4, the driver will return an error if the hint option is used.
func (u *Update) Hint(hint bool) *Update {
if u == nil {
u = new(Update)
}
u.hint = &hint
return u
}
// ArrayFilters is a flag to indicate that the update document contains an arrayFilters field. This option is only
// supported on server versions 3.6 and higher. For servers < 3.6, the driver will return an error.
func (u *Update) ArrayFilters(arrayFilters bool) *Update {
if u == nil {
u = new(Update)
}
u.arrayFilters = &arrayFilters
return u
}
// Ordered sets ordered. If true, when a write fails, the operation will return the error, when
// false write failures do not stop execution of the operation.
func (u *Update) Ordered(ordered bool) *Update {
if u == nil {
u = new(Update)
}
u.ordered = &ordered
return u
}
// Updates specifies an array of update statements to perform when this operation is executed.
// Each update document must have the following structure:
// {q: <query>, u: <update>, multi: <boolean>, collation: Optional<Document>, arrayFitlers: Optional<Array>, hint: Optional<string/Document>}.
func (u *Update) Updates(updates ...bsoncore.Document) *Update {
if u == nil {
u = new(Update)
}
u.updates = updates
return u
}
// Session sets the session for this operation.
func (u *Update) Session(session *session.Client) *Update {
if u == nil {
u = new(Update)
}
u.session = session
return u
}
// ClusterClock sets the cluster clock for this operation.
func (u *Update) ClusterClock(clock *session.ClusterClock) *Update {
if u == nil {
u = new(Update)
}
u.clock = clock
return u
}
// Collection sets the collection that this command will run against.
func (u *Update) Collection(collection string) *Update {
if u == nil {
u = new(Update)
}
u.collection = collection
return u
}
// CommandMonitor sets the monitor to use for APM events.
func (u *Update) CommandMonitor(monitor *event.CommandMonitor) *Update {
if u == nil {
u = new(Update)
}
u.monitor = monitor
return u
}
// Comment sets a value to help trace an operation.
func (u *Update) Comment(comment bsoncore.Value) *Update {
if u == nil {
u = new(Update)
}
u.comment = comment
return u
}
// Database sets the database to run this operation against.
func (u *Update) Database(database string) *Update {
if u == nil {
u = new(Update)
}
u.database = database
return u
}
// Deployment sets the deployment to use for this operation.
func (u *Update) Deployment(deployment driver.Deployment) *Update {
if u == nil {
u = new(Update)
}
u.deployment = deployment
return u
}
// ServerSelector sets the selector used to retrieve a server.
func (u *Update) ServerSelector(selector description.ServerSelector) *Update {
if u == nil {
u = new(Update)
}
u.selector = selector
return u
}
// WriteConcern sets the write concern for this operation.
func (u *Update) WriteConcern(writeConcern *writeconcern.WriteConcern) *Update {
if u == nil {
u = new(Update)
}
u.writeConcern = writeConcern
return u
}
// Retry enables retryable writes for this operation. Retries are not handled automatically,
// instead a boolean is returned from Execute and SelectAndExecute that indicates if the
// operation can be retried. Retrying is handled by calling RetryExecute.
func (u *Update) Retry(retry driver.RetryMode) *Update {
if u == nil {
u = new(Update)
}
u.retry = &retry
return u
}
// Crypt sets the Crypt object to use for automatic encryption and decryption.
func (u *Update) Crypt(crypt driver.Crypt) *Update {
if u == nil {
u = new(Update)
}
u.crypt = crypt
return u
}
// ServerAPI sets the server API version for this operation.
func (u *Update) ServerAPI(serverAPI *driver.ServerAPIOptions) *Update {
if u == nil {
u = new(Update)
}
u.serverAPI = serverAPI
return u
}
// Let specifies the let document to use. This option is only valid for server versions 5.0 and above.
func (u *Update) Let(let bsoncore.Document) *Update {
if u == nil {
u = new(Update)
}
u.let = let
return u
}
// Timeout sets the timeout for this operation.
func (u *Update) Timeout(timeout *time.Duration) *Update {
if u == nil {
u = new(Update)
}
u.timeout = timeout
return u
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver
import (
"context"
"errors"
)
// ExecuteExhaust reads a response from the provided StreamerConnection. This will error if the connection's
// CurrentlyStreaming function returns false.
func (op Operation) ExecuteExhaust(ctx context.Context, conn StreamerConnection, scratch []byte) error {
if !conn.CurrentlyStreaming() {
return errors.New("exhaust read must be done with a connection that is currently streaming")
}
scratch = scratch[:0]
res, err := op.readWireMessage(ctx, conn, scratch)
if err != nil {
return err
}
if op.ProcessResponseFn != nil {
// Server, ConnectionDescription, and CurrentIndex are unused in this mode.
info := ResponseInfo{
ServerResponse: res,
Connection: conn,
}
if err = op.ProcessResponseFn(info); err != nil {
return err
}
}
return nil
}
+719
View File
@@ -0,0 +1,719 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver
import (
"context"
"errors"
"strconv"
"time"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
"go.mongodb.org/mongo-driver/x/mongo/driver/wiremessage"
)
var (
firstBatchIdentifier = "firstBatch"
nextBatchIdentifier = "nextBatch"
listCollectionsNamespace = "system.namespaces"
listIndexesNamespace = "system.indexes"
// ErrFilterType is returned when the filter for a legacy list collections operation is of the wrong type.
ErrFilterType = errors.New("filter for list collections operation must be a string")
)
func (op Operation) getFullCollectionName(coll string) string {
return op.Database + "." + coll
}
func (op Operation) legacyFind(ctx context.Context, dst []byte, srvr Server, conn Connection, desc description.SelectedServer) error {
wm, startedInfo, collName, err := op.createLegacyFindWireMessage(dst, desc)
if err != nil {
return err
}
startedInfo.connID = conn.ID()
op.publishStartedEvent(ctx, startedInfo)
finishedInfo := finishedInformation{
cmdName: startedInfo.cmdName,
requestID: startedInfo.requestID,
startTime: time.Now(),
connID: startedInfo.connID,
}
finishedInfo.response, finishedInfo.cmdErr = op.roundTripLegacyCursor(ctx, wm, srvr, conn, collName, firstBatchIdentifier)
op.publishFinishedEvent(ctx, finishedInfo)
if finishedInfo.cmdErr != nil {
return finishedInfo.cmdErr
}
if op.ProcessResponseFn != nil {
// CurrentIndex is always 0 in this mode.
info := ResponseInfo{
ServerResponse: finishedInfo.response,
Server: srvr,
Connection: conn,
ConnectionDescription: desc.Server,
}
return op.ProcessResponseFn(info)
}
return nil
}
// returns wire message, collection name, error
func (op Operation) createLegacyFindWireMessage(dst []byte, desc description.SelectedServer) ([]byte, startedInformation, string, error) {
info := startedInformation{
requestID: wiremessage.NextRequestID(),
cmdName: "find",
}
// call CommandFn on an empty slice rather than dst because the options will need to be converted to legacy
var cmdDoc bsoncore.Document
var cmdIndex int32
var err error
cmdIndex, cmdDoc = bsoncore.AppendDocumentStart(cmdDoc)
cmdDoc, err = op.CommandFn(cmdDoc, desc)
if err != nil {
return dst, info, "", err
}
cmdDoc, _ = bsoncore.AppendDocumentEnd(cmdDoc, cmdIndex)
// for monitoring legacy events, the upconverted document should be captured rather than the legacy one
info.cmd = cmdDoc
cmdElems, err := cmdDoc.Elements()
if err != nil {
return dst, info, "", err
}
// take each option from the non-legacy command and convert it
// build options as a byte slice of elements rather than a bsoncore.Document because they will be appended
// to another document with $query
var optsElems []byte
flags := op.secondaryOK(desc)
var numToSkip, numToReturn, batchSize, limit int32 // numToReturn calculated from batchSize and limit
var filter, returnFieldsSelector bsoncore.Document
var collName string
var singleBatch bool
for _, elem := range cmdElems {
switch elem.Key() {
case "find":
collName = elem.Value().StringValue()
case "filter":
filter = elem.Value().Data
case "sort":
optsElems = bsoncore.AppendValueElement(optsElems, "$orderby", elem.Value())
case "hint":
optsElems = bsoncore.AppendValueElement(optsElems, "$hint", elem.Value())
case "comment":
optsElems = bsoncore.AppendValueElement(optsElems, "$comment", elem.Value())
case "max":
optsElems = bsoncore.AppendValueElement(optsElems, "$max", elem.Value())
case "min":
optsElems = bsoncore.AppendValueElement(optsElems, "$min", elem.Value())
case "returnKey":
optsElems = bsoncore.AppendValueElement(optsElems, "$returnKey", elem.Value())
case "showRecordId":
optsElems = bsoncore.AppendValueElement(optsElems, "$showDiskLoc", elem.Value())
case "maxTimeMS":
optsElems = bsoncore.AppendValueElement(optsElems, "$maxTimeMS", elem.Value())
case "snapshot":
optsElems = bsoncore.AppendValueElement(optsElems, "$snapshot", elem.Value())
case "projection":
returnFieldsSelector = elem.Value().Data
case "skip":
// CRUD spec declares skip as int64 but numToSkip is int32 in OP_QUERY
numToSkip = int32(elem.Value().Int64())
case "batchSize":
batchSize = elem.Value().Int32()
// Not possible to use batchSize = 1 because cursor will be closed on first batch
if batchSize == 1 {
batchSize = 2
}
case "limit":
// CRUD spec declares limit as int64 but numToReturn is int32 in OP_QUERY
limit = int32(elem.Value().Int64())
case "singleBatch":
singleBatch = elem.Value().Boolean()
case "tailable":
flags |= wiremessage.TailableCursor
case "awaitData":
flags |= wiremessage.AwaitData
case "oplogReplay":
flags |= wiremessage.OplogReplay
case "noCursorTimeout":
flags |= wiremessage.NoCursorTimeout
case "allowPartialResults":
flags |= wiremessage.Partial
}
}
// for non-legacy servers, a negative limit is implemented as a positive limit + singleBatch = true
if singleBatch {
limit = limit * -1
}
numToReturn = op.calculateNumberToReturn(limit, batchSize)
// add read preference if needed
rp, err := op.createReadPref(desc, true)
if err != nil {
return dst, info, "", err
}
if len(rp) > 0 {
optsElems = bsoncore.AppendDocumentElement(optsElems, "$readPreference", rp)
}
if len(filter) == 0 {
var fidx int32
fidx, filter = bsoncore.AppendDocumentStart(filter)
filter, _ = bsoncore.AppendDocumentEnd(filter, fidx)
}
var wmIdx int32
wmIdx, dst = wiremessage.AppendHeaderStart(dst, info.requestID, 0, wiremessage.OpQuery)
dst = wiremessage.AppendQueryFlags(dst, flags)
dst = wiremessage.AppendQueryFullCollectionName(dst, op.getFullCollectionName(collName))
dst = wiremessage.AppendQueryNumberToSkip(dst, numToSkip)
dst = wiremessage.AppendQueryNumberToReturn(dst, numToReturn)
dst = op.appendLegacyQueryDocument(dst, filter, optsElems)
if len(returnFieldsSelector) != 0 {
// returnFieldsSelector is optional
dst = append(dst, returnFieldsSelector...)
}
return bsoncore.UpdateLength(dst, wmIdx, int32(len(dst[wmIdx:]))), info, collName, nil
}
func (op Operation) calculateNumberToReturn(limit, batchSize int32) int32 {
var numToReturn int32
if limit < 0 {
numToReturn = limit
} else if limit == 0 {
numToReturn = batchSize
} else if batchSize == 0 {
numToReturn = limit
} else if limit < batchSize {
numToReturn = limit
} else {
numToReturn = batchSize
}
return numToReturn
}
func (op Operation) legacyGetMore(ctx context.Context, dst []byte, srvr Server, conn Connection, desc description.SelectedServer) error {
wm, startedInfo, collName, err := op.createLegacyGetMoreWiremessage(dst, desc)
if err != nil {
return err
}
startedInfo.connID = conn.ID()
op.publishStartedEvent(ctx, startedInfo)
finishedInfo := finishedInformation{
cmdName: startedInfo.cmdName,
requestID: startedInfo.requestID,
startTime: time.Now(),
connID: startedInfo.connID,
}
finishedInfo.response, finishedInfo.cmdErr = op.roundTripLegacyCursor(ctx, wm, srvr, conn, collName, nextBatchIdentifier)
op.publishFinishedEvent(ctx, finishedInfo)
if finishedInfo.cmdErr != nil {
return finishedInfo.cmdErr
}
if op.ProcessResponseFn != nil {
// CurrentIndex is always 0 in this mode.
info := ResponseInfo{
ServerResponse: finishedInfo.response,
Server: srvr,
Connection: conn,
ConnectionDescription: desc.Server,
}
return op.ProcessResponseFn(info)
}
return nil
}
func (op Operation) createLegacyGetMoreWiremessage(dst []byte, desc description.SelectedServer) ([]byte, startedInformation, string, error) {
info := startedInformation{
requestID: wiremessage.NextRequestID(),
cmdName: "getMore",
}
var cmdDoc bsoncore.Document
var cmdIdx int32
var err error
cmdIdx, cmdDoc = bsoncore.AppendDocumentStart(cmdDoc)
cmdDoc, err = op.CommandFn(cmdDoc, desc)
if err != nil {
return dst, info, "", err
}
cmdDoc, _ = bsoncore.AppendDocumentEnd(cmdDoc, cmdIdx)
info.cmd = cmdDoc
cmdElems, err := cmdDoc.Elements()
if err != nil {
return dst, info, "", err
}
var cursorID int64
var numToReturn int32
var collName string
for _, elem := range cmdElems {
switch elem.Key() {
case "getMore":
cursorID = elem.Value().Int64()
case "collection":
collName = elem.Value().StringValue()
case "batchSize":
numToReturn = elem.Value().Int32()
}
}
var wmIdx int32
wmIdx, dst = wiremessage.AppendHeaderStart(dst, info.requestID, 0, wiremessage.OpGetMore)
dst = wiremessage.AppendGetMoreZero(dst)
dst = wiremessage.AppendGetMoreFullCollectionName(dst, op.getFullCollectionName(collName))
dst = wiremessage.AppendGetMoreNumberToReturn(dst, numToReturn)
dst = wiremessage.AppendGetMoreCursorID(dst, cursorID)
return bsoncore.UpdateLength(dst, wmIdx, int32(len(dst[wmIdx:]))), info, collName, nil
}
func (op Operation) legacyKillCursors(ctx context.Context, dst []byte, srvr Server, conn Connection, desc description.SelectedServer) error {
wm, startedInfo, _, err := op.createLegacyKillCursorsWiremessage(dst, desc)
if err != nil {
return err
}
startedInfo.connID = conn.ID()
op.publishStartedEvent(ctx, startedInfo)
// skip startTime because OP_KILL_CURSORS does not return a response
finishedInfo := finishedInformation{
cmdName: "killCursors",
requestID: startedInfo.requestID,
connID: startedInfo.connID,
}
err = conn.WriteWireMessage(ctx, wm)
if err != nil {
err = Error{Message: err.Error(), Labels: []string{TransientTransactionError, NetworkError}}
if ep, ok := srvr.(ErrorProcessor); ok {
_ = ep.ProcessError(err, conn)
}
finishedInfo.cmdErr = err
op.publishFinishedEvent(ctx, finishedInfo)
return err
}
ridx, response := bsoncore.AppendDocumentStart(nil)
response = bsoncore.AppendInt32Element(response, "ok", 1)
response = bsoncore.AppendArrayElement(response, "cursorsUnknown", startedInfo.cmd.Lookup("cursors").Array())
response, _ = bsoncore.AppendDocumentEnd(response, ridx)
finishedInfo.response = response
op.publishFinishedEvent(ctx, finishedInfo)
return nil
}
func (op Operation) createLegacyKillCursorsWiremessage(dst []byte, desc description.SelectedServer) ([]byte, startedInformation, string, error) {
info := startedInformation{
cmdName: "killCursors",
requestID: wiremessage.NextRequestID(),
}
var cmdDoc bsoncore.Document
var cmdIdx int32
var err error
cmdIdx, cmdDoc = bsoncore.AppendDocumentStart(cmdDoc)
cmdDoc, err = op.CommandFn(cmdDoc, desc)
if err != nil {
return nil, info, "", err
}
cmdDoc, _ = bsoncore.AppendDocumentEnd(cmdDoc, cmdIdx)
info.cmd = cmdDoc
cmdElems, err := cmdDoc.Elements()
if err != nil {
return nil, info, "", err
}
var collName string
var cursors bsoncore.Array
for _, elem := range cmdElems {
switch elem.Key() {
case "killCursors":
collName = elem.Value().StringValue()
case "cursors":
cursors = elem.Value().Array()
}
}
var cursorIDs []int64
if cursors != nil {
cursorValues, err := cursors.Values()
if err != nil {
return nil, info, "", err
}
for _, cursorVal := range cursorValues {
cursorIDs = append(cursorIDs, cursorVal.Int64())
}
}
var wmIdx int32
wmIdx, dst = wiremessage.AppendHeaderStart(dst, info.requestID, 0, wiremessage.OpKillCursors)
dst = wiremessage.AppendKillCursorsZero(dst)
dst = wiremessage.AppendKillCursorsNumberIDs(dst, int32(len(cursorIDs)))
dst = wiremessage.AppendKillCursorsCursorIDs(dst, cursorIDs)
return bsoncore.UpdateLength(dst, wmIdx, int32(len(dst[wmIdx:]))), info, collName, nil
}
func (op Operation) legacyListCollections(ctx context.Context, dst []byte, srvr Server, conn Connection, desc description.SelectedServer) error {
wm, startedInfo, collName, err := op.createLegacyListCollectionsWiremessage(dst, desc)
if err != nil {
return err
}
startedInfo.connID = conn.ID()
op.publishStartedEvent(ctx, startedInfo)
finishedInfo := finishedInformation{
cmdName: startedInfo.cmdName,
requestID: startedInfo.requestID,
startTime: time.Now(),
connID: startedInfo.connID,
}
finishedInfo.response, finishedInfo.cmdErr = op.roundTripLegacyCursor(ctx, wm, srvr, conn, collName, firstBatchIdentifier)
op.publishFinishedEvent(ctx, finishedInfo)
if finishedInfo.cmdErr != nil {
return finishedInfo.cmdErr
}
if op.ProcessResponseFn != nil {
// CurrentIndex is always 0 in this mode.
info := ResponseInfo{
ServerResponse: finishedInfo.response,
Server: srvr,
Connection: conn,
ConnectionDescription: desc.Server,
}
return op.ProcessResponseFn(info)
}
return nil
}
func (op Operation) createLegacyListCollectionsWiremessage(dst []byte, desc description.SelectedServer) ([]byte, startedInformation, string, error) {
info := startedInformation{
cmdName: "find",
requestID: wiremessage.NextRequestID(),
}
var cmdDoc bsoncore.Document
var cmdIdx int32
var err error
cmdIdx, cmdDoc = bsoncore.AppendDocumentStart(cmdDoc)
if cmdDoc, err = op.CommandFn(cmdDoc, desc); err != nil {
return dst, info, "", err
}
cmdDoc, _ = bsoncore.AppendDocumentEnd(cmdDoc, cmdIdx)
info.cmd, err = op.convertCommandToFind(cmdDoc, listCollectionsNamespace)
if err != nil {
return nil, info, "", err
}
// lookup filter directly instead of calling cmdDoc.Elements() because the only listCollections option is nameOnly,
// which doesn't apply to legacy servers
var originalFilter bsoncore.Document
if filterVal, err := cmdDoc.LookupErr("filter"); err == nil {
originalFilter = filterVal.Document()
}
var optsElems []byte
filter, err := op.transformListCollectionsFilter(originalFilter)
if err != nil {
return dst, info, "", err
}
rp, err := op.createReadPref(desc, true)
if err != nil {
return dst, info, "", err
}
if len(rp) > 0 {
optsElems = bsoncore.AppendDocumentElement(optsElems, "$readPreference", rp)
}
var batchSize int32
if val, ok := cmdDoc.Lookup("cursor", "batchSize").AsInt32OK(); ok {
batchSize = val
}
var wmIdx int32
wmIdx, dst = wiremessage.AppendHeaderStart(dst, info.requestID, 0, wiremessage.OpQuery)
dst = wiremessage.AppendQueryFlags(dst, op.secondaryOK(desc))
dst = wiremessage.AppendQueryFullCollectionName(dst, op.getFullCollectionName(listCollectionsNamespace))
dst = wiremessage.AppendQueryNumberToSkip(dst, 0)
dst = wiremessage.AppendQueryNumberToReturn(dst, batchSize)
dst = op.appendLegacyQueryDocument(dst, filter, optsElems)
// leave out returnFieldsSelector because it is optional
return bsoncore.UpdateLength(dst, wmIdx, int32(len(dst[wmIdx:]))), info, listCollectionsNamespace, nil
}
func (op Operation) transformListCollectionsFilter(filter bsoncore.Document) (bsoncore.Document, error) {
// filter out results containing $ because those represent indexes
var regexFilter bsoncore.Document
var ridx int32
ridx, regexFilter = bsoncore.AppendDocumentStart(regexFilter)
regexFilter = bsoncore.AppendRegexElement(regexFilter, "name", "^[^$]*$", "")
regexFilter, _ = bsoncore.AppendDocumentEnd(regexFilter, ridx)
if len(filter) == 0 {
return regexFilter, nil
}
convertedIdx, convertedFilter := bsoncore.AppendDocumentStart(nil)
elems, err := filter.Elements()
if err != nil {
return nil, err
}
for _, elem := range elems {
if elem.Key() != "name" {
convertedFilter = append(convertedFilter, elem...)
continue
}
// the name value in a filter for legacy list collections must be a string and has to be prepended
// with the database name
nameVal := elem.Value()
if nameVal.Type != bsontype.String {
return nil, ErrFilterType
}
convertedFilter = bsoncore.AppendStringElement(convertedFilter, "name", op.getFullCollectionName(nameVal.StringValue()))
}
convertedFilter, _ = bsoncore.AppendDocumentEnd(convertedFilter, convertedIdx)
// combine regexFilter and convertedFilter with $and
var combinedFilter bsoncore.Document
var cidx, aidx int32
cidx, combinedFilter = bsoncore.AppendDocumentStart(combinedFilter)
aidx, combinedFilter = bsoncore.AppendArrayElementStart(combinedFilter, "$and")
combinedFilter = bsoncore.AppendDocumentElement(combinedFilter, "0", regexFilter)
combinedFilter = bsoncore.AppendDocumentElement(combinedFilter, "1", convertedFilter)
combinedFilter, _ = bsoncore.AppendArrayEnd(combinedFilter, aidx)
combinedFilter, _ = bsoncore.AppendDocumentEnd(combinedFilter, cidx)
return combinedFilter, nil
}
func (op Operation) legacyListIndexes(ctx context.Context, dst []byte, srvr Server, conn Connection, desc description.SelectedServer) error {
wm, startedInfo, collName, err := op.createLegacyListIndexesWiremessage(dst, desc)
if err != nil {
return err
}
startedInfo.connID = conn.ID()
op.publishStartedEvent(ctx, startedInfo)
finishedInfo := finishedInformation{
cmdName: startedInfo.cmdName,
requestID: startedInfo.requestID,
startTime: time.Now(),
connID: startedInfo.connID,
}
finishedInfo.response, finishedInfo.cmdErr = op.roundTripLegacyCursor(ctx, wm, srvr, conn, collName, firstBatchIdentifier)
op.publishFinishedEvent(ctx, finishedInfo)
if finishedInfo.cmdErr != nil {
return finishedInfo.cmdErr
}
if op.ProcessResponseFn != nil {
// CurrentIndex is always 0 in this mode.
info := ResponseInfo{
ServerResponse: finishedInfo.response,
Server: srvr,
Connection: conn,
ConnectionDescription: desc.Server,
}
return op.ProcessResponseFn(info)
}
return nil
}
func (op Operation) createLegacyListIndexesWiremessage(dst []byte, desc description.SelectedServer) ([]byte, startedInformation, string, error) {
info := startedInformation{
cmdName: "find",
requestID: wiremessage.NextRequestID(),
}
var cmdDoc bsoncore.Document
var cmdIndex int32
var err error
cmdIndex, cmdDoc = bsoncore.AppendDocumentStart(cmdDoc)
cmdDoc, err = op.CommandFn(cmdDoc, desc)
if err != nil {
return dst, info, "", err
}
cmdDoc, _ = bsoncore.AppendDocumentEnd(cmdDoc, cmdIndex)
info.cmd, err = op.convertCommandToFind(cmdDoc, listIndexesNamespace)
if err != nil {
return nil, info, "", err
}
cmdElems, err := cmdDoc.Elements()
if err != nil {
return nil, info, "", err
}
var filterCollName string
var batchSize int32
var optsElems []byte // options elements
for _, elem := range cmdElems {
switch elem.Key() {
case "listIndexes":
filterCollName = elem.Value().StringValue()
case "cursor":
// the batchSize option is embedded in a cursor subdocument
cursorDoc := elem.Value().Document()
if val, err := cursorDoc.LookupErr("batchSize"); err == nil {
batchSize = val.Int32()
}
case "maxTimeMS":
optsElems = bsoncore.AppendValueElement(optsElems, "$maxTimeMS", elem.Value())
}
}
// always filter with {ns: db.collection}
fidx, filter := bsoncore.AppendDocumentStart(nil)
filter = bsoncore.AppendStringElement(filter, "ns", op.getFullCollectionName(filterCollName))
filter, _ = bsoncore.AppendDocumentEnd(filter, fidx)
rp, err := op.createReadPref(desc, true)
if err != nil {
return dst, info, "", err
}
if len(rp) > 0 {
optsElems = bsoncore.AppendDocumentElement(optsElems, "$readPreference", rp)
}
var wmIdx int32
wmIdx, dst = wiremessage.AppendHeaderStart(dst, info.requestID, 0, wiremessage.OpQuery)
dst = wiremessage.AppendQueryFlags(dst, op.secondaryOK(desc))
dst = wiremessage.AppendQueryFullCollectionName(dst, op.getFullCollectionName(listIndexesNamespace))
dst = wiremessage.AppendQueryNumberToSkip(dst, 0)
dst = wiremessage.AppendQueryNumberToReturn(dst, batchSize)
dst = op.appendLegacyQueryDocument(dst, filter, optsElems)
// leave out returnFieldsSelector because it is optional
return bsoncore.UpdateLength(dst, wmIdx, int32(len(dst[wmIdx:]))), info, listIndexesNamespace, nil
}
// convertCommandToFind takes a non-legacy command document for a command that needs to be run as a find on legacy
// servers and converts it to a find command document for APM.
func (op Operation) convertCommandToFind(cmdDoc bsoncore.Document, collName string) (bsoncore.Document, error) {
cidx, converted := bsoncore.AppendDocumentStart(nil)
elems, err := cmdDoc.Elements()
if err != nil {
return nil, err
}
converted = bsoncore.AppendStringElement(converted, "find", collName)
// skip the first element because that will have the old command name
for i := 1; i < len(elems); i++ {
converted = bsoncore.AppendValueElement(converted, elems[i].Key(), elems[i].Value())
}
converted, _ = bsoncore.AppendDocumentEnd(converted, cidx)
return converted, nil
}
// appendLegacyQueryDocument takes a filter and a list of options elements for a legacy find operation, creates
// a query document, and appends it to dst.
func (op Operation) appendLegacyQueryDocument(dst []byte, filter bsoncore.Document, opts []byte) []byte {
if len(opts) == 0 {
dst = append(dst, filter...)
return dst
}
// filter must be wrapped in $query if other $-modifiers are used
var qidx int32
qidx, dst = bsoncore.AppendDocumentStart(dst)
dst = bsoncore.AppendDocumentElement(dst, "$query", filter)
dst = append(dst, opts...)
dst, _ = bsoncore.AppendDocumentEnd(dst, qidx)
return dst
}
// roundTripLegacyCursor sends a wiremessage for an operation expecting a cursor result and converts the legacy
// document sequence into a cursor document.
func (op Operation) roundTripLegacyCursor(ctx context.Context, wm []byte, srvr Server, conn Connection, collName, identifier string) (bsoncore.Document, error) {
wm, err := op.roundTripLegacy(ctx, conn, wm)
if ep, ok := srvr.(ErrorProcessor); ok {
_ = ep.ProcessError(err, conn)
}
if err != nil {
return nil, err
}
return op.upconvertCursorResponse(wm, identifier, collName)
}
// roundTripLegacy handles writing a wire message and reading the response.
func (op Operation) roundTripLegacy(ctx context.Context, conn Connection, wm []byte) ([]byte, error) {
err := conn.WriteWireMessage(ctx, wm)
if err != nil {
return nil, Error{Message: err.Error(), Labels: []string{TransientTransactionError, NetworkError}, Wrapped: err}
}
wm, err = conn.ReadWireMessage(ctx, wm[:0])
if err != nil {
err = Error{Message: err.Error(), Labels: []string{TransientTransactionError, NetworkError}, Wrapped: err}
}
return wm, err
}
func (op Operation) upconvertCursorResponse(wm []byte, batchIdentifier string, collName string) (bsoncore.Document, error) {
reply := op.decodeOpReply(wm, true)
if reply.err != nil {
return nil, reply.err
}
cursorIdx, cursorDoc := bsoncore.AppendDocumentStart(nil)
// convert reply documents to BSON array
var arrIdx int32
arrIdx, cursorDoc = bsoncore.AppendArrayElementStart(cursorDoc, batchIdentifier)
for i, doc := range reply.documents {
cursorDoc = bsoncore.AppendDocumentElement(cursorDoc, strconv.Itoa(i), doc)
}
cursorDoc, _ = bsoncore.AppendArrayEnd(cursorDoc, arrIdx)
cursorDoc = bsoncore.AppendInt64Element(cursorDoc, "id", reply.cursorID)
cursorDoc = bsoncore.AppendStringElement(cursorDoc, "ns", op.getFullCollectionName(collName))
cursorDoc, _ = bsoncore.AppendDocumentEnd(cursorDoc, cursorIdx)
resIdx, resDoc := bsoncore.AppendDocumentStart(nil)
resDoc = bsoncore.AppendInt32Element(resDoc, "ok", 1)
resDoc = bsoncore.AppendDocumentElement(resDoc, "cursor", cursorDoc)
resDoc, _ = bsoncore.AppendDocumentEnd(resDoc, resIdx)
return resDoc, nil
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package driver
// TestServerAPIVersion is the most recent, stable variant of options.ServerAPIVersion.
// Only to be used in testing.
const TestServerAPIVersion = "1"
// ServerAPIOptions represents options used to configure the API version sent to the server
// when running commands.
type ServerAPIOptions struct {
ServerAPIVersion string
Strict *bool
DeprecationErrors *bool
}
// NewServerAPIOptions creates a new ServerAPIOptions configured with the provided serverAPIVersion.
func NewServerAPIOptions(serverAPIVersion string) *ServerAPIOptions {
return &ServerAPIOptions{ServerAPIVersion: serverAPIVersion}
}
// SetStrict specifies whether the server should return errors for features that are not part of the API version.
func (s *ServerAPIOptions) SetStrict(strict bool) *ServerAPIOptions {
s.Strict = &strict
return s
}
// SetDeprecationErrors specifies whether the server should return errors for deprecated features.
func (s *ServerAPIOptions) SetDeprecationErrors(deprecationErrors bool) *ServerAPIOptions {
s.DeprecationErrors = &deprecationErrors
return s
}
@@ -0,0 +1,531 @@
// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package session // import "go.mongodb.org/mongo-driver/x/mongo/driver/session"
import (
"context"
"errors"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/internal/uuid"
"go.mongodb.org/mongo-driver/mongo/address"
"go.mongodb.org/mongo-driver/mongo/description"
"go.mongodb.org/mongo-driver/mongo/readconcern"
"go.mongodb.org/mongo-driver/mongo/readpref"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)
// ErrSessionEnded is returned when a client session is used after a call to endSession().
var ErrSessionEnded = errors.New("ended session was used")
// ErrNoTransactStarted is returned if a transaction operation is called when no transaction has started.
var ErrNoTransactStarted = errors.New("no transaction started")
// ErrTransactInProgress is returned if startTransaction() is called when a transaction is in progress.
var ErrTransactInProgress = errors.New("transaction already in progress")
// ErrAbortAfterCommit is returned when abort is called after a commit.
var ErrAbortAfterCommit = errors.New("cannot call abortTransaction after calling commitTransaction")
// ErrAbortTwice is returned if abort is called after transaction is already aborted.
var ErrAbortTwice = errors.New("cannot call abortTransaction twice")
// ErrCommitAfterAbort is returned if commit is called after an abort.
var ErrCommitAfterAbort = errors.New("cannot call commitTransaction after calling abortTransaction")
// ErrUnackWCUnsupported is returned if an unacknowledged write concern is supported for a transaction.
var ErrUnackWCUnsupported = errors.New("transactions do not support unacknowledged write concerns")
// ErrSnapshotTransaction is returned if an transaction is started on a snapshot session.
var ErrSnapshotTransaction = errors.New("transactions are not supported in snapshot sessions")
// Type describes the type of the session
type Type uint8
// These constants are the valid types for a client session.
const (
Explicit Type = iota
Implicit
)
// TransactionState indicates the state of the transactions FSM.
type TransactionState uint8
// Client Session states
const (
None TransactionState = iota
Starting
InProgress
Committed
Aborted
)
// String implements the fmt.Stringer interface.
func (s TransactionState) String() string {
switch s {
case None:
return "none"
case Starting:
return "starting"
case InProgress:
return "in progress"
case Committed:
return "committed"
case Aborted:
return "aborted"
default:
return "unknown"
}
}
// LoadBalancedTransactionConnection represents a connection that's pinned by a ClientSession because it's being used
// to execute a transaction when running against a load balancer. This interface is a copy of driver.PinnedConnection
// and exists to be able to pin transactions to a connection without causing an import cycle.
type LoadBalancedTransactionConnection interface {
// Functions copied over from driver.Connection.
WriteWireMessage(context.Context, []byte) error
ReadWireMessage(ctx context.Context, dst []byte) ([]byte, error)
Description() description.Server
Close() error
ID() string
ServerConnectionID() *int32
Address() address.Address
Stale() bool
// Functions copied over from driver.PinnedConnection that are not part of Connection or Expirable.
PinToCursor() error
PinToTransaction() error
UnpinFromCursor() error
UnpinFromTransaction() error
}
// Client is a session for clients to run commands.
type Client struct {
*Server
ClientID uuid.UUID
ClusterTime bson.Raw
Consistent bool // causal consistency
OperationTime *primitive.Timestamp
SessionType Type
Terminated bool
RetryingCommit bool
Committing bool
Aborting bool
RetryWrite bool
RetryRead bool
Snapshot bool
// options for the current transaction
// most recently set by transactionopt
CurrentRc *readconcern.ReadConcern
CurrentRp *readpref.ReadPref
CurrentWc *writeconcern.WriteConcern
CurrentMct *time.Duration
// default transaction options
transactionRc *readconcern.ReadConcern
transactionRp *readpref.ReadPref
transactionWc *writeconcern.WriteConcern
transactionMaxCommitTime *time.Duration
pool *Pool
TransactionState TransactionState
PinnedServer *description.Server
RecoveryToken bson.Raw
PinnedConnection LoadBalancedTransactionConnection
SnapshotTime *primitive.Timestamp
}
func getClusterTime(clusterTime bson.Raw) (uint32, uint32) {
if clusterTime == nil {
return 0, 0
}
clusterTimeVal, err := clusterTime.LookupErr("$clusterTime")
if err != nil {
return 0, 0
}
timestampVal, err := bson.Raw(clusterTimeVal.Value).LookupErr("clusterTime")
if err != nil {
return 0, 0
}
return timestampVal.Timestamp()
}
// MaxClusterTime compares 2 clusterTime documents and returns the document representing the highest cluster time.
func MaxClusterTime(ct1, ct2 bson.Raw) bson.Raw {
epoch1, ord1 := getClusterTime(ct1)
epoch2, ord2 := getClusterTime(ct2)
if epoch1 > epoch2 {
return ct1
} else if epoch1 < epoch2 {
return ct2
} else if ord1 > ord2 {
return ct1
} else if ord1 < ord2 {
return ct2
}
return ct1
}
// NewClientSession creates a Client.
func NewClientSession(pool *Pool, clientID uuid.UUID, sessionType Type, opts ...*ClientOptions) (*Client, error) {
mergedOpts := mergeClientOptions(opts...)
c := &Client{
ClientID: clientID,
SessionType: sessionType,
pool: pool,
}
if mergedOpts.DefaultReadPreference != nil {
c.transactionRp = mergedOpts.DefaultReadPreference
}
if mergedOpts.DefaultReadConcern != nil {
c.transactionRc = mergedOpts.DefaultReadConcern
}
if mergedOpts.DefaultWriteConcern != nil {
c.transactionWc = mergedOpts.DefaultWriteConcern
}
if mergedOpts.DefaultMaxCommitTime != nil {
c.transactionMaxCommitTime = mergedOpts.DefaultMaxCommitTime
}
if mergedOpts.Snapshot != nil {
c.Snapshot = *mergedOpts.Snapshot
}
// The default for causalConsistency is true, unless Snapshot is enabled, then it's false. Set
// the default and then allow any explicit causalConsistency setting to override it.
c.Consistent = !c.Snapshot
if mergedOpts.CausalConsistency != nil {
c.Consistent = *mergedOpts.CausalConsistency
}
if c.Consistent && c.Snapshot {
return nil, errors.New("causal consistency and snapshot cannot both be set for a session")
}
servSess, err := pool.GetSession()
if err != nil {
return nil, err
}
c.Server = servSess
return c, nil
}
// AdvanceClusterTime updates the session's cluster time.
func (c *Client) AdvanceClusterTime(clusterTime bson.Raw) error {
if c.Terminated {
return ErrSessionEnded
}
c.ClusterTime = MaxClusterTime(c.ClusterTime, clusterTime)
return nil
}
// AdvanceOperationTime updates the session's operation time.
func (c *Client) AdvanceOperationTime(opTime *primitive.Timestamp) error {
if c.Terminated {
return ErrSessionEnded
}
if c.OperationTime == nil {
c.OperationTime = opTime
return nil
}
if opTime.T > c.OperationTime.T {
c.OperationTime = opTime
} else if (opTime.T == c.OperationTime.T) && (opTime.I > c.OperationTime.I) {
c.OperationTime = opTime
}
return nil
}
// UpdateUseTime sets the session's last used time to the current time. This must be called whenever the session is
// used to send a command to the server to ensure that the session is not prematurely marked expired in the driver's
// session pool. If the session has already been ended, this method will return ErrSessionEnded.
func (c *Client) UpdateUseTime() error {
if c.Terminated {
return ErrSessionEnded
}
c.updateUseTime()
return nil
}
// UpdateRecoveryToken updates the session's recovery token from the server response.
func (c *Client) UpdateRecoveryToken(response bson.Raw) {
if c == nil {
return
}
token, err := response.LookupErr("recoveryToken")
if err != nil {
return
}
c.RecoveryToken = token.Document()
}
// UpdateSnapshotTime updates the session's value for the atClusterTime field of ReadConcern.
func (c *Client) UpdateSnapshotTime(response bsoncore.Document) {
if c == nil {
return
}
subDoc := response
if cur, ok := response.Lookup("cursor").DocumentOK(); ok {
subDoc = cur
}
ssTimeElem, err := subDoc.LookupErr("atClusterTime")
if err != nil {
// atClusterTime not included by the server
return
}
t, i := ssTimeElem.Timestamp()
c.SnapshotTime = &primitive.Timestamp{
T: t,
I: i,
}
}
// ClearPinnedResources clears the pinned server and/or connection associated with the session.
func (c *Client) ClearPinnedResources() error {
if c == nil {
return nil
}
c.PinnedServer = nil
if c.PinnedConnection != nil {
if err := c.PinnedConnection.UnpinFromTransaction(); err != nil {
return err
}
if err := c.PinnedConnection.Close(); err != nil {
return err
}
}
c.PinnedConnection = nil
return nil
}
// UnpinConnection gracefully unpins the connection associated with the session if there is one. This is done via
// the pinned connection's UnpinFromTransaction function.
func (c *Client) UnpinConnection() error {
if c == nil || c.PinnedConnection == nil {
return nil
}
err := c.PinnedConnection.UnpinFromTransaction()
closeErr := c.PinnedConnection.Close()
if err == nil && closeErr != nil {
err = closeErr
}
c.PinnedConnection = nil
return err
}
// EndSession ends the session.
func (c *Client) EndSession() {
if c.Terminated {
return
}
c.Terminated = true
c.pool.ReturnSession(c.Server)
}
// TransactionInProgress returns true if the client session is in an active transaction.
func (c *Client) TransactionInProgress() bool {
return c.TransactionState == InProgress
}
// TransactionStarting returns true if the client session is starting a transaction.
func (c *Client) TransactionStarting() bool {
return c.TransactionState == Starting
}
// TransactionRunning returns true if the client session has started the transaction
// and it hasn't been committed or aborted
func (c *Client) TransactionRunning() bool {
return c != nil && (c.TransactionState == Starting || c.TransactionState == InProgress)
}
// TransactionCommitted returns true of the client session just committed a transaction.
func (c *Client) TransactionCommitted() bool {
return c.TransactionState == Committed
}
// CheckStartTransaction checks to see if allowed to start transaction and returns
// an error if not allowed
func (c *Client) CheckStartTransaction() error {
if c.TransactionState == InProgress || c.TransactionState == Starting {
return ErrTransactInProgress
}
if c.Snapshot {
return ErrSnapshotTransaction
}
return nil
}
// StartTransaction initializes the transaction options and advances the state machine.
// It does not contact the server to start the transaction.
func (c *Client) StartTransaction(opts *TransactionOptions) error {
err := c.CheckStartTransaction()
if err != nil {
return err
}
c.IncrementTxnNumber()
c.RetryingCommit = false
if opts != nil {
c.CurrentRc = opts.ReadConcern
c.CurrentRp = opts.ReadPreference
c.CurrentWc = opts.WriteConcern
c.CurrentMct = opts.MaxCommitTime
}
if c.CurrentRc == nil {
c.CurrentRc = c.transactionRc
}
if c.CurrentRp == nil {
c.CurrentRp = c.transactionRp
}
if c.CurrentWc == nil {
c.CurrentWc = c.transactionWc
}
if c.CurrentMct == nil {
c.CurrentMct = c.transactionMaxCommitTime
}
if !writeconcern.AckWrite(c.CurrentWc) {
_ = c.clearTransactionOpts()
return ErrUnackWCUnsupported
}
c.TransactionState = Starting
return c.ClearPinnedResources()
}
// CheckCommitTransaction checks to see if allowed to commit transaction and returns
// an error if not allowed.
func (c *Client) CheckCommitTransaction() error {
if c.TransactionState == None {
return ErrNoTransactStarted
} else if c.TransactionState == Aborted {
return ErrCommitAfterAbort
}
return nil
}
// CommitTransaction updates the state for a successfully committed transaction and returns
// an error if not permissible. It does not actually perform the commit.
func (c *Client) CommitTransaction() error {
err := c.CheckCommitTransaction()
if err != nil {
return err
}
c.TransactionState = Committed
return nil
}
// UpdateCommitTransactionWriteConcern will set the write concern to majority and potentially set a
// w timeout of 10 seconds. This should be called after a commit transaction operation fails with a
// retryable error or after a successful commit transaction operation.
func (c *Client) UpdateCommitTransactionWriteConcern() {
wc := c.CurrentWc
timeout := 10 * time.Second
if wc != nil && wc.GetWTimeout() != 0 {
timeout = wc.GetWTimeout()
}
c.CurrentWc = wc.WithOptions(writeconcern.WMajority(), writeconcern.WTimeout(timeout))
}
// CheckAbortTransaction checks to see if allowed to abort transaction and returns
// an error if not allowed.
func (c *Client) CheckAbortTransaction() error {
if c.TransactionState == None {
return ErrNoTransactStarted
} else if c.TransactionState == Committed {
return ErrAbortAfterCommit
} else if c.TransactionState == Aborted {
return ErrAbortTwice
}
return nil
}
// AbortTransaction updates the state for a successfully aborted transaction and returns
// an error if not permissible. It does not actually perform the abort.
func (c *Client) AbortTransaction() error {
err := c.CheckAbortTransaction()
if err != nil {
return err
}
c.TransactionState = Aborted
return c.clearTransactionOpts()
}
// StartCommand updates the session's internal state at the beginning of an operation. This must be called before
// server selection is done for the operation as the session's state can impact the result of that process.
func (c *Client) StartCommand() error {
if c == nil {
return nil
}
// If we're executing the first operation using this session after a transaction, we must ensure that the session
// is not pinned to any resources.
if !c.TransactionRunning() && !c.Committing && !c.Aborting {
return c.ClearPinnedResources()
}
return nil
}
// ApplyCommand advances the state machine upon command execution. This must be called after server selection is
// complete.
func (c *Client) ApplyCommand(desc description.Server) error {
if c.Committing {
// Do not change state if committing after already committed
return nil
}
if c.TransactionState == Starting {
c.TransactionState = InProgress
// If this is in a transaction and the server is a mongos, pin it
if desc.Kind == description.Mongos {
c.PinnedServer = &desc
}
} else if c.TransactionState == Committed || c.TransactionState == Aborted {
c.TransactionState = None
return c.clearTransactionOpts()
}
return nil
}
func (c *Client) clearTransactionOpts() error {
c.RetryingCommit = false
c.Aborting = false
c.Committing = false
c.CurrentWc = nil
c.CurrentRp = nil
c.CurrentRc = nil
c.RecoveryToken = nil
return c.ClearPinnedResources()
}

Some files were not shown because too many files have changed in this diff Show More