| // Copyright 2014 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package webdav |
| |
| import ( |
| "container/heap" |
| "errors" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| ) |
| |
| var ( |
| // ErrConfirmationFailed is returned by a LockSystem's Confirm method. |
| ErrConfirmationFailed = errors.New("webdav: confirmation failed") |
| // ErrForbidden is returned by a LockSystem's Unlock method. |
| ErrForbidden = errors.New("webdav: forbidden") |
| // ErrLocked is returned by a LockSystem's Create, Refresh and Unlock methods. |
| ErrLocked = errors.New("webdav: locked") |
| // ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods. |
| ErrNoSuchLock = errors.New("webdav: no such lock") |
| ) |
| |
| // Condition can match a WebDAV resource, based on a token or ETag. |
| // Exactly one of Token and ETag should be non-empty. |
| type Condition struct { |
| Not bool |
| Token string |
| ETag string |
| } |
| |
| // LockSystem manages access to a collection of named resources. The elements |
| // in a lock name are separated by slash ('/', U+002F) characters, regardless |
| // of host operating system convention. |
| type LockSystem interface { |
| // Confirm confirms that the caller can claim all of the locks specified by |
| // the given conditions, and that holding the union of all of those locks |
| // gives exclusive access to all of the named resources. Up to two resources |
| // can be named. Empty names are ignored. |
| // |
| // Exactly one of release and err will be non-nil. If release is non-nil, |
| // all of the requested locks are held until release is called. Calling |
| // release does not unlock the lock, in the WebDAV UNLOCK sense, but once |
| // Confirm has confirmed that a lock claim is valid, that lock cannot be |
| // Confirmed again until it has been released. |
| // |
| // If Confirm returns ErrConfirmationFailed then the Handler will continue |
| // to try any other set of locks presented (a WebDAV HTTP request can |
| // present more than one set of locks). If it returns any other non-nil |
| // error, the Handler will write a "500 Internal Server Error" HTTP status. |
| Confirm(now time.Time, name0, name1 string, conditions ...Condition) (release func(), err error) |
| |
| // Create creates a lock with the given depth, duration, owner and root |
| // (name). The depth will either be negative (meaning infinite) or zero. |
| // |
| // If Create returns ErrLocked then the Handler will write a "423 Locked" |
| // HTTP status. If it returns any other non-nil error, the Handler will |
| // write a "500 Internal Server Error" HTTP status. |
| // |
| // See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for |
| // when to use each error. |
| // |
| // The token returned identifies the created lock. It should be an absolute |
| // URI as defined by RFC 3986, Section 4.3. In particular, it should not |
| // contain whitespace. |
| Create(now time.Time, details LockDetails) (token string, err error) |
| |
| // Refresh refreshes the lock with the given token. |
| // |
| // If Refresh returns ErrLocked then the Handler will write a "423 Locked" |
| // HTTP Status. If Refresh returns ErrNoSuchLock then the Handler will write |
| // a "412 Precondition Failed" HTTP Status. If it returns any other non-nil |
| // error, the Handler will write a "500 Internal Server Error" HTTP status. |
| // |
| // See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for |
| // when to use each error. |
| Refresh(now time.Time, token string, duration time.Duration) (LockDetails, error) |
| |
| // Unlock unlocks the lock with the given token. |
| // |
| // If Unlock returns ErrForbidden then the Handler will write a "403 |
| // Forbidden" HTTP Status. If Unlock returns ErrLocked then the Handler |
| // will write a "423 Locked" HTTP status. If Unlock returns ErrNoSuchLock |
| // then the Handler will write a "409 Conflict" HTTP Status. If it returns |
| // any other non-nil error, the Handler will write a "500 Internal Server |
| // Error" HTTP status. |
| // |
| // See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.11.1 for |
| // when to use each error. |
| Unlock(now time.Time, token string) error |
| } |
| |
| // LockDetails are a lock's metadata. |
| type LockDetails struct { |
| // Root is the root resource name being locked. For a zero-depth lock, the |
| // root is the only resource being locked. |
| Root string |
| // Duration is the lock timeout. A negative duration means infinite. |
| Duration time.Duration |
| // OwnerXML is the verbatim <owner> XML given in a LOCK HTTP request. |
| // |
| // TODO: does the "verbatim" nature play well with XML namespaces? |
| // Does the OwnerXML field need to have more structure? See |
| // https://codereview.appspot.com/175140043/#msg2 |
| OwnerXML string |
| // ZeroDepth is whether the lock has zero depth. If it does not have zero |
| // depth, it has infinite depth. |
| ZeroDepth bool |
| } |
| |
| // NewMemLS returns a new in-memory LockSystem. |
| func NewMemLS() LockSystem { |
| return &memLS{ |
| byName: make(map[string]*memLSNode), |
| byToken: make(map[string]*memLSNode), |
| gen: uint64(time.Now().Unix()), |
| } |
| } |
| |
| type memLS struct { |
| mu sync.Mutex |
| byName map[string]*memLSNode |
| byToken map[string]*memLSNode |
| gen uint64 |
| // byExpiry only contains those nodes whose LockDetails have a finite |
| // Duration and are yet to expire. |
| byExpiry byExpiry |
| } |
| |
| func (m *memLS) nextToken() string { |
| m.gen++ |
| return strconv.FormatUint(m.gen, 10) |
| } |
| |
| func (m *memLS) collectExpiredNodes(now time.Time) { |
| for len(m.byExpiry) > 0 { |
| if now.Before(m.byExpiry[0].expiry) { |
| break |
| } |
| m.remove(m.byExpiry[0]) |
| } |
| } |
| |
| func (m *memLS) Confirm(now time.Time, name0, name1 string, conditions ...Condition) (func(), error) { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| m.collectExpiredNodes(now) |
| |
| var n0, n1 *memLSNode |
| if name0 != "" { |
| if n0 = m.lookup(slashClean(name0), conditions...); n0 == nil { |
| return nil, ErrConfirmationFailed |
| } |
| } |
| if name1 != "" { |
| if n1 = m.lookup(slashClean(name1), conditions...); n1 == nil { |
| return nil, ErrConfirmationFailed |
| } |
| } |
| |
| // Don't hold the same node twice. |
| if n1 == n0 { |
| n1 = nil |
| } |
| |
| if n0 != nil { |
| m.hold(n0) |
| } |
| if n1 != nil { |
| m.hold(n1) |
| } |
| return func() { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| if n1 != nil { |
| m.unhold(n1) |
| } |
| if n0 != nil { |
| m.unhold(n0) |
| } |
| }, nil |
| } |
| |
| // lookup returns the node n that locks the named resource, provided that n |
| // matches at least one of the given conditions and that lock isn't held by |
| // another party. Otherwise, it returns nil. |
| // |
| // n may be a parent of the named resource, if n is an infinite depth lock. |
| func (m *memLS) lookup(name string, conditions ...Condition) (n *memLSNode) { |
| // TODO: support Condition.Not and Condition.ETag. |
| for _, c := range conditions { |
| n = m.byToken[c.Token] |
| if n == nil || n.held { |
| continue |
| } |
| if name == n.details.Root { |
| return n |
| } |
| if n.details.ZeroDepth { |
| continue |
| } |
| if n.details.Root == "/" || strings.HasPrefix(name, n.details.Root+"/") { |
| return n |
| } |
| } |
| return nil |
| } |
| |
| func (m *memLS) hold(n *memLSNode) { |
| if n.held { |
| panic("webdav: memLS inconsistent held state") |
| } |
| n.held = true |
| if n.details.Duration >= 0 && n.byExpiryIndex >= 0 { |
| heap.Remove(&m.byExpiry, n.byExpiryIndex) |
| } |
| } |
| |
| func (m *memLS) unhold(n *memLSNode) { |
| if !n.held { |
| panic("webdav: memLS inconsistent held state") |
| } |
| n.held = false |
| if n.details.Duration >= 0 { |
| heap.Push(&m.byExpiry, n) |
| } |
| } |
| |
| func (m *memLS) Create(now time.Time, details LockDetails) (string, error) { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| m.collectExpiredNodes(now) |
| details.Root = slashClean(details.Root) |
| |
| if !m.canCreate(details.Root, details.ZeroDepth) { |
| return "", ErrLocked |
| } |
| n := m.create(details.Root) |
| n.token = m.nextToken() |
| m.byToken[n.token] = n |
| n.details = details |
| if n.details.Duration >= 0 { |
| n.expiry = now.Add(n.details.Duration) |
| heap.Push(&m.byExpiry, n) |
| } |
| return n.token, nil |
| } |
| |
| func (m *memLS) Refresh(now time.Time, token string, duration time.Duration) (LockDetails, error) { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| m.collectExpiredNodes(now) |
| |
| n := m.byToken[token] |
| if n == nil { |
| return LockDetails{}, ErrNoSuchLock |
| } |
| if n.held { |
| return LockDetails{}, ErrLocked |
| } |
| if n.byExpiryIndex >= 0 { |
| heap.Remove(&m.byExpiry, n.byExpiryIndex) |
| } |
| n.details.Duration = duration |
| if n.details.Duration >= 0 { |
| n.expiry = now.Add(n.details.Duration) |
| heap.Push(&m.byExpiry, n) |
| } |
| return n.details, nil |
| } |
| |
| func (m *memLS) Unlock(now time.Time, token string) error { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| m.collectExpiredNodes(now) |
| |
| n := m.byToken[token] |
| if n == nil { |
| return ErrNoSuchLock |
| } |
| if n.held { |
| return ErrLocked |
| } |
| m.remove(n) |
| return nil |
| } |
| |
| func (m *memLS) canCreate(name string, zeroDepth bool) bool { |
| return walkToRoot(name, func(name0 string, first bool) bool { |
| n := m.byName[name0] |
| if n == nil { |
| return true |
| } |
| if first { |
| if n.token != "" { |
| // The target node is already locked. |
| return false |
| } |
| if !zeroDepth { |
| // The requested lock depth is infinite, and the fact that n exists |
| // (n != nil) means that a descendent of the target node is locked. |
| return false |
| } |
| } else if n.token != "" && !n.details.ZeroDepth { |
| // An ancestor of the target node is locked with infinite depth. |
| return false |
| } |
| return true |
| }) |
| } |
| |
| func (m *memLS) create(name string) (ret *memLSNode) { |
| walkToRoot(name, func(name0 string, first bool) bool { |
| n := m.byName[name0] |
| if n == nil { |
| n = &memLSNode{ |
| details: LockDetails{ |
| Root: name0, |
| }, |
| byExpiryIndex: -1, |
| } |
| m.byName[name0] = n |
| } |
| n.refCount++ |
| if first { |
| ret = n |
| } |
| return true |
| }) |
| return ret |
| } |
| |
| func (m *memLS) remove(n *memLSNode) { |
| delete(m.byToken, n.token) |
| n.token = "" |
| walkToRoot(n.details.Root, func(name0 string, first bool) bool { |
| x := m.byName[name0] |
| x.refCount-- |
| if x.refCount == 0 { |
| delete(m.byName, name0) |
| } |
| return true |
| }) |
| if n.byExpiryIndex >= 0 { |
| heap.Remove(&m.byExpiry, n.byExpiryIndex) |
| } |
| } |
| |
| func walkToRoot(name string, f func(name0 string, first bool) bool) bool { |
| for first := true; ; first = false { |
| if !f(name, first) { |
| return false |
| } |
| if name == "/" { |
| break |
| } |
| name = name[:strings.LastIndex(name, "/")] |
| if name == "" { |
| name = "/" |
| } |
| } |
| return true |
| } |
| |
| type memLSNode struct { |
| // details are the lock metadata. Even if this node's name is not explicitly locked, |
| // details.Root will still equal the node's name. |
| details LockDetails |
| // token is the unique identifier for this node's lock. An empty token means that |
| // this node is not explicitly locked. |
| token string |
| // refCount is the number of self-or-descendent nodes that are explicitly locked. |
| refCount int |
| // expiry is when this node's lock expires. |
| expiry time.Time |
| // byExpiryIndex is the index of this node in memLS.byExpiry. It is -1 |
| // if this node does not expire, or has expired. |
| byExpiryIndex int |
| // held is whether this node's lock is actively held by a Confirm call. |
| held bool |
| } |
| |
| type byExpiry []*memLSNode |
| |
| func (b *byExpiry) Len() int { |
| return len(*b) |
| } |
| |
| func (b *byExpiry) Less(i, j int) bool { |
| return (*b)[i].expiry.Before((*b)[j].expiry) |
| } |
| |
| func (b *byExpiry) Swap(i, j int) { |
| (*b)[i], (*b)[j] = (*b)[j], (*b)[i] |
| (*b)[i].byExpiryIndex = i |
| (*b)[j].byExpiryIndex = j |
| } |
| |
| func (b *byExpiry) Push(x interface{}) { |
| n := x.(*memLSNode) |
| n.byExpiryIndex = len(*b) |
| *b = append(*b, n) |
| } |
| |
| func (b *byExpiry) Pop() interface{} { |
| i := len(*b) - 1 |
| n := (*b)[i] |
| (*b)[i] = nil |
| n.byExpiryIndex = -1 |
| *b = (*b)[:i] |
| return n |
| } |
| |
| const infiniteTimeout = -1 |
| |
| // parseTimeout parses the Timeout HTTP header, as per section 10.7. If s is |
| // empty, an infiniteTimeout is returned. |
| func parseTimeout(s string) (time.Duration, error) { |
| if s == "" { |
| return infiniteTimeout, nil |
| } |
| if i := strings.IndexByte(s, ','); i >= 0 { |
| s = s[:i] |
| } |
| s = strings.TrimSpace(s) |
| if s == "Infinite" { |
| return infiniteTimeout, nil |
| } |
| const pre = "Second-" |
| if !strings.HasPrefix(s, pre) { |
| return 0, errInvalidTimeout |
| } |
| s = s[len(pre):] |
| if s == "" || s[0] < '0' || '9' < s[0] { |
| return 0, errInvalidTimeout |
| } |
| n, err := strconv.ParseInt(s, 10, 64) |
| if err != nil || 1<<32-1 < n { |
| return 0, errInvalidTimeout |
| } |
| return time.Duration(n) * time.Second, nil |
| } |