github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/locks.go (about) 1 // Copyright 2018-2021 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package ocdav 20 21 import ( 22 "context" 23 "encoding/xml" 24 "errors" 25 "fmt" 26 "io" 27 "net/http" 28 "path" 29 "strconv" 30 "strings" 31 "time" 32 33 gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" 34 userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 35 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 36 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 37 types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" 38 ocdavErrors "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors" 39 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" 40 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/prop" 41 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/spacelookup" 42 "github.com/cs3org/reva/v2/pkg/appctx" 43 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 44 "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" 45 "github.com/cs3org/reva/v2/pkg/utils" 46 "github.com/google/uuid" 47 "go.opentelemetry.io/otel/attribute" 48 ) 49 50 // Most of this is taken from https://github.com/golang/net/blob/master/webdav/lock.go 51 52 // From RFC4918 http://www.webdav.org/specs/rfc4918.html#lock-tokens 53 // This specification encourages servers to create Universally Unique Identifiers (UUIDs) for lock tokens, 54 // and to use the URI form defined by "A Universally Unique Identifier (UUID) URN Namespace" ([RFC4122]). 55 // However, servers are free to use any URI (e.g., from another scheme) so long as it meets the uniqueness 56 // requirements. For example, a valid lock token might be constructed using the "opaquelocktoken" scheme 57 // defined in Appendix C. 58 // 59 // Example: "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6" 60 // 61 // we stick to the recommendation and use the URN Namespace 62 const lockTokenPrefix = "urn:uuid:" 63 64 // TODO(jfd) implement lock 65 // see Web Distributed Authoring and Versioning (WebDAV) Locking Protocol: 66 // https://www.greenbytes.de/tech/webdav/draft-reschke-webdav-locking-latest.html 67 // Webdav supports a Depth: infinity lock, wopi only needs locks on files 68 69 // https://www.greenbytes.de/tech/webdav/draft-reschke-webdav-locking-latest.html#write.locks.and.the.if.request.header 70 // [...] a lock token MUST be submitted in the If header for all locked resources 71 // that a method may interact with or the method MUST fail. [...] 72 /* 73 COPY /~fielding/index.html HTTP/1.1 74 Host: example.com 75 Destination: http://example.com/users/f/fielding/index.html 76 If: <http://example.com/users/f/fielding/index.html> 77 (<opaquelocktoken:f81d4fae-7dec-11d0-a765-00a0c91e6bf6>) 78 */ 79 80 // http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo 81 type lockInfo struct { 82 XMLName xml.Name `xml:"lockinfo"` 83 Exclusive *struct{} `xml:"lockscope>exclusive"` 84 Shared *struct{} `xml:"lockscope>shared"` 85 Write *struct{} `xml:"locktype>write"` 86 Owner owner `xml:"owner"` 87 LockID string `xml:"locktoken>href"` 88 } 89 90 // http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner 91 type owner struct { 92 InnerXML string `xml:",innerxml"` 93 } 94 95 // Condition can match a WebDAV resource, based on a token or ETag. 96 // Exactly one of Token and ETag should be non-empty. 97 type Condition struct { 98 Not bool 99 Token string 100 ETag string 101 } 102 103 // LockSystem manages access to a collection of named resources. The elements 104 // in a lock name are separated by slash ('/', U+002F) characters, regardless 105 // of host operating system convention. 106 type LockSystem interface { 107 // Confirm confirms that the caller can claim all of the locks specified by 108 // the given conditions, and that holding the union of all of those locks 109 // gives exclusive access to all of the named resources. Up to two resources 110 // can be named. Empty names are ignored. 111 // 112 // Exactly one of release and err will be non-nil. If release is non-nil, 113 // all of the requested locks are held until release is called. Calling 114 // release does not unlock the lock, in the WebDAV UNLOCK sense, but once 115 // Confirm has confirmed that a lock claim is valid, that lock cannot be 116 // Confirmed again until it has been released. 117 // 118 // If Confirm returns ErrConfirmationFailed then the Handler will continue 119 // to try any other set of locks presented (a WebDAV HTTP request can 120 // present more than one set of locks). If it returns any other non-nil 121 // error, the Handler will write a "500 Internal Server Error" HTTP status. 122 Confirm(ctx context.Context, now time.Time, name0, name1 string, conditions ...Condition) (release func(), err error) 123 124 // Create creates a lock with the given depth, duration, owner and root 125 // (name). The depth will either be negative (meaning infinite) or zero. 126 // 127 // If Create returns ErrLocked then the Handler will write a "423 Locked" 128 // HTTP status. If it returns any other non-nil error, the Handler will 129 // write a "500 Internal Server Error" HTTP status. 130 // 131 // See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for 132 // when to use each error. 133 // 134 // The token returned identifies the created lock. It should be an absolute 135 // URI as defined by RFC 3986, Section 4.3. In particular, it should not 136 // contain whitespace. 137 Create(ctx context.Context, now time.Time, details LockDetails) (token string, err error) 138 139 // Refresh refreshes the lock with the given token. 140 // 141 // If Refresh returns ErrLocked then the Handler will write a "423 Locked" 142 // HTTP Status. If Refresh returns ErrNoSuchLock then the Handler will write 143 // a "412 Precondition Failed" HTTP Status. If it returns any other non-nil 144 // error, the Handler will write a "500 Internal Server Error" HTTP status. 145 // 146 // See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for 147 // when to use each error. 148 Refresh(ctx context.Context, now time.Time, ref *provider.Reference, token string) error 149 150 // Unlock unlocks the lock with the given token. 151 // 152 // If Unlock returns ErrForbidden then the Handler will write a "403 153 // Forbidden" HTTP Status. If Unlock returns ErrLocked then the Handler 154 // will write a "423 Locked" HTTP status. If Unlock returns ErrNoSuchLock 155 // then the Handler will write a "409 Conflict" HTTP Status. If it returns 156 // any other non-nil error, the Handler will write a "500 Internal Server 157 // Error" HTTP status. 158 // 159 // See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.11.1 for 160 // when to use each error. 161 Unlock(ctx context.Context, now time.Time, ref *provider.Reference, token string) error 162 } 163 164 // NewCS3LS returns a new CS3 based LockSystem. 165 func NewCS3LS(s pool.Selectable[gateway.GatewayAPIClient]) LockSystem { 166 return &cs3LS{ 167 selector: s, 168 } 169 } 170 171 type cs3LS struct { 172 selector pool.Selectable[gateway.GatewayAPIClient] 173 } 174 175 func (cls *cs3LS) Confirm(ctx context.Context, now time.Time, name0, name1 string, conditions ...Condition) (func(), error) { 176 return nil, ocdavErrors.ErrNotImplemented 177 } 178 179 func (cls *cs3LS) Create(ctx context.Context, now time.Time, details LockDetails) (string, error) { 180 // always assume depth infinity? 181 /* 182 if !details.ZeroDepth { 183 The CS3 Lock api currently has no depth property, it only locks single resources 184 return "", ocdavErrors.ErrUnsupportedLockInfo 185 } 186 */ 187 188 u := ctxpkg.ContextMustGetUser(ctx) 189 190 // add metadata via opaque 191 // TODO: upate cs3api: https://github.com/cs3org/cs3apis/issues/213 192 o := utils.AppendPlainToOpaque(nil, "lockownername", u.GetDisplayName()) 193 o = utils.AppendPlainToOpaque(o, "locktime", now.Format(time.RFC3339)) 194 195 lockid := details.LockID 196 if lockid == "" { 197 // Having a lock token provides no special access rights. Anyone can find out anyone 198 // else's lock token by performing lock discovery. Locks must be enforced based upon 199 // whatever authentication mechanism is used by the server, not based on the secrecy 200 // of the token values. 201 // see: http://www.webdav.org/specs/rfc2518.html#n-lock-tokens 202 token := uuid.New() 203 204 lockid = lockTokenPrefix + token.String() 205 } 206 r := &provider.SetLockRequest{ 207 Ref: details.Root, 208 Lock: &provider.Lock{ 209 Opaque: o, 210 Type: provider.LockType_LOCK_TYPE_EXCL, 211 User: details.UserID, // no way to set an app lock? TODO maybe via the ownerxml 212 //AppName: , // TODO use a urn scheme? 213 LockId: lockid, 214 }, 215 } 216 if details.Duration > 0 { 217 expiration := time.Now().UTC().Add(details.Duration) 218 r.Lock.Expiration = &types.Timestamp{ 219 Seconds: uint64(expiration.Unix()), 220 Nanos: uint32(expiration.Nanosecond()), 221 } 222 } 223 224 client, err := cls.selector.Next() 225 if err != nil { 226 return "", err 227 } 228 229 res, err := client.SetLock(ctx, r) 230 if err != nil { 231 return "", err 232 } 233 switch res.GetStatus().GetCode() { 234 case rpc.Code_CODE_OK: 235 return lockid, nil 236 default: 237 return "", ocdavErrors.NewErrFromStatus(res.GetStatus()) 238 } 239 240 } 241 242 func (cls *cs3LS) Refresh(ctx context.Context, now time.Time, ref *provider.Reference, token string) error { 243 u := ctxpkg.ContextMustGetUser(ctx) 244 245 // add metadata via opaque 246 // TODO: upate cs3api: https://github.com/cs3org/cs3apis/issues/213 247 o := utils.AppendPlainToOpaque(nil, "lockownername", u.GetDisplayName()) 248 o = utils.AppendPlainToOpaque(o, "locktime", now.Format(time.RFC3339)) 249 250 if token == "" { 251 return errors.New("token is empty") 252 } 253 254 r := &provider.RefreshLockRequest{ 255 Ref: ref, 256 Lock: &provider.Lock{ 257 Opaque: o, 258 Type: provider.LockType_LOCK_TYPE_EXCL, 259 //AppName: , // TODO use a urn scheme? 260 LockId: token, 261 User: u.GetId(), 262 }, 263 } 264 265 client, err := cls.selector.Next() 266 if err != nil { 267 return err 268 } 269 270 res, err := client.RefreshLock(ctx, r) 271 if err != nil { 272 return err 273 } 274 switch res.GetStatus().GetCode() { 275 case rpc.Code_CODE_OK: 276 return nil 277 278 default: 279 return ocdavErrors.NewErrFromStatus(res.GetStatus()) 280 } 281 } 282 283 func (cls *cs3LS) Unlock(ctx context.Context, now time.Time, ref *provider.Reference, token string) error { 284 u := ctxpkg.ContextMustGetUser(ctx) 285 286 r := &provider.UnlockRequest{ 287 Ref: ref, 288 Lock: &provider.Lock{ 289 LockId: token, // can be a token or a Coded-URL 290 User: u.Id, 291 }, 292 } 293 294 client, err := cls.selector.Next() 295 if err != nil { 296 return err 297 } 298 299 res, err := client.Unlock(ctx, r) 300 if err != nil { 301 return err 302 } 303 304 newErr := ocdavErrors.NewErrFromStatus(res.GetStatus()) 305 if newErr != nil { 306 appctx.GetLogger(ctx).Error().Str("token", token).Interface("unlock", ref).Msg("could not unlock " + res.GetStatus().GetMessage()) 307 } 308 return newErr 309 } 310 311 // LockDetails are a lock's metadata. 312 type LockDetails struct { 313 // Root is the root resource name being locked. For a zero-depth lock, the 314 // root is the only resource being locked. 315 Root *provider.Reference 316 // Duration is the lock timeout. A negative duration means infinite. 317 Duration time.Duration 318 // OwnerXML is the verbatim <owner> XML given in a LOCK HTTP request. 319 // 320 // TODO: does the "verbatim" nature play well with XML namespaces? 321 // Does the OwnerXML field need to have more structure? See 322 // https://codereview.appspot.com/175140043/#msg2 323 OwnerXML string 324 UserID *userpb.UserId 325 // ZeroDepth is whether the lock has zero depth. If it does not have zero 326 // depth, it has infinite depth. 327 ZeroDepth bool 328 // OwnerName is the name of the lock owner 329 OwnerName string 330 // Locktime is the time the lock was created 331 Locktime time.Time 332 // LockID is the lock token 333 LockID string 334 } 335 336 func readLockInfo(r io.Reader) (li lockInfo, status int, err error) { 337 c := &countingReader{r: r} 338 if err = xml.NewDecoder(c).Decode(&li); err != nil { 339 if err == io.EOF { 340 if c.n == 0 { 341 // An empty body means to refresh the lock. 342 // http://www.webdav.org/specs/rfc4918.html#refreshing-locks 343 return lockInfo{}, 0, nil 344 } 345 err = ocdavErrors.ErrInvalidLockInfo 346 } 347 return lockInfo{}, http.StatusBadRequest, err 348 } 349 // We only support exclusive (non-shared) write locks. In practice, these are 350 // the only types of locks that seem to matter. 351 // We are ignoring the any properties in the lock details, and assume an exclusive write lock is requested. 352 // https://datatracker.ietf.org/doc/html/rfc4918#section-7 only describes write locks 353 // 354 // if li.Exclusive == nil || li.Shared != nil { 355 // return lockInfo{}, http.StatusNotImplemented, errors.ErrUnsupportedLockInfo 356 // } 357 // what should we return if the user requests a shared lock? or leaves out the locktype? the testsuite will only send the property lockscope, not locktype 358 // the oc tests cover both shared and exclusive locks. What is the WOPI lock? a shared or an exclusive lock? 359 // since it is issued by a service it seems to be an exclusive lock. 360 // the owner could be a link to the collaborative app ... to join the session 361 return li, 0, nil 362 } 363 364 type countingReader struct { 365 n int 366 r io.Reader 367 } 368 369 func (c *countingReader) Read(p []byte) (int, error) { 370 n, err := c.r.Read(p) 371 c.n += n 372 return n, err 373 } 374 375 const infiniteTimeout = -1 376 377 // parseTimeout parses the Timeout HTTP header, as per section 10.7. If s is 378 // empty, an infiniteTimeout is returned. 379 func parseTimeout(s string) (time.Duration, error) { 380 if s == "" { 381 return infiniteTimeout, nil 382 } 383 if i := strings.IndexByte(s, ','); i >= 0 { 384 s = s[:i] 385 } 386 s = strings.TrimSpace(s) 387 if s == "Infinite" { 388 return infiniteTimeout, nil 389 } 390 const pre = "Second-" 391 if !strings.HasPrefix(s, pre) { 392 return 0, ocdavErrors.ErrInvalidTimeout 393 } 394 s = s[len(pre):] 395 if s == "" || s[0] < '0' || '9' < s[0] { 396 return 0, ocdavErrors.ErrInvalidTimeout 397 } 398 n, err := strconv.ParseInt(s, 10, 64) 399 if err != nil || 1<<32-1 < n { 400 return 0, ocdavErrors.ErrInvalidTimeout 401 } 402 return time.Duration(n) * time.Second, nil 403 } 404 405 const ( 406 infiniteDepth = -1 407 invalidDepth = -2 408 ) 409 410 // parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and 411 // infiniteDepth. Parsing any other string returns invalidDepth. 412 // 413 // Different WebDAV methods have further constraints on valid depths: 414 // - PROPFIND has no further restrictions, as per section 9.1. 415 // - COPY accepts only "0" or "infinity", as per section 9.8.3. 416 // - MOVE accepts only "infinity", as per section 9.9.2. 417 // - LOCK accepts only "0" or "infinity", as per section 9.10.3. 418 // 419 // These constraints are enforced by the handleXxx methods. 420 func parseDepth(s string) int { 421 switch s { 422 case "0": 423 return 0 424 case "1": 425 return 1 426 case "infinity": 427 return infiniteDepth 428 } 429 return invalidDepth 430 } 431 432 /* 433 the oc 10 wopi app code locks like this: 434 435 $storage->lockNodePersistent($file->getInternalPath(), [ 436 'token' => $wopiLock, 437 'owner' => "{$user->getDisplayName()} via Office Online" 438 ]); 439 440 if owner is empty it defaults to '{displayname} ({email})', which is not a url ... but ... shrug 441 442 The LockManager also defaults to exclusive locks: 443 444 $scope = ILock::LOCK_SCOPE_EXCLUSIVE; 445 if (isset($lockInfo['scope'])) { 446 $scope = $lockInfo['scope']; 447 } 448 */ 449 func (s *svc) handleLock(w http.ResponseWriter, r *http.Request, ns string) (retStatus int, retErr error) { 450 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path)) 451 defer span.End() 452 453 span.SetAttributes(attribute.String("component", "ocdav")) 454 455 fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces? 456 457 // TODO instead of using a string namespace ns pass in the space with the request? 458 ref, cs3Status, err := spacelookup.LookupReferenceForPath(ctx, s.gatewaySelector, fn) 459 if err != nil { 460 return http.StatusInternalServerError, err 461 } 462 if cs3Status.Code != rpc.Code_CODE_OK { 463 return http.StatusInternalServerError, ocdavErrors.NewErrFromStatus(cs3Status) 464 } 465 466 return s.lockReference(ctx, w, r, ref) 467 } 468 469 func (s *svc) handleSpacesLock(w http.ResponseWriter, r *http.Request, spaceID string) (retStatus int, retErr error) { 470 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path)) 471 defer span.End() 472 473 span.SetAttributes(attribute.String("component", "ocdav")) 474 475 ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path) 476 if err != nil { 477 return http.StatusBadRequest, fmt.Errorf("invalid space id") 478 } 479 480 return s.lockReference(ctx, w, r, &ref) 481 } 482 483 func (s *svc) lockReference(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference) (retStatus int, retErr error) { 484 sublog := appctx.GetLogger(ctx).With().Interface("ref", ref).Logger() 485 duration, err := parseTimeout(r.Header.Get(net.HeaderTimeout)) 486 if err != nil { 487 return http.StatusBadRequest, ocdavErrors.ErrInvalidTimeout 488 } 489 490 li, status, err := readLockInfo(r.Body) 491 if err != nil { 492 return status, ocdavErrors.ErrInvalidLockInfo 493 } 494 495 u := ctxpkg.ContextMustGetUser(ctx) 496 token, now, created := "", time.Now(), false 497 ld := LockDetails{UserID: u.Id, Root: ref, Duration: duration, OwnerName: u.GetDisplayName(), Locktime: now, LockID: li.LockID} 498 if li == (lockInfo{}) { 499 // An empty lockInfo means to refresh the lock. 500 ih, ok := parseIfHeader(r.Header.Get(net.HeaderIf)) 501 if !ok { 502 return http.StatusBadRequest, ocdavErrors.ErrInvalidIfHeader 503 } 504 if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 { 505 token = ih.lists[0].conditions[0].Token 506 } 507 if token == "" { 508 return http.StatusBadRequest, ocdavErrors.ErrInvalidLockToken 509 } 510 err = s.LockSystem.Refresh(ctx, now, ref, token) 511 if err != nil { 512 if err == ocdavErrors.ErrNoSuchLock { 513 return http.StatusPreconditionFailed, err 514 } 515 return http.StatusInternalServerError, err 516 } 517 518 ld.LockID = token 519 520 } else { 521 // Section 9.10.3 says that "If no Depth header is submitted on a LOCK request, 522 // then the request MUST act as if a "Depth:infinity" had been submitted." 523 depth := infiniteDepth 524 if hdr := r.Header.Get(net.HeaderDepth); hdr != "" { 525 depth = parseDepth(hdr) 526 if depth != 0 && depth != infiniteDepth { 527 // Section 9.10.3 says that "Values other than 0 or infinity must not be 528 // used with the Depth header on a LOCK method". 529 return http.StatusBadRequest, ocdavErrors.ErrInvalidDepth 530 } 531 } 532 /* our url path has been shifted, so we don't need to do this? 533 reqPath, status, err := h.stripPrefix(r.URL.Path) 534 if err != nil { 535 return status, err 536 } 537 */ 538 // TODO look up username and email 539 // if li.Owner.InnerXML == "" { 540 // // PHP version: 'owner' => "{$user->getDisplayName()} via Office Online" 541 // ld.OwnerXML = ld.UserID.OpaqueId 542 // } 543 ld.OwnerXML = li.Owner.InnerXML // TODO optional, should be a URL 544 ld.ZeroDepth = depth == 0 545 546 //TODO: @jfd the code tries to create a lock for a file that may not even exist, 547 // should we do that in the decomposedfs as well? the node does not exist 548 // this actually is a name based lock ... ugh 549 token, err = s.LockSystem.Create(ctx, now, ld) 550 551 // 552 if err != nil { 553 switch { 554 case errors.Is(err, ocdavErrors.ErrLocked): 555 return http.StatusLocked, err 556 case errors.Is(err, ocdavErrors.ErrForbidden): 557 return http.StatusForbidden, err 558 default: 559 return http.StatusInternalServerError, err 560 } 561 } 562 563 defer func() { 564 if retErr != nil { 565 if err := s.LockSystem.Unlock(ctx, now, ref, token); err != nil { 566 appctx.GetLogger(ctx).Error().Err(err).Interface("lock", ld).Msg("could not unlock after failed lock") 567 } 568 } 569 }() 570 571 // Create the resource if it didn't previously exist. 572 // TODO use sdk to stat? 573 /* 574 if _, err := s.FileSystem.Stat(ctx, reqPath); err != nil { 575 f, err := s.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 576 if err != nil { 577 // TODO: detect missing intermediate dirs and return http.StatusConflict? 578 return http.StatusInternalServerError, err 579 } 580 f.Close() 581 created = true 582 } 583 */ 584 // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the 585 // Lock-Token value is a Coded-URL. We add angle brackets. 586 w.Header().Set("Lock-Token", "<"+token+">") 587 } 588 589 w.Header().Set("Content-Type", "application/xml; charset=utf-8") 590 if created { 591 // This is "w.WriteHeader(http.StatusCreated)" and not "return 592 // http.StatusCreated, nil" because we write our own (XML) response to w 593 // and Handler.ServeHTTP would otherwise write "Created". 594 w.WriteHeader(http.StatusCreated) 595 } 596 n, err := writeLockInfo(w, token, ld) 597 if err != nil { 598 sublog.Err(err).Int("bytes_written", n).Msg("error writing response") 599 } 600 return 0, nil 601 } 602 603 func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) { 604 depth := "infinity" 605 if ld.ZeroDepth { 606 depth = "0" 607 } 608 href := ld.Root.Path // FIXME add base url and space? 609 610 lockdiscovery := strings.Builder{} 611 lockdiscovery.WriteString(xml.Header) 612 lockdiscovery.WriteString("<d:prop xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\"><d:lockdiscovery><d:activelock>\n") 613 lockdiscovery.WriteString(" <d:locktype><d:write/></d:locktype>\n") 614 lockdiscovery.WriteString(" <d:lockscope><d:exclusive/></d:lockscope>\n") 615 lockdiscovery.WriteString(fmt.Sprintf(" <d:depth>%s</d:depth>\n", depth)) 616 if ld.OwnerXML != "" { 617 lockdiscovery.WriteString(fmt.Sprintf(" <d:owner>%s</d:owner>\n", ld.OwnerXML)) 618 } 619 if ld.Duration > 0 { 620 timeout := ld.Duration / time.Second 621 lockdiscovery.WriteString(fmt.Sprintf(" <d:timeout>Second-%d</d:timeout>\n", timeout)) 622 } else { 623 lockdiscovery.WriteString(" <d:timeout>Infinite</d:timeout>\n") 624 } 625 if token != "" { 626 lockdiscovery.WriteString(fmt.Sprintf(" <d:locktoken><d:href>%s</d:href></d:locktoken>\n", prop.Escape(token))) 627 } 628 if href != "" { 629 lockdiscovery.WriteString(fmt.Sprintf(" <d:lockroot><d:href>%s</d:href></d:lockroot>\n", prop.Escape(href))) 630 } 631 if ld.OwnerName != "" { 632 lockdiscovery.WriteString(fmt.Sprintf(" <oc:ownername>%s</oc:ownername>\n", prop.Escape(ld.OwnerName))) 633 } 634 if !ld.Locktime.IsZero() { 635 lockdiscovery.WriteString(fmt.Sprintf(" <oc:locktime>%s</oc:locktime>\n", prop.Escape(ld.Locktime.Format(time.RFC3339)))) 636 } 637 638 lockdiscovery.WriteString("</d:activelock></d:lockdiscovery></d:prop>") 639 640 return fmt.Fprint(w, lockdiscovery.String()) 641 } 642 643 func (s *svc) handleUnlock(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) { 644 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path)) 645 defer span.End() 646 647 span.SetAttributes(attribute.String("component", "ocdav")) 648 649 fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces? 650 651 // TODO instead of using a string namespace ns pass in the space with the request? 652 ref, cs3Status, err := spacelookup.LookupReferenceForPath(ctx, s.gatewaySelector, fn) 653 if err != nil { 654 return http.StatusInternalServerError, err 655 } 656 if cs3Status.Code != rpc.Code_CODE_OK { 657 return http.StatusInternalServerError, ocdavErrors.NewErrFromStatus(cs3Status) 658 } 659 660 return s.unlockReference(ctx, w, r, ref) 661 } 662 663 func (s *svc) handleSpaceUnlock(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) { 664 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path)) 665 defer span.End() 666 667 span.SetAttributes(attribute.String("component", "ocdav")) 668 669 ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path) 670 if err != nil { 671 return http.StatusBadRequest, fmt.Errorf("invalid space id") 672 } 673 674 return s.unlockReference(ctx, w, r, &ref) 675 } 676 677 func (s *svc) unlockReference(ctx context.Context, _ http.ResponseWriter, r *http.Request, ref *provider.Reference) (retStatus int, retErr error) { 678 // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the 679 // Lock-Token value should be a Coded-URL OR a token. We strip its angle brackets. 680 t := r.Header.Get(net.HeaderLockToken) 681 if len(t) > 2 && t[0] == '<' && t[len(t)-1] == '>' { 682 t = t[1 : len(t)-1] 683 } 684 685 err := s.LockSystem.Unlock(ctx, time.Now(), ref, t) 686 switch { 687 case err == nil: 688 return http.StatusNoContent, nil 689 case errors.Is(err, ocdavErrors.ErrLocked): 690 return http.StatusLocked, err 691 case errors.Is(err, ocdavErrors.ErrForbidden): 692 return http.StatusForbidden, err 693 } 694 return http.StatusInternalServerError, err 695 } 696 697 func requestLockToken(r *http.Request) string { 698 return strings.TrimSuffix(strings.TrimPrefix(r.Header.Get(net.HeaderLockToken), "<"), ">") 699 }