github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/tpc.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 "fmt" 24 "io" 25 "net/http" 26 "path" 27 "strconv" 28 "strings" 29 "time" 30 31 gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" 32 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 33 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 34 typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" 35 "github.com/cs3org/reva/v2/internal/http/services/datagateway" 36 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors" 37 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" 38 "github.com/cs3org/reva/v2/pkg/appctx" 39 "github.com/cs3org/reva/v2/pkg/errtypes" 40 "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" 41 "github.com/cs3org/reva/v2/pkg/rhttp" 42 ) 43 44 const ( 45 // PerfMarkerResponseTime corresponds to the interval at which a performance marker is sent back to the TPC client 46 PerfMarkerResponseTime float64 = 5 47 ) 48 49 // PerfResponse provides a single chunk of permormance marker response 50 type PerfResponse struct { 51 Timestamp time.Time 52 Bytes uint64 53 Index int 54 Count int 55 } 56 57 func (p *PerfResponse) getPerfResponseString() string { 58 var sb strings.Builder 59 sb.WriteString("Perf Marker\n") 60 sb.WriteString("Timestamp: " + strconv.FormatInt(p.Timestamp.Unix(), 10) + "\n") 61 sb.WriteString("Stripe Bytes Transferred: " + strconv.FormatUint(p.Bytes, 10) + "\n") 62 sb.WriteString("Strip Index: " + strconv.Itoa(p.Index) + "\n") 63 sb.WriteString("Total Stripe Count: " + strconv.Itoa(p.Count) + "\n") 64 sb.WriteString("End\n") 65 return sb.String() 66 } 67 68 // WriteCounter counts the number of bytes transferred and reports 69 // back to the TPC client about the progress of the transfer 70 // through the performance marker response stream. 71 type WriteCounter struct { 72 Total uint64 73 PrevTime time.Time 74 w http.ResponseWriter 75 } 76 77 // SendPerfMarker flushes a single chunk (performance marker) as 78 // part of the chunked transfer encoding scheme. 79 func (wc *WriteCounter) SendPerfMarker(size uint64) { 80 flusher, ok := wc.w.(http.Flusher) 81 if !ok { 82 panic("expected http.ResponseWriter to be an http.Flusher") 83 } 84 perfResp := PerfResponse{time.Now(), size, 0, 1} 85 pString := perfResp.getPerfResponseString() 86 fmt.Fprintln(wc.w, pString) 87 flusher.Flush() 88 } 89 90 func (wc *WriteCounter) Write(p []byte) (int, error) { 91 92 n := len(p) 93 wc.Total += uint64(n) 94 NowTime := time.Now() 95 96 diff := NowTime.Sub(wc.PrevTime).Seconds() 97 if diff >= PerfMarkerResponseTime { 98 wc.SendPerfMarker(wc.Total) 99 wc.PrevTime = NowTime 100 } 101 return n, nil 102 } 103 104 // 105 // An example of an HTTP TPC Pull 106 // 107 // +-----------------+ GET +----------------+ 108 // | Src server | <---------------- | Dest server | 109 // | (Remote) | ----------------> | (Reva) | 110 // +-----------------+ Data +----------------+ 111 // ^ 112 // | 113 // | COPY 114 // | 115 // +----------+ 116 // | Client | 117 // +----------+ 118 119 // handleTPCPull performs a GET request on the remote site and upload it 120 // the requested reva endpoint. 121 func (s *svc) handleTPCPull(ctx context.Context, w http.ResponseWriter, r *http.Request, ns string) { 122 src := r.Header.Get("Source") 123 dst := path.Join(ns, r.URL.Path) 124 sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger() 125 126 oh := r.Header.Get(net.HeaderOverwrite) 127 overwrite, err := net.ParseOverwrite(oh) 128 if err != nil { 129 w.WriteHeader(http.StatusBadRequest) 130 m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite) 131 sublog.Warn().Msgf("HTTP TPC Pull: %s", m) 132 b, err := errors.Marshal(http.StatusBadRequest, m, "", "") 133 errors.HandleWebdavError(&sublog, w, b, err) 134 return 135 } 136 sublog.Debug().Bool("overwrite", overwrite).Msg("TPC Pull") 137 138 client, err := s.gatewaySelector.Next() 139 if err != nil { 140 sublog.Error().Err(err).Msg("error selecting next gateway client") 141 w.WriteHeader(http.StatusInternalServerError) 142 return 143 } 144 // check if destination exists 145 ref := &provider.Reference{Path: dst} 146 dstStatReq := &provider.StatRequest{Ref: ref} 147 dstStatRes, err := client.Stat(ctx, dstStatReq) 148 if err != nil { 149 sublog.Error().Err(err).Msg("error sending grpc stat request") 150 w.WriteHeader(http.StatusInternalServerError) 151 return 152 } 153 if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { 154 errors.HandleErrorStatus(&sublog, w, dstStatRes.Status) 155 return 156 } 157 if dstStatRes.Status.Code == rpc.Code_CODE_OK && oh == "F" { 158 sublog.Warn().Bool("overwrite", overwrite).Msg("Destination already exists") 159 w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5 160 return 161 } 162 163 err = s.performHTTPPull(ctx, s.gatewaySelector, r, w, ns) 164 if err != nil { 165 sublog.Error().Err(err).Msg("error performing TPC Pull") 166 return 167 } 168 fmt.Fprintf(w, "success: Created") 169 } 170 171 func (s *svc) performHTTPPull(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], r *http.Request, w http.ResponseWriter, ns string) error { 172 src := r.Header.Get("Source") 173 dst := path.Join(ns, r.URL.Path) 174 sublog := appctx.GetLogger(ctx) 175 sublog.Debug().Str("src", src).Str("dst", dst).Msg("Performing HTTP Pull") 176 177 // get http client for remote 178 httpClient := &http.Client{} 179 180 req, err := http.NewRequest("GET", src, nil) 181 if err != nil { 182 w.WriteHeader(http.StatusInternalServerError) 183 return err 184 } 185 186 // add authentication header 187 bearerHeader := r.Header.Get(net.HeaderTransferAuth) 188 req.Header.Add("Authorization", bearerHeader) 189 190 // do download 191 httpDownloadRes, err := httpClient.Do(req) // lgtm[go/request-forgery] 192 if err != nil { 193 w.WriteHeader(http.StatusInternalServerError) 194 return err 195 } 196 defer httpDownloadRes.Body.Close() 197 198 if httpDownloadRes.StatusCode == http.StatusNotImplemented { 199 w.WriteHeader(http.StatusBadRequest) 200 return errtypes.NotSupported("Third-Party copy not supported, source might be a folder") 201 } 202 if httpDownloadRes.StatusCode != http.StatusOK { 203 w.WriteHeader(httpDownloadRes.StatusCode) 204 return errtypes.InternalError(fmt.Sprintf("Remote GET returned status code %d", httpDownloadRes.StatusCode)) 205 } 206 207 client, err := s.gatewaySelector.Next() 208 if err != nil { 209 sublog.Error().Err(err).Msg("error selecting next gateway client") 210 w.WriteHeader(http.StatusInternalServerError) 211 return errtypes.InternalError(err.Error()) 212 } 213 // get upload url 214 uReq := &provider.InitiateFileUploadRequest{ 215 Ref: &provider.Reference{Path: dst}, 216 Opaque: &typespb.Opaque{ 217 Map: map[string]*typespb.OpaqueEntry{ 218 "sizedeferred": { 219 Value: []byte("true"), 220 }, 221 }, 222 }, 223 } 224 uRes, err := client.InitiateFileUpload(ctx, uReq) 225 if err != nil { 226 w.WriteHeader(http.StatusInternalServerError) 227 return err 228 } 229 230 if uRes.Status.Code != rpc.Code_CODE_OK { 231 w.WriteHeader(http.StatusInternalServerError) 232 return fmt.Errorf("status code %d", uRes.Status.Code) 233 } 234 235 var uploadEP, uploadToken string 236 for _, p := range uRes.Protocols { 237 if p.Protocol == "simple" { 238 uploadEP, uploadToken = p.UploadEndpoint, p.Token 239 } 240 } 241 242 // send performance markers periodically every PerfMarkerResponseTime (5 seconds unless configured) 243 w.WriteHeader(http.StatusAccepted) 244 wc := WriteCounter{0, time.Now(), w} 245 tempReader := io.TeeReader(httpDownloadRes.Body, &wc) 246 247 // do Upload 248 httpUploadReq, err := rhttp.NewRequest(ctx, "PUT", uploadEP, tempReader) 249 if err != nil { 250 w.WriteHeader(http.StatusInternalServerError) 251 return err 252 } 253 httpUploadReq.Header.Set(datagateway.TokenTransportHeader, uploadToken) 254 httpUploadRes, err := s.client.Do(httpUploadReq) 255 if err != nil { 256 w.WriteHeader(http.StatusInternalServerError) 257 return err 258 } 259 260 defer httpUploadRes.Body.Close() 261 if httpUploadRes.StatusCode != http.StatusOK { 262 w.WriteHeader(httpUploadRes.StatusCode) 263 return err 264 } 265 return nil 266 } 267 268 // 269 // An example of an HTTP TPC Push 270 // 271 // +-----------------+ PUT +----------------+ 272 // | Dest server | <---------------- | Src server | 273 // | (Remote) | ----------------> | (Reva) | 274 // +-----------------+ Done +----------------+ 275 // ^ 276 // | 277 // | COPY 278 // | 279 // +----------+ 280 // | Client | 281 // +----------+ 282 283 // handleTPCPush performs a PUT request on the remote site and while downloading 284 // data from the requested reva endpoint. 285 func (s *svc) handleTPCPush(ctx context.Context, w http.ResponseWriter, r *http.Request, ns string) { 286 src := path.Join(ns, r.URL.Path) 287 dst := r.Header.Get("Destination") 288 sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger() 289 290 oh := r.Header.Get(net.HeaderOverwrite) 291 overwrite, err := net.ParseOverwrite(oh) 292 if err != nil { 293 w.WriteHeader(http.StatusBadRequest) 294 m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite) 295 sublog.Warn().Msgf("HTTP TPC Push: %s", m) 296 b, err := errors.Marshal(http.StatusBadRequest, m, "", "") 297 errors.HandleWebdavError(&sublog, w, b, err) 298 return 299 } 300 301 sublog.Debug().Bool("overwrite", overwrite).Msg("TPC Push") 302 303 client, err := s.gatewaySelector.Next() 304 if err != nil { 305 sublog.Error().Err(err).Msg("error selecting next gateway client") 306 w.WriteHeader(http.StatusInternalServerError) 307 return 308 } 309 ref := &provider.Reference{Path: src} 310 srcStatReq := &provider.StatRequest{Ref: ref} 311 srcStatRes, err := client.Stat(ctx, srcStatReq) 312 if err != nil { 313 sublog.Error().Err(err).Msg("error sending grpc stat request") 314 w.WriteHeader(http.StatusInternalServerError) 315 return 316 } 317 if srcStatRes.Status.Code != rpc.Code_CODE_OK && srcStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { 318 errors.HandleErrorStatus(&sublog, w, srcStatRes.Status) 319 return 320 } 321 if srcStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 322 sublog.Error().Msg("Third-Party copy of a folder is not supported") 323 w.WriteHeader(http.StatusBadRequest) 324 return 325 } 326 327 err = s.performHTTPPush(ctx, r, w, srcStatRes.Info, ns) 328 if err != nil { 329 sublog.Error().Err(err).Msg("error performing TPC Push") 330 return 331 } 332 fmt.Fprintf(w, "success: Created") 333 } 334 335 func (s *svc) performHTTPPush(ctx context.Context, r *http.Request, w http.ResponseWriter, srcInfo *provider.ResourceInfo, ns string) error { 336 src := path.Join(ns, r.URL.Path) 337 dst := r.Header.Get("Destination") 338 339 sublog := appctx.GetLogger(ctx) 340 sublog.Debug().Str("src", src).Str("dst", dst).Msg("Performing HTTP Push") 341 342 // get download url 343 dReq := &provider.InitiateFileDownloadRequest{ 344 Ref: &provider.Reference{Path: src}, 345 } 346 347 client, err := s.gatewaySelector.Next() 348 if err != nil { 349 sublog.Error().Err(err).Msg("error selecting next gateway client") 350 w.WriteHeader(http.StatusInternalServerError) 351 return err 352 } 353 dRes, err := client.InitiateFileDownload(ctx, dReq) 354 if err != nil { 355 w.WriteHeader(http.StatusInternalServerError) 356 return err 357 } 358 359 if dRes.Status.Code != rpc.Code_CODE_OK { 360 w.WriteHeader(http.StatusInternalServerError) 361 return fmt.Errorf("status code %d", dRes.Status.Code) 362 } 363 364 var downloadEP, downloadToken string 365 for _, p := range dRes.Protocols { 366 if p.Protocol == "simple" { 367 downloadEP, downloadToken = p.DownloadEndpoint, p.Token 368 } 369 } 370 371 // do download 372 httpDownloadReq, err := rhttp.NewRequest(ctx, "GET", downloadEP, nil) 373 if err != nil { 374 w.WriteHeader(http.StatusInternalServerError) 375 return err 376 } 377 httpDownloadReq.Header.Set(datagateway.TokenTransportHeader, downloadToken) 378 379 httpDownloadRes, err := s.client.Do(httpDownloadReq) 380 if err != nil { 381 w.WriteHeader(http.StatusInternalServerError) 382 return err 383 } 384 defer httpDownloadRes.Body.Close() 385 if httpDownloadRes.StatusCode != http.StatusOK { 386 w.WriteHeader(httpDownloadRes.StatusCode) 387 return fmt.Errorf("Remote PUT returned status code %d", httpDownloadRes.StatusCode) 388 } 389 390 // send performance markers periodically every PerfMarkerResponseTime (5 seconds unless configured) 391 w.WriteHeader(http.StatusAccepted) 392 wc := WriteCounter{0, time.Now(), w} 393 tempReader := io.TeeReader(httpDownloadRes.Body, &wc) 394 395 // get http client for a remote call 396 httpClient := &http.Client{} 397 req, err := http.NewRequest("PUT", dst, tempReader) 398 if err != nil { 399 w.WriteHeader(http.StatusInternalServerError) 400 return err 401 } 402 403 // add authentication header and content length 404 bearerHeader := r.Header.Get(net.HeaderTransferAuth) 405 req.Header.Add("Authorization", bearerHeader) 406 req.ContentLength = int64(srcInfo.GetSize()) 407 408 // do Upload 409 httpUploadRes, err := httpClient.Do(req) // lgtm[go/request-forgery] 410 if err != nil { 411 w.WriteHeader(http.StatusInternalServerError) 412 return err 413 } 414 defer httpUploadRes.Body.Close() 415 416 if httpUploadRes.StatusCode != http.StatusOK { 417 w.WriteHeader(httpUploadRes.StatusCode) 418 return err 419 } 420 421 return nil 422 }