github.com/cs3org/reva/v2@v2.27.7/pkg/eosclient/eosgrpc/eoshttp.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 eosgrpc 20 21 import ( 22 "bytes" 23 "context" 24 "crypto/tls" 25 "fmt" 26 "io" 27 "net/http" 28 "net/url" 29 "os" 30 "strconv" 31 "time" 32 33 "github.com/cs3org/reva/v2/pkg/appctx" 34 "github.com/cs3org/reva/v2/pkg/eosclient" 35 "github.com/cs3org/reva/v2/pkg/errtypes" 36 "github.com/cs3org/reva/v2/pkg/logger" 37 ) 38 39 // HTTPOptions to configure the Client. 40 type HTTPOptions struct { 41 42 // HTTP URL of the EOS MGM. 43 // Default is https://eos-example.org 44 BaseURL string 45 46 // Timeout in seconds for connecting to the service 47 ConnectTimeout int 48 49 // Timeout in seconds for sending a request to the service and getting a response 50 // Does not include redirections 51 RWTimeout int 52 53 // Timeout in seconds for performing an operation. Includes every redirection, retry, etc 54 OpTimeout int 55 56 // Max idle conns per Transport 57 MaxIdleConns int 58 59 // Max conns per transport per destination host 60 MaxConnsPerHost int 61 62 // Max idle conns per transport per destination host 63 MaxIdleConnsPerHost int 64 65 // TTL for an idle conn per transport 66 IdleConnTimeout int 67 68 // If the URL is https, then we need to configure this client 69 // with the usual TLS stuff 70 // Defaults are /etc/grid-security/hostcert.pem and /etc/grid-security/hostkey.pem 71 ClientCertFile string 72 ClientKeyFile string 73 74 // These will override the defaults, which are common system paths hardcoded 75 // in the go x509 implementation (why did they do that?!?!?) 76 // of course /etc/grid-security/certificates is NOT in those defaults! 77 ClientCADirs string 78 ClientCAFiles string 79 } 80 81 // Init fills the basic fields 82 func (opt *HTTPOptions) init() { 83 84 if opt.BaseURL == "" { 85 opt.BaseURL = "https://eos-example.org" 86 } 87 88 if opt.ConnectTimeout == 0 { 89 opt.ConnectTimeout = 30 90 } 91 if opt.RWTimeout == 0 { 92 opt.RWTimeout = 180 93 } 94 if opt.OpTimeout == 0 { 95 opt.OpTimeout = 360 96 } 97 if opt.MaxIdleConns == 0 { 98 opt.MaxIdleConns = 100 99 } 100 if opt.MaxConnsPerHost == 0 { 101 opt.MaxConnsPerHost = 64 102 } 103 if opt.MaxIdleConnsPerHost == 0 { 104 opt.MaxIdleConnsPerHost = 8 105 } 106 if opt.IdleConnTimeout == 0 { 107 opt.IdleConnTimeout = 30 108 } 109 110 if opt.ClientCertFile == "" { 111 opt.ClientCertFile = "/etc/grid-security/hostcert.pem" 112 } 113 if opt.ClientKeyFile == "" { 114 opt.ClientKeyFile = "/etc/grid-security/hostkey.pem" 115 } 116 117 if opt.ClientCAFiles != "" { 118 os.Setenv("SSL_CERT_FILE", opt.ClientCAFiles) 119 } 120 if opt.ClientCADirs != "" { 121 os.Setenv("SSL_CERT_DIR", opt.ClientCADirs) 122 } else { 123 os.Setenv("SSL_CERT_DIR", "/etc/grid-security/certificates") 124 } 125 } 126 127 // EOSHTTPClient performs HTTP-based tasks (e.g. upload, download) 128 // against a EOS management node (MGM) 129 // using the EOS XrdHTTP interface. 130 // In this module we wrap eos-related behaviour, e.g. headers or r/w retries 131 type EOSHTTPClient struct { 132 opt *HTTPOptions 133 cl *http.Client 134 } 135 136 // NewEOSHTTPClient creates a new client with the given options. 137 func NewEOSHTTPClient(opt *HTTPOptions) (*EOSHTTPClient, error) { 138 log := logger.New().With().Int("pid", os.Getpid()).Logger() 139 log.Debug().Str("func", "New").Str("Creating new eoshttp client. opt: ", "'"+fmt.Sprintf("%#v", opt)+"' ").Msg("") 140 141 if opt == nil { 142 log.Debug().Str("opt is nil, error creating http client ", "").Msg("") 143 return nil, errtypes.InternalError("HTTPOptions is nil") 144 } 145 146 opt.init() 147 cert, err := tls.LoadX509KeyPair(opt.ClientCertFile, opt.ClientKeyFile) 148 if err != nil { 149 return nil, err 150 } 151 152 // TODO: the error reporting of http.transport is insufficient 153 // we may want to check manually at least the existence of the certfiles 154 // The point is that also the error reporting of the context that calls this function 155 // is weak 156 t := &http.Transport{ 157 TLSClientConfig: &tls.Config{ 158 Certificates: []tls.Certificate{cert}, 159 }, 160 MaxIdleConns: opt.MaxIdleConns, 161 MaxConnsPerHost: opt.MaxConnsPerHost, 162 MaxIdleConnsPerHost: opt.MaxIdleConnsPerHost, 163 IdleConnTimeout: time.Duration(opt.IdleConnTimeout) * time.Second, 164 DisableCompression: true, 165 } 166 167 cl := &http.Client{ 168 Transport: t, 169 CheckRedirect: func(req *http.Request, via []*http.Request) error { 170 return http.ErrUseLastResponse 171 }, 172 } 173 174 return &EOSHTTPClient{ 175 opt: opt, 176 cl: cl, 177 }, nil 178 } 179 180 // Format a human readable line that describes a response 181 func rspdesc(rsp *http.Response) string { 182 desc := "'" + fmt.Sprintf("%d", rsp.StatusCode) + "'" + ": '" + rsp.Status + "'" 183 184 buf := new(bytes.Buffer) 185 r := "<none>" 186 n, e := buf.ReadFrom(rsp.Body) 187 188 if e != nil { 189 r = "Error reading body: '" + e.Error() + "'" 190 } else if n > 0 { 191 r = buf.String() 192 } 193 194 desc += " - '" + r + "'" 195 196 return desc 197 } 198 199 // If the error is not nil, take that 200 // If there is an error coming from EOS, erturn a descriptive error 201 func (c *EOSHTTPClient) getRespError(rsp *http.Response, err error) error { 202 if err != nil { 203 return err 204 } 205 206 if rsp.StatusCode == 0 { 207 return nil 208 } 209 210 switch rsp.StatusCode { 211 case 0, 200, 201: 212 return nil 213 case 403: 214 return errtypes.PermissionDenied(rspdesc(rsp)) 215 case 404: 216 return errtypes.NotFound(rspdesc(rsp)) 217 } 218 219 err2 := errtypes.InternalError("Err from EOS: " + rspdesc(rsp)) 220 return err2 221 } 222 223 // From the basepath and the file path... build an url 224 func (c *EOSHTTPClient) buildFullURL(urlpath string, auth eosclient.Authorization) (string, error) { 225 226 u, err := url.Parse(c.opt.BaseURL) 227 if err != nil { 228 return "", err 229 } 230 231 u, err = u.Parse(url.PathEscape(urlpath)) 232 if err != nil { 233 return "", err 234 } 235 236 // Prohibit malicious users from injecting a false uid/gid into the url 237 v := u.Query() 238 if v.Get("eos.ruid") != "" || v.Get("eos.rgid") != "" { 239 return "", errtypes.PermissionDenied("Illegal malicious url " + urlpath) 240 } 241 242 if len(auth.Role.UID) > 0 { 243 v.Set("eos.ruid", auth.Role.UID) 244 } 245 if len(auth.Role.GID) > 0 { 246 v.Set("eos.rgid", auth.Role.GID) 247 } 248 249 u.RawQuery = v.Encode() 250 return u.String(), nil 251 } 252 253 // GETFile does an entire GET to download a full file. Returns a stream to read the content from 254 func (c *EOSHTTPClient) GETFile(ctx context.Context, remoteuser string, auth eosclient.Authorization, urlpath string, stream io.WriteCloser) (io.ReadCloser, error) { 255 256 log := appctx.GetLogger(ctx) 257 log.Info().Str("func", "GETFile").Str("remoteuser", remoteuser).Str("uid,gid", auth.Role.UID+","+auth.Role.GID).Str("path", urlpath).Msg("") 258 259 // Now send the req and see what happens 260 finalurl, err := c.buildFullURL(urlpath, auth) 261 if err != nil { 262 log.Error().Str("func", "GETFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't create request") 263 return nil, err 264 } 265 req, err := http.NewRequestWithContext(ctx, "GET", finalurl, nil) 266 if err != nil { 267 log.Error().Str("func", "GETFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't create request") 268 return nil, err 269 } 270 271 ntries := 0 272 nredirs := 0 273 timebegin := time.Now().Unix() 274 275 for { 276 // Check for a max count of redirections or retries 277 278 // Check for a global timeout in any case 279 tdiff := time.Now().Unix() - timebegin 280 if tdiff > int64(c.opt.OpTimeout) { 281 log.Error().Str("func", "GETFile").Str("url", finalurl).Int64("timeout", tdiff).Int("ntries", ntries).Msg("") 282 return nil, errtypes.InternalError("Timeout with url" + finalurl) 283 } 284 285 // Execute the request. I don't like that there is no explicit timeout or buffer control on the input stream 286 log.Debug().Str("func", "GETFile").Msg("sending req") 287 resp, err := c.cl.Do(req) 288 289 // Let's support redirections... and if we retry we have to retry at the same FST, avoid going back to the MGM 290 if resp != nil && (resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusTemporaryRedirect) { 291 292 // io.Copy(io.Discard, resp.Body) 293 // resp.Body.Close() 294 295 loc, err := resp.Location() 296 if err != nil { 297 log.Error().Str("func", "GETFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't get a new location for a redirection") 298 return nil, err 299 } 300 301 req, err = http.NewRequestWithContext(ctx, "GET", loc.String(), nil) 302 if err != nil { 303 log.Error().Str("func", "GETFile").Str("url", loc.String()).Str("err", err.Error()).Msg("can't create redirected request") 304 return nil, err 305 } 306 307 req.Close = true 308 309 log.Debug().Str("func", "GETFile").Str("location", loc.String()).Msg("redirection") 310 nredirs++ 311 resp = nil 312 err = nil 313 continue 314 } 315 316 // And get an error code (if error) that is worth propagating 317 e := c.getRespError(resp, err) 318 if e != nil { 319 if os.IsTimeout(e) { 320 ntries++ 321 log.Warn().Str("func", "GETFile").Str("url", finalurl).Str("err", e.Error()).Int("try", ntries).Msg("recoverable network timeout") 322 continue 323 } 324 log.Error().Str("func", "GETFile").Str("url", finalurl).Str("err", e.Error()).Msg("") 325 return nil, e 326 } 327 328 log.Debug().Str("func", "GETFile").Str("url", finalurl).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("") 329 if resp == nil { 330 return nil, errtypes.NotFound(fmt.Sprintf("url: %s", finalurl)) 331 } 332 333 if stream != nil { 334 // Streaming versus localfile. If we have bene given a dest stream then copy the body into it 335 _, err = io.Copy(stream, resp.Body) 336 return nil, err 337 } 338 339 // If we have not been given a stream to write into then return our stream to read from 340 return resp.Body, nil 341 } 342 343 } 344 345 // PUTFile does an entire PUT to upload a full file, taking the data from a stream 346 func (c *EOSHTTPClient) PUTFile(ctx context.Context, remoteuser string, auth eosclient.Authorization, urlpath string, stream io.ReadCloser, length int64) error { 347 348 log := appctx.GetLogger(ctx) 349 log.Info().Str("func", "PUTFile").Str("remoteuser", remoteuser).Str("uid,gid", auth.Role.UID+","+auth.Role.GID).Str("path", urlpath).Int64("length", length).Msg("") 350 351 // Now send the req and see what happens 352 finalurl, err := c.buildFullURL(urlpath, auth) 353 if err != nil { 354 log.Error().Str("func", "PUTFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't create request") 355 return err 356 } 357 req, err := http.NewRequestWithContext(ctx, "PUT", finalurl, nil) 358 if err != nil { 359 log.Error().Str("func", "PUTFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't create request") 360 return err 361 } 362 363 req.Close = true 364 365 ntries := 0 366 nredirs := 0 367 timebegin := time.Now().Unix() 368 369 for { 370 // Check for a max count of redirections or retries 371 372 // Check for a global timeout in any case 373 tdiff := time.Now().Unix() - timebegin 374 if tdiff > int64(c.opt.OpTimeout) { 375 log.Error().Str("func", "PUTFile").Str("url", finalurl).Int64("timeout", tdiff).Int("ntries", ntries).Msg("") 376 return errtypes.InternalError("Timeout with url" + finalurl) 377 } 378 379 // Execute the request. I don't like that there is no explicit timeout or buffer control on the input stream 380 log.Debug().Str("func", "PUTFile").Msg("sending req") 381 resp, err := c.cl.Do(req) 382 if resp != nil { 383 resp.Body.Close() 384 } 385 386 // Let's support redirections... and if we retry we retry at the same FST 387 if resp != nil && resp.StatusCode == 307 { 388 389 // io.Copy(io.Discard, resp.Body) 390 // resp.Body.Close() 391 392 loc, err := resp.Location() 393 if err != nil { 394 log.Error().Str("func", "PUTFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't get a new location for a redirection") 395 return err 396 } 397 398 req, err = http.NewRequestWithContext(ctx, "PUT", loc.String(), stream) 399 if err != nil { 400 log.Error().Str("func", "PUTFile").Str("url", loc.String()).Str("err", err.Error()).Msg("can't create redirected request") 401 return err 402 } 403 if length >= 0 { 404 log.Debug().Str("func", "PUTFile").Int64("Content-Length", length).Msg("setting header") 405 req.Header.Set("Content-Length", strconv.FormatInt(length, 10)) 406 407 } 408 if err != nil { 409 log.Error().Str("func", "PUTFile").Str("url", loc.String()).Str("err", err.Error()).Msg("can't create redirected request") 410 return err 411 } 412 if length >= 0 { 413 log.Debug().Str("func", "PUTFile").Int64("Content-Length", length).Msg("setting header") 414 req.Header.Set("Content-Length", strconv.FormatInt(length, 10)) 415 416 } 417 418 log.Debug().Str("func", "PUTFile").Str("location", loc.String()).Msg("redirection") 419 nredirs++ 420 resp = nil 421 err = nil 422 continue 423 } 424 425 // And get an error code (if error) that is worth propagating 426 e := c.getRespError(resp, err) 427 if e != nil { 428 if os.IsTimeout(e) { 429 ntries++ 430 log.Warn().Str("func", "PUTFile").Str("url", finalurl).Str("err", e.Error()).Int("try", ntries).Msg("recoverable network timeout") 431 continue 432 } 433 log.Error().Str("func", "PUTFile").Str("url", finalurl).Str("err", e.Error()).Msg("") 434 return e 435 } 436 437 log.Debug().Str("func", "PUTFile").Str("url", finalurl).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("") 438 if resp == nil { 439 return errtypes.NotFound(fmt.Sprintf("url: %s", finalurl)) 440 } 441 442 return nil 443 } 444 445 } 446 447 // Head performs a HEAD req. Useful to check the server 448 func (c *EOSHTTPClient) Head(ctx context.Context, remoteuser string, auth eosclient.Authorization, urlpath string) error { 449 450 log := appctx.GetLogger(ctx) 451 log.Info().Str("func", "Head").Str("remoteuser", remoteuser).Str("uid,gid", auth.Role.UID+","+auth.Role.GID).Str("path", urlpath).Msg("") 452 453 // Now send the req and see what happens 454 finalurl, err := c.buildFullURL(urlpath, auth) 455 if err != nil { 456 log.Error().Str("func", "Head").Str("url", finalurl).Str("err", err.Error()).Msg("can't create request") 457 return err 458 } 459 460 req, err := http.NewRequestWithContext(ctx, "HEAD", finalurl, nil) 461 if err != nil { 462 log.Error().Str("func", "Head").Str("remoteuser", remoteuser).Str("uid,gid", auth.Role.UID+","+auth.Role.GID).Str("url", finalurl).Str("err", err.Error()).Msg("can't create request") 463 return err 464 } 465 466 ntries := 0 467 468 timebegin := time.Now().Unix() 469 for { 470 tdiff := time.Now().Unix() - timebegin 471 if tdiff > int64(c.opt.OpTimeout) { 472 log.Error().Str("func", "Head").Str("url", finalurl).Int64("timeout", tdiff).Int("ntries", ntries).Msg("") 473 return errtypes.InternalError("Timeout with url" + finalurl) 474 } 475 // Execute the request. I don't like that there is no explicit timeout or buffer control on the input stream 476 resp, err := c.cl.Do(req) 477 if resp != nil { 478 resp.Body.Close() 479 } 480 481 // And get an error code (if error) that is worth propagating 482 e := c.getRespError(resp, err) 483 if e != nil { 484 if os.IsTimeout(e) { 485 ntries++ 486 log.Warn().Str("func", "Head").Str("url", finalurl).Str("err", e.Error()).Int("try", ntries).Msg("recoverable network timeout") 487 continue 488 } 489 log.Error().Str("func", "Head").Str("url", finalurl).Str("err", e.Error()).Msg("") 490 return e 491 } 492 493 log.Debug().Str("func", "Head").Str("url", finalurl).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("") 494 if resp == nil { 495 return errtypes.NotFound(fmt.Sprintf("url: %s", finalurl)) 496 } 497 } 498 // return nil 499 500 }