switch back to upstream
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
This commit is contained in:
+66
-8
@@ -412,7 +412,7 @@ func readPacket(reader io.Reader) (*Packet, int, error) {
|
||||
p.Value = val
|
||||
}
|
||||
case TagRelativeOID:
|
||||
oid, err := parseObjectIdentifier(content)
|
||||
oid, err := parseRelativeObjectIdentifier(content)
|
||||
if err == nil {
|
||||
p.Value = OIDToString(oid)
|
||||
}
|
||||
@@ -560,16 +560,14 @@ func NewBoolean(classType Class, tagType Type, tag Tag, value bool, description
|
||||
|
||||
// NewLDAPBoolean returns a RFC 4511-compliant Boolean packet.
|
||||
func NewLDAPBoolean(classType Class, tagType Type, tag Tag, value bool, description string) *Packet {
|
||||
intValue := int64(0)
|
||||
|
||||
if value {
|
||||
intValue = 255
|
||||
}
|
||||
|
||||
p := Encode(classType, tagType, tag, nil, description)
|
||||
|
||||
p.Value = value
|
||||
p.Data.Write(encodeInteger(intValue))
|
||||
if value {
|
||||
p.Data.Write([]byte{255})
|
||||
} else {
|
||||
p.Data.Write([]byte{0})
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
@@ -663,6 +661,25 @@ func NewOID(classType Class, tagType Type, tag Tag, value interface{}, descripti
|
||||
return p
|
||||
}
|
||||
|
||||
func NewRelativeOID(classType Class, tagType Type, tag Tag, value interface{}, description string) *Packet {
|
||||
p := Encode(classType, tagType, tag, nil, description)
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
encoded, err := encodeRelativeOID(v)
|
||||
if err != nil {
|
||||
fmt.Printf("failed writing %v", err)
|
||||
return nil
|
||||
}
|
||||
p.Value = v
|
||||
p.Data.Write(encoded)
|
||||
// TODO: support []int already ?
|
||||
default:
|
||||
panic(fmt.Sprintf("Invalid type %T, expected float{64|32}", v))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// encodeOID takes a string representation of an OID and returns its DER-encoded byte slice along with any error.
|
||||
func encodeOID(oidString string) ([]byte, error) {
|
||||
// Convert the string representation to an asn1.ObjectIdentifier
|
||||
@@ -688,6 +705,26 @@ func encodeOID(oidString string) ([]byte, error) {
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func encodeRelativeOID(oidString string) ([]byte, error) {
|
||||
parts := strings.Split(oidString, ".")
|
||||
oid := make([]int, len(parts))
|
||||
for i, part := range parts {
|
||||
var val int
|
||||
if _, err := fmt.Sscanf(part, "%d", &val); err != nil {
|
||||
return nil, fmt.Errorf("invalid RELATIVE OID part '%s': %w", part, err)
|
||||
}
|
||||
oid[i] = val
|
||||
}
|
||||
|
||||
encoded := make([]byte, 0)
|
||||
|
||||
for i := 0; i < len(oid); i++ {
|
||||
encoded = appendBase128Int(encoded, int64(oid[i]))
|
||||
}
|
||||
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func appendBase128Int(dst []byte, n int64) []byte {
|
||||
l := base128IntLength(n)
|
||||
|
||||
@@ -772,6 +809,27 @@ func parseObjectIdentifier(bytes []byte) (s []int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func parseRelativeObjectIdentifier(bytes []byte) (s []int, err error) {
|
||||
if len(bytes) == 0 {
|
||||
err = fmt.Errorf("zero length RELATIVE OBJECT IDENTIFIER")
|
||||
return
|
||||
}
|
||||
|
||||
s = make([]int, len(bytes)+1)
|
||||
|
||||
var v, offset int
|
||||
i := 0
|
||||
for ; offset < len(bytes); i++ {
|
||||
v, offset, err = parseBase128Int(bytes, offset)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s[i] = v
|
||||
}
|
||||
s = s[0:i]
|
||||
return
|
||||
}
|
||||
|
||||
// parseBase128Int parses a base-128 encoded int from the given offset in the
|
||||
// given byte slice. It returns the value and the new offset.
|
||||
func parseBase128Int(bytes []byte, initOffset int) (ret, offset int, err error) {
|
||||
|
||||
+94
-12
@@ -3,15 +3,19 @@ package ldap
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
enchex "encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/Azure/go-ntlmssp"
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"golang.org/x/crypto/md4" //nolint:staticcheck
|
||||
)
|
||||
|
||||
// SimpleBindRequest represents a username/password bind operation
|
||||
@@ -216,7 +220,7 @@ func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*Diges
|
||||
}
|
||||
}
|
||||
|
||||
if params != nil {
|
||||
if len(params) > 0 {
|
||||
resp := computeResponse(
|
||||
params,
|
||||
"ldap/"+strings.ToLower(digestMD5BindRequest.Host),
|
||||
@@ -249,6 +253,34 @@ func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*Diges
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet: %s", err)
|
||||
}
|
||||
|
||||
if len(packet.Children) == 2 {
|
||||
response := packet.Children[1]
|
||||
if response == nil {
|
||||
return result, GetLDAPError(packet)
|
||||
}
|
||||
if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 {
|
||||
if ber.Type(response.Children[0].Tag) == ber.Type(ber.TagInteger) || ber.Type(response.Children[0].Tag) == ber.Type(ber.TagEnumerated) {
|
||||
resultCode := uint16(response.Children[0].Value.(int64))
|
||||
if resultCode == 14 {
|
||||
msgCtx, err := l.doRequest(digestMD5BindRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer l.finishMessage(msgCtx)
|
||||
packetResponse, ok := <-msgCtx.responses
|
||||
if !ok {
|
||||
return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed"))
|
||||
}
|
||||
packet, err = packetResponse.ReadPacket()
|
||||
l.Debug.Printf("%d: got response %p", msgCtx.id, packet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = GetLDAPError(packet)
|
||||
@@ -406,17 +438,36 @@ type NTLMBindRequest struct {
|
||||
Hash string
|
||||
// Controls are optional controls to send with the bind request
|
||||
Controls []Control
|
||||
// Negotiator allows to specify a custom NTLM negotiator.
|
||||
Negotiator NTLMNegotiator
|
||||
}
|
||||
|
||||
func (req *NTLMBindRequest) appendTo(envelope *ber.Packet) error {
|
||||
// NTLMNegotiator is an abstraction of an NTLM implementation that produces and
|
||||
// processes NTLM binary tokens.
|
||||
type NTLMNegotiator interface {
|
||||
Negotiate(domain string, workstation string) ([]byte, error)
|
||||
ChallengeResponse(challenge []byte, username string, hash string) ([]byte, error)
|
||||
}
|
||||
|
||||
func (req *NTLMBindRequest) appendTo(envelope *ber.Packet) (err error) {
|
||||
request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request")
|
||||
request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version"))
|
||||
request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name"))
|
||||
|
||||
var negMessage []byte
|
||||
|
||||
// generate an NTLMSSP Negotiation message for the specified domain (it can be blank)
|
||||
negMessage, err := ntlmssp.NewNegotiateMessage(req.Domain, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("err creating negmessage: %s", err)
|
||||
switch {
|
||||
case req.Negotiator == nil:
|
||||
negMessage, err = ntlmssp.NewNegotiateMessage(req.Domain, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create NTLM negotiate message: %s", err)
|
||||
}
|
||||
default:
|
||||
negMessage, err = req.Negotiator.Negotiate(req.Domain, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create NTLM negotiate message with custom negotiator: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// append the generated NTLMSSP message as a TagEnumerated BER value
|
||||
@@ -514,18 +565,29 @@ func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindRes
|
||||
if ntlmsspChallenge != nil {
|
||||
var err error
|
||||
var responseMessage []byte
|
||||
// generate a response message to the challenge with the given Username/Password if password is provided
|
||||
if ntlmBindRequest.Hash != "" {
|
||||
|
||||
switch {
|
||||
case ntlmBindRequest.Hash == "" && ntlmBindRequest.Password == "" && !ntlmBindRequest.AllowEmptyPassword:
|
||||
err = fmt.Errorf("need a password or hash to generate reply")
|
||||
case ntlmBindRequest.Negotiator == nil && ntlmBindRequest.Hash != "":
|
||||
responseMessage, err = ntlmssp.ProcessChallengeWithHash(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Hash)
|
||||
} else if ntlmBindRequest.Password != "" || ntlmBindRequest.AllowEmptyPassword {
|
||||
case ntlmBindRequest.Negotiator == nil && (ntlmBindRequest.Password != "" || ntlmBindRequest.AllowEmptyPassword):
|
||||
// generate a response message to the challenge with the given Username/Password if password is provided
|
||||
_, _, domainNeeded := ntlmssp.GetDomain(ntlmBindRequest.Username)
|
||||
responseMessage, err = ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password, domainNeeded)
|
||||
} else {
|
||||
err = fmt.Errorf("need a password or hash to generate reply")
|
||||
default:
|
||||
hash := ntlmBindRequest.Hash
|
||||
if len(hash) == 0 {
|
||||
hash = ntHash(ntlmBindRequest.Password)
|
||||
}
|
||||
|
||||
responseMessage, err = ntlmBindRequest.Negotiator.ChallengeResponse(ntlmsspChallenge, ntlmBindRequest.Username, hash)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("parsing ntlm-challenge: %s", err)
|
||||
return result, fmt.Errorf("process NTLM challenge: %s", err)
|
||||
}
|
||||
|
||||
packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
|
||||
packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID"))
|
||||
|
||||
@@ -559,6 +621,18 @@ func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindRes
|
||||
return result, err
|
||||
}
|
||||
|
||||
func ntHash(pass string) string {
|
||||
runes := utf16.Encode([]rune(pass))
|
||||
|
||||
b := bytes.Buffer{}
|
||||
_ = binary.Write(&b, binary.LittleEndian, &runes)
|
||||
|
||||
hash := md4.New()
|
||||
_, _ = hash.Write(b.Bytes())
|
||||
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
// GSSAPIClient interface is used as the client-side implementation for the
|
||||
// GSSAPI SASL mechanism.
|
||||
// Interface inspired by GSSAPIClient from golang.org/x/crypto/ssh
|
||||
@@ -577,6 +651,9 @@ type GSSAPIClient interface {
|
||||
// to InitSecContext via the token parameters.
|
||||
// See RFC 4752 section 3.1.
|
||||
InitSecContext(target string, token []byte) (outputToken []byte, needContinue bool, err error)
|
||||
// InitSecContextWithOptions is the same as InitSecContext but allows for additional options to be passed to the context establishment.
|
||||
// See RFC 4752 section 3.1.
|
||||
InitSecContextWithOptions(target string, token []byte, options []int) (outputToken []byte, needContinue bool, err error)
|
||||
// NegotiateSaslAuth performs the last step of the Sasl handshake.
|
||||
// It takes a token, which, when unwrapped, describes the servers supported
|
||||
// security layers (first octet) and maximum receive buffer (remaining
|
||||
@@ -614,6 +691,11 @@ func (l *Conn) GSSAPIBind(client GSSAPIClient, servicePrincipal, authzid string)
|
||||
|
||||
// GSSAPIBindRequest performs the GSSAPI SASL bind using the provided GSSAPI client.
|
||||
func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest) error {
|
||||
return l.GSSAPIBindRequestWithAPOptions(client, req, []int{})
|
||||
}
|
||||
|
||||
// GSSAPIBindRequest performs the GSSAPI SASL bind using the provided GSSAPI client.
|
||||
func (l *Conn) GSSAPIBindRequestWithAPOptions(client GSSAPIClient, req *GSSAPIBindRequest, APOptions []int) error {
|
||||
//nolint:errcheck
|
||||
defer client.DeleteSecContext()
|
||||
|
||||
@@ -624,7 +706,7 @@ func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest) er
|
||||
for {
|
||||
if needInit {
|
||||
// Establish secure context between client and server.
|
||||
reqToken, needInit, err = client.InitSecContext(req.ServicePrincipalName, recvToken)
|
||||
reqToken, needInit, err = client.InitSecContextWithOptions(req.ServicePrincipalName, recvToken, APOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+9
-6
@@ -709,7 +709,7 @@ func (c *ControlDirSync) Encode() *ber.Packet {
|
||||
|
||||
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control")
|
||||
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeDirSync, "Control Type ("+ControlTypeMap[ControlTypeDirSync]+")"))
|
||||
packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) // must be true always
|
||||
packet.AppendChild(ber.NewLDAPBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) // must be true always
|
||||
|
||||
val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (DirSync)")
|
||||
seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "DirSync Control Value")
|
||||
@@ -755,7 +755,7 @@ func NewControlServerSideSorting(value *ber.Packet) (*ControlServerSideSorting,
|
||||
sequences := val[0].Children
|
||||
|
||||
for i, sequence := range sequences {
|
||||
sortKey := &SortKey{}
|
||||
sortKey := new(SortKey)
|
||||
|
||||
if len(sequence.Children) < 2 {
|
||||
return nil, fmt.Errorf("attributeType or matchingRule is missing from sequence %d", i)
|
||||
@@ -864,10 +864,14 @@ func (c ControlServerSideSortingCode) Valid() error {
|
||||
}
|
||||
|
||||
func NewControlServerSideSortingResult(pkt *ber.Packet) (*ControlServerSideSortingResult, error) {
|
||||
control := &ControlServerSideSortingResult{}
|
||||
control := new(ControlServerSideSortingResult)
|
||||
|
||||
if pkt == nil || len(pkt.Children) == 0 {
|
||||
return nil, fmt.Errorf("bad packet")
|
||||
// This is currently not compliant with the ServerSideSorting RFC (see https://datatracker.ietf.org/doc/html/rfc2891#section-1.2).
|
||||
// but it's necessary because there seems to be a bug in the implementation of the popular OpenLDAP server.
|
||||
//
|
||||
// See: https://github.com/go-ldap/ldap/pull/546
|
||||
return control, nil
|
||||
}
|
||||
|
||||
codeInt, err := ber.ParseInt64(pkt.Children[0].Data.Bytes())
|
||||
@@ -875,8 +879,7 @@ func NewControlServerSideSortingResult(pkt *ber.Packet) (*ControlServerSideSorti
|
||||
return nil, err
|
||||
}
|
||||
|
||||
code := ControlServerSideSortingCode(codeInt)
|
||||
if err := code.Valid(); err != nil {
|
||||
if err = ControlServerSideSortingCode(codeInt).Valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+9
-4
@@ -2,9 +2,9 @@
|
||||
|
||||
[](https://github.com/avelino/awesome-go#utilities)
|
||||
|
||||
[](https://github.com/jonboulle/clockwork/actions?query=workflow%3ACI)
|
||||
[](https://github.com/jonboulle/clockwork/actions?query=workflow%3ACI)
|
||||
[](https://goreportcard.com/report/github.com/jonboulle/clockwork)
|
||||

|
||||

|
||||
[](https://pkg.go.dev/mod/github.com/jonboulle/clockwork)
|
||||
|
||||
**A simple fake clock for Go.**
|
||||
@@ -36,6 +36,7 @@ Now you can easily test `myFunc` with a `FakeClock`:
|
||||
|
||||
```go
|
||||
func TestMyFunc(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c := clockwork.NewFakeClock()
|
||||
|
||||
// Start our sleepy function
|
||||
@@ -46,8 +47,12 @@ func TestMyFunc(t *testing.T) {
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// Ensure we wait until myFunc is sleeping
|
||||
c.BlockUntil(1)
|
||||
// Ensure we wait until myFunc is waiting on the clock.
|
||||
// Use a context to avoid blocking forever if something
|
||||
// goes wrong.
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
c.BlockUntilContext(ctx, 1)
|
||||
|
||||
assertState()
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Security Policy
|
||||
|
||||
If you have discovered a security vulnerability in this project, please report it
|
||||
privately. **Do not disclose it as a public issue.** This gives me time to work with you
|
||||
to fix the issue before public exposure, reducing the chance that the exploit will be
|
||||
used before a patch is released.
|
||||
|
||||
You may submit the report in the following ways:
|
||||
|
||||
- send an email to ???@???; and/or
|
||||
- send a [private vulnerability report](https://github.com/jonboulle/clockwork/security/advisories/new)
|
||||
|
||||
Please provide the following information in your report:
|
||||
|
||||
- A description of the vulnerability and its impact
|
||||
- How to reproduce the issue
|
||||
|
||||
This project is maintained by a single maintainer on a reasonable-effort basis. As such,
|
||||
please give me 90 days to work on a fix before public exposure.
|
||||
+237
-113
@@ -1,30 +1,25 @@
|
||||
// Package clockwork contains a simple fake clock for Go.
|
||||
package clockwork
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Clock provides an interface that packages can use instead of directly
|
||||
// using the time module, so that chronology-related behavior can be tested
|
||||
// Clock provides an interface that packages can use instead of directly using
|
||||
// the [time] module, so that chronology-related behavior can be tested.
|
||||
type Clock interface {
|
||||
After(d time.Duration) <-chan time.Time
|
||||
Sleep(d time.Duration)
|
||||
Now() time.Time
|
||||
Since(t time.Time) time.Duration
|
||||
Until(t time.Time) time.Duration
|
||||
NewTicker(d time.Duration) Ticker
|
||||
}
|
||||
|
||||
// FakeClock provides an interface for a clock which can be
|
||||
// manually advanced through time
|
||||
type FakeClock interface {
|
||||
Clock
|
||||
// Advance advances the FakeClock to a new point in time, ensuring any existing
|
||||
// sleepers are notified appropriately before returning
|
||||
Advance(d time.Duration)
|
||||
// BlockUntil will block until the FakeClock has the given number of
|
||||
// sleepers (callers of Sleep or After)
|
||||
BlockUntil(n int)
|
||||
NewTimer(d time.Duration) Timer
|
||||
AfterFunc(d time.Duration, f func()) Timer
|
||||
}
|
||||
|
||||
// NewRealClock returns a Clock which simply delegates calls to the actual time
|
||||
@@ -33,21 +28,6 @@ func NewRealClock() Clock {
|
||||
return &realClock{}
|
||||
}
|
||||
|
||||
// NewFakeClock returns a FakeClock implementation which can be
|
||||
// manually advanced through time for testing. The initial time of the
|
||||
// FakeClock will be an arbitrary non-zero time.
|
||||
func NewFakeClock() FakeClock {
|
||||
// use a fixture that does not fulfill Time.IsZero()
|
||||
return NewFakeClockAt(time.Date(1984, time.April, 4, 0, 0, 0, 0, time.UTC))
|
||||
}
|
||||
|
||||
// NewFakeClockAt returns a FakeClock initialised at the given time.Time.
|
||||
func NewFakeClockAt(t time.Time) FakeClock {
|
||||
return &fakeClock{
|
||||
time: t,
|
||||
}
|
||||
}
|
||||
|
||||
type realClock struct{}
|
||||
|
||||
func (rc *realClock) After(d time.Duration) <-chan time.Time {
|
||||
@@ -66,130 +46,274 @@ func (rc *realClock) Since(t time.Time) time.Duration {
|
||||
return rc.Now().Sub(t)
|
||||
}
|
||||
|
||||
func (rc *realClock) NewTicker(d time.Duration) Ticker {
|
||||
return &realTicker{time.NewTicker(d)}
|
||||
func (rc *realClock) Until(t time.Time) time.Duration {
|
||||
return t.Sub(rc.Now())
|
||||
}
|
||||
|
||||
type fakeClock struct {
|
||||
sleepers []*sleeper
|
||||
func (rc *realClock) NewTicker(d time.Duration) Ticker {
|
||||
return realTicker{time.NewTicker(d)}
|
||||
}
|
||||
|
||||
func (rc *realClock) NewTimer(d time.Duration) Timer {
|
||||
return realTimer{time.NewTimer(d)}
|
||||
}
|
||||
|
||||
func (rc *realClock) AfterFunc(d time.Duration, f func()) Timer {
|
||||
return realTimer{time.AfterFunc(d, f)}
|
||||
}
|
||||
|
||||
// FakeClock provides an interface for a clock which can be manually advanced
|
||||
// through time.
|
||||
//
|
||||
// FakeClock maintains a list of "waiters," which consists of all callers
|
||||
// waiting on the underlying clock (i.e. Tickers and Timers including callers of
|
||||
// Sleep or After). Users can call BlockUntil to block until the clock has an
|
||||
// expected number of waiters.
|
||||
type FakeClock struct {
|
||||
// l protects all attributes of the clock, including all attributes of all
|
||||
// waiters and blockers.
|
||||
l sync.RWMutex
|
||||
waiters []expirer
|
||||
blockers []*blocker
|
||||
time time.Time
|
||||
|
||||
l sync.RWMutex
|
||||
}
|
||||
|
||||
// sleeper represents a caller of After or Sleep
|
||||
type sleeper struct {
|
||||
until time.Time
|
||||
done chan time.Time
|
||||
// NewFakeClock returns a FakeClock implementation which can be
|
||||
// manually advanced through time for testing. The initial time of the
|
||||
// FakeClock will be the current system time.
|
||||
//
|
||||
// Tests that require a deterministic time must use NewFakeClockAt.
|
||||
func NewFakeClock() *FakeClock {
|
||||
return NewFakeClockAt(time.Now())
|
||||
}
|
||||
|
||||
// blocker represents a caller of BlockUntil
|
||||
// NewFakeClockAt returns a FakeClock initialised at the given time.Time.
|
||||
func NewFakeClockAt(t time.Time) *FakeClock {
|
||||
return &FakeClock{
|
||||
time: t,
|
||||
}
|
||||
}
|
||||
|
||||
// blocker is a caller of BlockUntil.
|
||||
type blocker struct {
|
||||
count int
|
||||
ch chan struct{}
|
||||
|
||||
// ch is closed when the underlying clock has the specified number of blockers.
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
// After mimics time.After; it waits for the given duration to elapse on the
|
||||
// expirer is a timer or ticker that expires at some point in the future.
|
||||
type expirer interface {
|
||||
// expire the expirer at the given time, returning the desired duration until
|
||||
// the next expiration, if any.
|
||||
expire(now time.Time) (next *time.Duration)
|
||||
|
||||
// Get and set the expiration time.
|
||||
expiration() time.Time
|
||||
setExpiration(time.Time)
|
||||
}
|
||||
|
||||
// After mimics [time.After]; it waits for the given duration to elapse on the
|
||||
// fakeClock, then sends the current time on the returned channel.
|
||||
func (fc *fakeClock) After(d time.Duration) <-chan time.Time {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
now := fc.time
|
||||
done := make(chan time.Time, 1)
|
||||
if d.Nanoseconds() <= 0 {
|
||||
// special case - trigger immediately
|
||||
done <- now
|
||||
} else {
|
||||
// otherwise, add to the set of sleepers
|
||||
s := &sleeper{
|
||||
until: now.Add(d),
|
||||
done: done,
|
||||
}
|
||||
fc.sleepers = append(fc.sleepers, s)
|
||||
// and notify any blockers
|
||||
fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers))
|
||||
}
|
||||
return done
|
||||
func (fc *FakeClock) After(d time.Duration) <-chan time.Time {
|
||||
return fc.NewTimer(d).Chan()
|
||||
}
|
||||
|
||||
// notifyBlockers notifies all the blockers waiting until the
|
||||
// given number of sleepers are waiting on the fakeClock. It
|
||||
// returns an updated slice of blockers (i.e. those still waiting)
|
||||
func notifyBlockers(blockers []*blocker, count int) (newBlockers []*blocker) {
|
||||
for _, b := range blockers {
|
||||
if b.count == count {
|
||||
close(b.ch)
|
||||
} else {
|
||||
newBlockers = append(newBlockers, b)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Sleep blocks until the given duration has passed on the fakeClock
|
||||
func (fc *fakeClock) Sleep(d time.Duration) {
|
||||
// Sleep blocks until the given duration has passed on the fakeClock.
|
||||
func (fc *FakeClock) Sleep(d time.Duration) {
|
||||
<-fc.After(d)
|
||||
}
|
||||
|
||||
// Time returns the current time of the fakeClock
|
||||
func (fc *fakeClock) Now() time.Time {
|
||||
// Now returns the current time of the fakeClock
|
||||
func (fc *FakeClock) Now() time.Time {
|
||||
fc.l.RLock()
|
||||
t := fc.time
|
||||
fc.l.RUnlock()
|
||||
return t
|
||||
defer fc.l.RUnlock()
|
||||
return fc.time
|
||||
}
|
||||
|
||||
// Since returns the duration that has passed since the given time on the fakeClock
|
||||
func (fc *fakeClock) Since(t time.Time) time.Duration {
|
||||
// Since returns the duration that has passed since the given time on the
|
||||
// fakeClock.
|
||||
func (fc *FakeClock) Since(t time.Time) time.Duration {
|
||||
return fc.Now().Sub(t)
|
||||
}
|
||||
|
||||
func (fc *fakeClock) NewTicker(d time.Duration) Ticker {
|
||||
ft := &fakeTicker{
|
||||
c: make(chan time.Time, 1),
|
||||
stop: make(chan bool, 1),
|
||||
clock: fc,
|
||||
period: d,
|
||||
// Until returns the duration that has to pass from the given time on the fakeClock
|
||||
// to reach the given time.
|
||||
func (fc *FakeClock) Until(t time.Time) time.Duration {
|
||||
return t.Sub(fc.Now())
|
||||
}
|
||||
|
||||
// NewTicker returns a Ticker that will expire only after calls to
|
||||
// FakeClock.Advance() have moved the clock past the given duration.
|
||||
//
|
||||
// The duration d must be greater than zero; if not, NewTicker will panic.
|
||||
func (fc *FakeClock) NewTicker(d time.Duration) Ticker {
|
||||
// Maintain parity with
|
||||
// https://cs.opensource.google/go/go/+/refs/tags/go1.20.3:src/time/tick.go;l=23-25
|
||||
if d <= 0 {
|
||||
panic(errors.New("non-positive interval for NewTicker"))
|
||||
}
|
||||
ft.runTickThread()
|
||||
ft := newFakeTicker(fc, d)
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
fc.setExpirer(ft, d)
|
||||
return ft
|
||||
}
|
||||
|
||||
// Advance advances fakeClock to a new point in time, ensuring channels from any
|
||||
// previous invocations of After are notified appropriately before returning
|
||||
func (fc *fakeClock) Advance(d time.Duration) {
|
||||
// NewTimer returns a Timer that will fire only after calls to
|
||||
// fakeClock.Advance() have moved the clock past the given duration.
|
||||
func (fc *FakeClock) NewTimer(d time.Duration) Timer {
|
||||
t, _ := fc.newTimer(d, nil)
|
||||
return t
|
||||
}
|
||||
|
||||
// AfterFunc mimics [time.AfterFunc]; it returns a Timer that will invoke the
|
||||
// given function only after calls to fakeClock.Advance() have moved the clock
|
||||
// past the given duration.
|
||||
func (fc *FakeClock) AfterFunc(d time.Duration, f func()) Timer {
|
||||
t, _ := fc.newTimer(d, f)
|
||||
return t
|
||||
}
|
||||
|
||||
// newTimer returns a new timer using an optional afterFunc and the time that
|
||||
// timer expires.
|
||||
func (fc *FakeClock) newTimer(d time.Duration, afterfunc func()) (*fakeTimer, time.Time) {
|
||||
ft := newFakeTimer(fc, afterfunc)
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
fc.setExpirer(ft, d)
|
||||
return ft, ft.expiration()
|
||||
}
|
||||
|
||||
// newTimerAtTime is like newTimer, but uses a time instead of a duration.
|
||||
//
|
||||
// It is used to ensure FakeClock's lock is held constant through calling
|
||||
// fc.After(t.Sub(fc.Now())). It should not be exposed externally.
|
||||
func (fc *FakeClock) newTimerAtTime(t time.Time, afterfunc func()) *fakeTimer {
|
||||
ft := newFakeTimer(fc, afterfunc)
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
fc.setExpirer(ft, t.Sub(fc.time))
|
||||
return ft
|
||||
}
|
||||
|
||||
// Advance advances fakeClock to a new point in time, ensuring waiters and
|
||||
// blockers are notified appropriately before returning.
|
||||
func (fc *FakeClock) Advance(d time.Duration) {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
end := fc.time.Add(d)
|
||||
var newSleepers []*sleeper
|
||||
for _, s := range fc.sleepers {
|
||||
if end.Sub(s.until) >= 0 {
|
||||
s.done <- end
|
||||
} else {
|
||||
newSleepers = append(newSleepers, s)
|
||||
// Expire the earliest waiter until the earliest waiter's expiration is after
|
||||
// end.
|
||||
//
|
||||
// We don't iterate because the callback of the waiter might register a new
|
||||
// waiter, so the list of waiters might change as we execute this.
|
||||
for len(fc.waiters) > 0 && !end.Before(fc.waiters[0].expiration()) {
|
||||
w := fc.waiters[0]
|
||||
fc.waiters = fc.waiters[1:]
|
||||
|
||||
// Use the waiter's expiration as the current time for this expiration.
|
||||
now := w.expiration()
|
||||
fc.time = now
|
||||
if d := w.expire(now); d != nil {
|
||||
// Set the new expiration if needed.
|
||||
fc.setExpirer(w, *d)
|
||||
}
|
||||
}
|
||||
fc.sleepers = newSleepers
|
||||
fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers))
|
||||
fc.time = end
|
||||
}
|
||||
|
||||
// BlockUntil will block until the fakeClock has the given number of sleepers
|
||||
// (callers of Sleep or After)
|
||||
func (fc *fakeClock) BlockUntil(n int) {
|
||||
fc.l.Lock()
|
||||
// Fast path: current number of sleepers is what we're looking for
|
||||
if len(fc.sleepers) == n {
|
||||
fc.l.Unlock()
|
||||
return
|
||||
// BlockUntil blocks until the FakeClock has the given number of waiters.
|
||||
//
|
||||
// Prefer BlockUntilContext in new code, which offers context cancellation to
|
||||
// prevent deadlock.
|
||||
//
|
||||
// Deprecated: New code should prefer BlockUntilContext.
|
||||
func (fc *FakeClock) BlockUntil(n int) {
|
||||
fc.BlockUntilContext(context.TODO(), n)
|
||||
}
|
||||
|
||||
// BlockUntilContext blocks until the fakeClock has the given number of waiters
|
||||
// or the context is cancelled.
|
||||
func (fc *FakeClock) BlockUntilContext(ctx context.Context, n int) error {
|
||||
b := fc.newBlocker(n)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
// Otherwise, set up a new blocker
|
||||
|
||||
select {
|
||||
case <-b.ch:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (fc *FakeClock) newBlocker(n int) *blocker {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
// Fast path: we already have >= n waiters.
|
||||
if len(fc.waiters) >= n {
|
||||
return nil
|
||||
}
|
||||
// Set up a new blocker to wait for more waiters.
|
||||
b := &blocker{
|
||||
count: n,
|
||||
ch: make(chan struct{}),
|
||||
}
|
||||
fc.blockers = append(fc.blockers, b)
|
||||
fc.l.Unlock()
|
||||
<-b.ch
|
||||
return b
|
||||
}
|
||||
|
||||
// stop stops an expirer, returning true if the expirer was stopped.
|
||||
func (fc *FakeClock) stop(e expirer) bool {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
return fc.stopExpirer(e)
|
||||
}
|
||||
|
||||
// stopExpirer stops an expirer, returning true if the expirer was stopped.
|
||||
//
|
||||
// The caller must hold fc.l.
|
||||
func (fc *FakeClock) stopExpirer(e expirer) bool {
|
||||
idx := slices.Index(fc.waiters, e)
|
||||
if idx == -1 {
|
||||
return false
|
||||
}
|
||||
// Remove element, maintaining order, setting inaccessible elements to nil so
|
||||
// they can be garbage collected.
|
||||
copy(fc.waiters[idx:], fc.waiters[idx+1:])
|
||||
fc.waiters[len(fc.waiters)-1] = nil
|
||||
fc.waiters = fc.waiters[:len(fc.waiters)-1]
|
||||
return true
|
||||
}
|
||||
|
||||
// setExpirer sets an expirer to expire at a future point in time.
|
||||
//
|
||||
// The caller must hold fc.l.
|
||||
func (fc *FakeClock) setExpirer(e expirer, d time.Duration) {
|
||||
if d.Nanoseconds() <= 0 {
|
||||
// Special case for timers with duration <= 0: trigger immediately, never
|
||||
// reset.
|
||||
//
|
||||
// Tickers never get here, they panic if d is < 0.
|
||||
e.expire(fc.time)
|
||||
return
|
||||
}
|
||||
// Add the expirer to the set of waiters and notify any blockers.
|
||||
e.setExpiration(fc.time.Add(d))
|
||||
fc.waiters = append(fc.waiters, e)
|
||||
slices.SortFunc(fc.waiters, func(a, b expirer) int {
|
||||
return a.expiration().Compare(b.expiration())
|
||||
})
|
||||
|
||||
// Notify blockers of our new waiter.
|
||||
count := len(fc.waiters)
|
||||
fc.blockers = slices.DeleteFunc(fc.blockers, func(b *blocker) bool {
|
||||
if b.count <= count {
|
||||
close(b.ch)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
package clockwork
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// contextKey is private to this package so we can ensure uniqueness here. This
|
||||
// type identifies context values provided by this package.
|
||||
type contextKey string
|
||||
|
||||
// keyClock provides a clock for injecting during tests. If absent, a real clock
|
||||
// should be used.
|
||||
var keyClock = contextKey("clock") // clockwork.Clock
|
||||
|
||||
// AddToContext creates a derived context that references the specified clock.
|
||||
//
|
||||
// Be aware this doesn't change the behavior of standard library functions, such
|
||||
// as [context.WithTimeout] or [context.WithDeadline]. For this reason, users
|
||||
// should prefer passing explicit [clockwork.Clock] variables rather can passing
|
||||
// the clock via the context.
|
||||
func AddToContext(ctx context.Context, clock Clock) context.Context {
|
||||
return context.WithValue(ctx, keyClock, clock)
|
||||
}
|
||||
|
||||
// FromContext extracts a clock from the context. If not present, a real clock
|
||||
// is returned.
|
||||
func FromContext(ctx context.Context) Clock {
|
||||
if clock, ok := ctx.Value(keyClock).(Clock); ok {
|
||||
return clock
|
||||
}
|
||||
return NewRealClock()
|
||||
}
|
||||
|
||||
// ErrFakeClockDeadlineExceeded is the error returned by [context.Context] when
|
||||
// the deadline passes on a context which uses a [FakeClock].
|
||||
//
|
||||
// It wraps a [context.DeadlineExceeded] error, i.e.:
|
||||
//
|
||||
// // The following is true for any Context whose deadline has been exceeded,
|
||||
// // including contexts made with clockwork.WithDeadline or clockwork.WithTimeout.
|
||||
//
|
||||
// errors.Is(ctx.Err(), context.DeadlineExceeded)
|
||||
//
|
||||
// // The following can only be true for contexts made
|
||||
// // with clockwork.WithDeadline or clockwork.WithTimeout.
|
||||
//
|
||||
// errors.Is(ctx.Err(), clockwork.ErrFakeClockDeadlineExceeded)
|
||||
var ErrFakeClockDeadlineExceeded error = fmt.Errorf("clockwork.FakeClock: %w", context.DeadlineExceeded)
|
||||
|
||||
// WithDeadline returns a context with a deadline based on a [FakeClock].
|
||||
//
|
||||
// The returned context ignores parent cancelation if the parent was cancelled
|
||||
// with a [context.DeadlineExceeded] error. Any other error returned by the
|
||||
// parent is treated normally, cancelling the returned context.
|
||||
//
|
||||
// If the parent is cancelled with a [context.DeadlineExceeded] error, the only
|
||||
// way to then cancel the returned context is by calling the returned
|
||||
// context.CancelFunc.
|
||||
func WithDeadline(parent context.Context, clock Clock, t time.Time) (context.Context, context.CancelFunc) {
|
||||
if fc, ok := clock.(*FakeClock); ok {
|
||||
return newFakeClockContext(parent, t, fc.newTimerAtTime(t, nil).Chan())
|
||||
}
|
||||
return context.WithDeadline(parent, t)
|
||||
}
|
||||
|
||||
// WithTimeout returns a context with a timeout based on a [FakeClock].
|
||||
//
|
||||
// The returned context follows the same behaviors as [WithDeadline].
|
||||
func WithTimeout(parent context.Context, clock Clock, d time.Duration) (context.Context, context.CancelFunc) {
|
||||
if fc, ok := clock.(*FakeClock); ok {
|
||||
t, deadline := fc.newTimer(d, nil)
|
||||
return newFakeClockContext(parent, deadline, t.Chan())
|
||||
}
|
||||
return context.WithTimeout(parent, d)
|
||||
}
|
||||
|
||||
// fakeClockContext implements context.Context, using a fake clock for its
|
||||
// deadline.
|
||||
//
|
||||
// It ignores parent cancellation if the parent is cancelled with
|
||||
// context.DeadlineExceeded.
|
||||
type fakeClockContext struct {
|
||||
parent context.Context
|
||||
deadline time.Time // The user-facing deadline based on the fake clock's time.
|
||||
|
||||
// Tracks timeout/deadline cancellation.
|
||||
timerDone <-chan time.Time
|
||||
|
||||
// Tracks manual calls to the cancel function.
|
||||
cancel func() // Closes cancelCalled wrapped in a sync.Once.
|
||||
cancelCalled chan struct{}
|
||||
|
||||
// The user-facing data from the context.Context interface.
|
||||
ctxDone chan struct{} // Returned by Done().
|
||||
err error // nil until ctxDone is ready to be closed.
|
||||
}
|
||||
|
||||
func newFakeClockContext(parent context.Context, deadline time.Time, timer <-chan time.Time) (context.Context, context.CancelFunc) {
|
||||
cancelCalled := make(chan struct{})
|
||||
ctx := &fakeClockContext{
|
||||
parent: parent,
|
||||
deadline: deadline,
|
||||
timerDone: timer,
|
||||
cancelCalled: cancelCalled,
|
||||
ctxDone: make(chan struct{}),
|
||||
cancel: sync.OnceFunc(func() {
|
||||
close(cancelCalled)
|
||||
}),
|
||||
}
|
||||
ready := make(chan struct{}, 1)
|
||||
go ctx.runCancel(ready)
|
||||
<-ready // Wait until the cancellation goroutine is running.
|
||||
return ctx, ctx.cancel
|
||||
}
|
||||
|
||||
func (c *fakeClockContext) Deadline() (time.Time, bool) {
|
||||
return c.deadline, true
|
||||
}
|
||||
|
||||
func (c *fakeClockContext) Done() <-chan struct{} {
|
||||
return c.ctxDone
|
||||
}
|
||||
|
||||
func (c *fakeClockContext) Err() error {
|
||||
<-c.Done() // Don't return the error before it is ready.
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *fakeClockContext) Value(key any) any {
|
||||
return c.parent.Value(key)
|
||||
}
|
||||
|
||||
// runCancel runs the fakeClockContext's cancel goroutine and returns the
|
||||
// fakeClockContext's cancel function.
|
||||
//
|
||||
// fakeClockContext is then cancelled when any of the following occur:
|
||||
//
|
||||
// - The fakeClockContext.done channel is closed by its timer.
|
||||
// - The returned CancelFunc is executed.
|
||||
// - The fakeClockContext's parent context is cancelled with an error other
|
||||
// than context.DeadlineExceeded.
|
||||
func (c *fakeClockContext) runCancel(ready chan struct{}) {
|
||||
parentDone := c.parent.Done()
|
||||
|
||||
// Close ready when done, just in case the ready signal races with other
|
||||
// branches of our select statement below.
|
||||
defer close(ready)
|
||||
|
||||
for c.err == nil {
|
||||
select {
|
||||
case <-c.timerDone:
|
||||
c.err = ErrFakeClockDeadlineExceeded
|
||||
case <-c.cancelCalled:
|
||||
c.err = context.Canceled
|
||||
case <-parentDone:
|
||||
c.err = c.parent.Err()
|
||||
|
||||
case ready <- struct{}{}:
|
||||
// Signals the cancellation goroutine has begun, in an attempt to minimize
|
||||
// race conditions related to goroutine startup time.
|
||||
ready = nil // This case statement can only fire once.
|
||||
}
|
||||
}
|
||||
close(c.ctxDone)
|
||||
return
|
||||
}
|
||||
+52
-53
@@ -1,72 +1,71 @@
|
||||
package clockwork
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
import "time"
|
||||
|
||||
// Ticker provides an interface which can be used instead of directly
|
||||
// using the ticker within the time module. The real-time ticker t
|
||||
// provides ticks through t.C which becomes now t.Chan() to make
|
||||
// this channel requirement definable in this interface.
|
||||
// Ticker provides an interface which can be used instead of directly using
|
||||
// [time.Ticker]. The real-time ticker t provides ticks through t.C which
|
||||
// becomes t.Chan() to make this channel requirement definable in this
|
||||
// interface.
|
||||
type Ticker interface {
|
||||
Chan() <-chan time.Time
|
||||
Reset(d time.Duration)
|
||||
Stop()
|
||||
}
|
||||
|
||||
type realTicker struct{ *time.Ticker }
|
||||
|
||||
func (rt *realTicker) Chan() <-chan time.Time {
|
||||
return rt.C
|
||||
func (r realTicker) Chan() <-chan time.Time {
|
||||
return r.C
|
||||
}
|
||||
|
||||
type fakeTicker struct {
|
||||
c chan time.Time
|
||||
stop chan bool
|
||||
clock FakeClock
|
||||
period time.Duration
|
||||
// The channel associated with the firer, used to send expiration times.
|
||||
c chan time.Time
|
||||
|
||||
// The time when the ticker expires. Only meaningful if the ticker is currently
|
||||
// one of a FakeClock's waiters.
|
||||
exp time.Time
|
||||
|
||||
// reset and stop provide the implementation of the respective exported
|
||||
// functions.
|
||||
reset func(d time.Duration)
|
||||
stop func()
|
||||
|
||||
// The duration of the ticker.
|
||||
d time.Duration
|
||||
}
|
||||
|
||||
func (ft *fakeTicker) Chan() <-chan time.Time {
|
||||
return ft.c
|
||||
func newFakeTicker(fc *FakeClock, d time.Duration) *fakeTicker {
|
||||
var ft *fakeTicker
|
||||
ft = &fakeTicker{
|
||||
c: make(chan time.Time, 1),
|
||||
d: d,
|
||||
reset: func(d time.Duration) {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
ft.d = d
|
||||
fc.setExpirer(ft, d)
|
||||
},
|
||||
stop: func() { fc.stop(ft) },
|
||||
}
|
||||
return ft
|
||||
}
|
||||
|
||||
func (ft *fakeTicker) Stop() {
|
||||
ft.stop <- true
|
||||
func (f *fakeTicker) Chan() <-chan time.Time { return f.c }
|
||||
|
||||
func (f *fakeTicker) Reset(d time.Duration) { f.reset(d) }
|
||||
|
||||
func (f *fakeTicker) Stop() { f.stop() }
|
||||
|
||||
func (f *fakeTicker) expire(now time.Time) *time.Duration {
|
||||
// Never block on expiration.
|
||||
select {
|
||||
case f.c <- now:
|
||||
default:
|
||||
}
|
||||
return &f.d
|
||||
}
|
||||
|
||||
// runTickThread initializes a background goroutine to send the tick time to the ticker channel
|
||||
// after every period. Tick events are discarded if the underlying ticker channel does not have
|
||||
// enough capacity.
|
||||
func (ft *fakeTicker) runTickThread() {
|
||||
nextTick := ft.clock.Now().Add(ft.period)
|
||||
next := ft.clock.After(ft.period)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ft.stop:
|
||||
return
|
||||
case <-next:
|
||||
// We send the time that the tick was supposed to occur at.
|
||||
tick := nextTick
|
||||
// Before sending the tick, we'll compute the next tick time and star the clock.After call.
|
||||
now := ft.clock.Now()
|
||||
// First, figure out how many periods there have been between "now" and the time we were
|
||||
// supposed to have trigged, then advance over all of those.
|
||||
skipTicks := (now.Sub(tick) + ft.period - 1) / ft.period
|
||||
nextTick = nextTick.Add(skipTicks * ft.period)
|
||||
// Now, keep advancing until we are past now. This should happen at most once.
|
||||
for !nextTick.After(now) {
|
||||
nextTick = nextTick.Add(ft.period)
|
||||
}
|
||||
// Figure out how long between now and the next scheduled tick, then wait that long.
|
||||
remaining := nextTick.Sub(now)
|
||||
next = ft.clock.After(remaining)
|
||||
// Finally, we can actually send the tick.
|
||||
select {
|
||||
case ft.c <- tick:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
func (f *fakeTicker) expiration() time.Time { return f.exp }
|
||||
|
||||
func (f *fakeTicker) setExpiration(t time.Time) { f.exp = t }
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package clockwork
|
||||
|
||||
import "time"
|
||||
|
||||
// Timer provides an interface which can be used instead of directly using
|
||||
// [time.Timer]. The real-time timer t provides events through t.C which becomes
|
||||
// t.Chan() to make this channel requirement definable in this interface.
|
||||
type Timer interface {
|
||||
Chan() <-chan time.Time
|
||||
Reset(d time.Duration) bool
|
||||
Stop() bool
|
||||
}
|
||||
|
||||
type realTimer struct{ *time.Timer }
|
||||
|
||||
func (r realTimer) Chan() <-chan time.Time {
|
||||
return r.C
|
||||
}
|
||||
|
||||
type fakeTimer struct {
|
||||
// The channel associated with the firer, used to send expiration times.
|
||||
c chan time.Time
|
||||
|
||||
// The time when the firer expires. Only meaningful if the firer is currently
|
||||
// one of a FakeClock's waiters.
|
||||
exp time.Time
|
||||
|
||||
// reset and stop provide the implementation of the respective exported
|
||||
// functions.
|
||||
reset func(d time.Duration) bool
|
||||
stop func() bool
|
||||
|
||||
// If present when the timer fires, the timer calls afterFunc in its own
|
||||
// goroutine rather than sending the time on Chan().
|
||||
afterFunc func()
|
||||
}
|
||||
|
||||
func newFakeTimer(fc *FakeClock, afterfunc func()) *fakeTimer {
|
||||
var ft *fakeTimer
|
||||
ft = &fakeTimer{
|
||||
c: make(chan time.Time, 1),
|
||||
reset: func(d time.Duration) bool {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
// fc.l must be held across the calls to stopExpirer & setExpirer.
|
||||
stopped := fc.stopExpirer(ft)
|
||||
fc.setExpirer(ft, d)
|
||||
return stopped
|
||||
},
|
||||
stop: func() bool { return fc.stop(ft) },
|
||||
|
||||
afterFunc: afterfunc,
|
||||
}
|
||||
return ft
|
||||
}
|
||||
|
||||
func (f *fakeTimer) Chan() <-chan time.Time { return f.c }
|
||||
|
||||
func (f *fakeTimer) Reset(d time.Duration) bool { return f.reset(d) }
|
||||
|
||||
func (f *fakeTimer) Stop() bool { return f.stop() }
|
||||
|
||||
func (f *fakeTimer) expire(now time.Time) *time.Duration {
|
||||
if f.afterFunc != nil {
|
||||
go f.afterFunc()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Never block on expiration.
|
||||
select {
|
||||
case f.c <- now:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeTimer) expiration() time.Time { return f.exp }
|
||||
|
||||
func (f *fakeTimer) setExpiration(t time.Time) { f.exp = t }
|
||||
+1
-1
@@ -65,7 +65,7 @@
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"terser": "^5.30.4",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.2",
|
||||
"vite": "^4.5.13",
|
||||
"vite-plugin-checker": "^0.6.2",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vitest": "^0.34.6"
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
arch:
|
||||
- amd64
|
||||
- ppc64le
|
||||
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.14.x"
|
||||
- "1.15.x"
|
||||
- "1.17.x"
|
||||
- master
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Security vulnerabilities can be reported using GitHub's [private vulnerability reporting tool](https://github.com/russellhaering/goxmldsig/security/advisories/new).
|
||||
+140
-41
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
@@ -169,6 +170,7 @@ func (ctx *ValidationContext) transform(
|
||||
return el, canonicalizer, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (ctx *ValidationContext) digest(el *etree.Element, digestAlgorithmId string, canonicalizer Canonicalizer) ([]byte, error) {
|
||||
data, err := canonicalizer.Canonicalize(el)
|
||||
if err != nil {
|
||||
@@ -189,6 +191,33 @@ func (ctx *ValidationContext) digest(el *etree.Element, digestAlgorithmId string
|
||||
return hash.Sum(nil), nil
|
||||
}
|
||||
|
||||
func (ctx *ValidationContext) getCanonicalSignedInfo(sig *types.Signature) ([]byte, error) {
|
||||
signatureElement := sig.UnderlyingElement()
|
||||
|
||||
nsCtx, err := etreeutils.NSBuildParentContext(signatureElement)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedInfo, err := etreeutils.NSFindOneChildCtx(nsCtx, signatureElement, Namespace, SignedInfoTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if signedInfo == nil {
|
||||
return nil, errors.New("Missing SignedInfo")
|
||||
}
|
||||
|
||||
// Canonicalize the xml
|
||||
canonical, err := canonicalSerialize(signedInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return canonical, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (ctx *ValidationContext) verifySignedInfo(sig *types.Signature, canonicalizer Canonicalizer, signatureMethodId string, cert *x509.Certificate, decodedSignature []byte) error {
|
||||
signatureElement := sig.UnderlyingElement()
|
||||
|
||||
@@ -226,44 +255,24 @@ func (ctx *ValidationContext) verifySignedInfo(sig *types.Signature, canonicaliz
|
||||
}
|
||||
|
||||
func (ctx *ValidationContext) validateSignature(el *etree.Element, sig *types.Signature, cert *x509.Certificate) (*etree.Element, error) {
|
||||
idAttrEl := el.SelectAttr(ctx.IdAttribute)
|
||||
idAttr := ""
|
||||
if idAttrEl != nil {
|
||||
idAttr = idAttrEl.Value
|
||||
}
|
||||
|
||||
var ref *types.Reference
|
||||
// Actually verify the 'SignedInfo' was signed by a trusted source
|
||||
signatureMethod := sig.SignedInfo.SignatureMethod.Algorithm
|
||||
|
||||
// Find the first reference which references the top-level element
|
||||
for _, _ref := range sig.SignedInfo.References {
|
||||
if _ref.URI == "" || _ref.URI[1:] == idAttr {
|
||||
ref = &_ref
|
||||
}
|
||||
}
|
||||
|
||||
// Perform all transformations listed in the 'SignedInfo'
|
||||
// Basically, this means removing the 'SignedInfo'
|
||||
transformed, canonicalizer, err := ctx.transform(el, sig, ref)
|
||||
canonicalSignedInfoBytes, err := ctx.getCanonicalSignedInfo(sig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.New("Could not obtain canonical signed info bytes")
|
||||
}
|
||||
|
||||
digestAlgorithm := ref.DigestAlgo.Algorithm
|
||||
|
||||
// Digest the transformed XML and compare it to the 'DigestValue' from the 'SignedInfo'
|
||||
digest, err := ctx.digest(transformed, digestAlgorithm, canonicalizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if canonicalSignedInfoBytes == nil {
|
||||
return nil, errors.New("Missing SignedInfo")
|
||||
}
|
||||
|
||||
decodedDigestValue, err := base64.StdEncoding.DecodeString(ref.DigestValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
algo, ok := x509SignatureAlgorithmByIdentifier[signatureMethod]
|
||||
if !ok {
|
||||
return nil, errors.New("Unknown signature method: " + signatureMethod)
|
||||
}
|
||||
|
||||
if !bytes.Equal(digest, decodedDigestValue) {
|
||||
return nil, errors.New("Signature could not be verified")
|
||||
}
|
||||
if sig.SignatureValue == nil {
|
||||
return nil, errors.New("Signature could not be verified")
|
||||
}
|
||||
@@ -274,14 +283,92 @@ func (ctx *ValidationContext) validateSignature(el *etree.Element, sig *types.Si
|
||||
return nil, errors.New("Could not decode signature")
|
||||
}
|
||||
|
||||
// Actually verify the 'SignedInfo' was signed by a trusted source
|
||||
signatureMethod := sig.SignedInfo.SignatureMethod.Algorithm
|
||||
err = ctx.verifySignedInfo(sig, canonicalizer, signatureMethod, cert, decodedSignature)
|
||||
err = cert.CheckSignature(algo, canonicalSignedInfoBytes, decodedSignature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transformed, nil
|
||||
// only use the verified canonicalSignedInfoBytes
|
||||
// unmarshal canonicalSignedInfoBytes into a new SignedInfo type
|
||||
// to obtain the reference
|
||||
signedInfo := &types.SignedInfo{}
|
||||
err = xml.Unmarshal(canonicalSignedInfoBytes, signedInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idAttrEl := el.SelectAttr(ctx.IdAttribute)
|
||||
idAttr := ""
|
||||
if idAttrEl != nil {
|
||||
idAttr = idAttrEl.Value
|
||||
}
|
||||
|
||||
var ref *types.Reference
|
||||
|
||||
// Find the first reference which references the top-level element
|
||||
for _, _ref := range signedInfo.References {
|
||||
if _ref.URI == "" || _ref.URI[1:] == idAttr {
|
||||
ref = &_ref
|
||||
}
|
||||
}
|
||||
|
||||
// prevents null pointer deref
|
||||
if ref == nil {
|
||||
return nil, errors.New("Missing reference")
|
||||
}
|
||||
|
||||
digestAlgorithmId := ref.DigestAlgo.Algorithm
|
||||
signedDigestValue, err := base64.StdEncoding.DecodeString(ref.DigestValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Perform all transformations listed in the 'SignedInfo'
|
||||
// Basically, this means removing the 'SignedInfo'
|
||||
transformed, canonicalizer, err := ctx.transform(el, sig, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
referencedBytes, err := canonicalizer.Canonicalize(transformed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use a known digest hashing algorithm
|
||||
hashAlgorithm, ok := digestAlgorithmsByIdentifier[digestAlgorithmId]
|
||||
if !ok {
|
||||
return nil, errors.New("Unknown digest algorithm: " + digestAlgorithmId)
|
||||
}
|
||||
|
||||
hash := hashAlgorithm.New()
|
||||
_, err = hash.Write(referencedBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
computedDigest := hash.Sum(nil)
|
||||
/* Digest the transformed XML and compare it to the 'DigestValue' from the 'SignedInfo'
|
||||
digest, err := ctx.digest(transformed, digestAlgorithm, canonicalizer)
|
||||
*/
|
||||
|
||||
if !bytes.Equal(computedDigest, signedDigestValue) {
|
||||
return nil, errors.New("Signature could not be verified")
|
||||
}
|
||||
|
||||
if !(len(computedDigest) >= 20) {
|
||||
return nil, errors.New("Computed digest is less than 20 something went wrong")
|
||||
}
|
||||
|
||||
// now only the referencedBytes is verified,
|
||||
// unmarshal into new etree
|
||||
doc := etree.NewDocument()
|
||||
err = doc.ReadFromBytes(referencedBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return doc.Root(), nil
|
||||
}
|
||||
|
||||
func contains(roots []*x509.Certificate, cert *x509.Certificate) bool {
|
||||
@@ -425,7 +512,7 @@ func (ctx *ValidationContext) verifyCertificate(sig *types.Signature) (*x509.Cer
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cert *x509.Certificate
|
||||
var untrustedCert *x509.Certificate
|
||||
|
||||
if sig.KeyInfo != nil {
|
||||
// If the Signature includes KeyInfo, extract the certificate from there
|
||||
@@ -439,29 +526,40 @@ func (ctx *ValidationContext) verifyCertificate(sig *types.Signature) (*x509.Cer
|
||||
return nil, errors.New("Failed to parse certificate")
|
||||
}
|
||||
|
||||
cert, err = x509.ParseCertificate(certData)
|
||||
untrustedCert, err = x509.ParseCertificate(certData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// If the Signature doesn't have KeyInfo, Use the root certificate if there is only one
|
||||
if len(roots) == 1 {
|
||||
cert = roots[0]
|
||||
untrustedCert = roots[0]
|
||||
} else {
|
||||
return nil, errors.New("Missing x509 Element")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the certificate is one we trust
|
||||
if !contains(roots, cert) {
|
||||
return nil, errors.New("Could not verify certificate against trusted certs")
|
||||
rootIdx := -1
|
||||
for i, root := range roots {
|
||||
if root.Equal(untrustedCert) {
|
||||
rootIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
|
||||
if rootIdx == -1 {
|
||||
return nil, errors.New("Could not verify certificate against trusted certs")
|
||||
}
|
||||
var trustedCert *x509.Certificate
|
||||
|
||||
trustedCert = roots[rootIdx]
|
||||
|
||||
// Verify that the certificate is one we trust
|
||||
|
||||
if now.Before(trustedCert.NotBefore) || now.After(trustedCert.NotAfter) {
|
||||
return nil, errors.New("Cert is not valid at this time")
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
return trustedCert, nil
|
||||
}
|
||||
|
||||
// Validate verifies that the passed element contains a valid enveloped signature
|
||||
@@ -475,6 +573,7 @@ func (ctx *ValidationContext) Validate(el *etree.Element) (*etree.Element, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// function to get the trusted certificate
|
||||
cert, err := ctx.verifyCertificate(sig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user