github.com/pelicanplatform/pelican@v1.0.5/client/director.go (about)

     1  /***************************************************************
     2   *
     3   * Copyright (C) 2023, Pelican Project, Morgridge Institute for Research
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License"); you
     6   * may not use this file except in compliance with the License.  You may
     7   * obtain a copy of the License at
     8   *
     9   *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   ***************************************************************/
    18  
    19  package client
    20  
    21  import (
    22  	"encoding/json"
    23  	"io"
    24  	"net/http"
    25  	"net/url"
    26  	"sort"
    27  	"strconv"
    28  	"strings"
    29  
    30  	"github.com/pelicanplatform/pelican/config"
    31  	namespaces "github.com/pelicanplatform/pelican/namespaces"
    32  	"github.com/pkg/errors"
    33  	log "github.com/sirupsen/logrus"
    34  )
    35  
    36  type directorResponse struct {
    37  	Error string `json:"error"`
    38  }
    39  
    40  // Simple parser to that takes a "values" string from a header and turns it
    41  // into a map of key/value pairs
    42  func HeaderParser(values string) (retMap map[string]string) {
    43  	retMap = map[string]string{}
    44  
    45  	// Some headers might not have values, such as the
    46  	// X-OSDF-Authorization header when the resource is public
    47  	if values == "" {
    48  		return
    49  	}
    50  
    51  	mapPairs := strings.Split(values, ",")
    52  	for _, pair := range mapPairs {
    53  		// Remove any unwanted spaces
    54  		pair = strings.ReplaceAll(pair, " ", "")
    55  
    56  		// Break out key/value pairs and put in the map
    57  		split := strings.Split(pair, "=")
    58  		retMap[split[0]] = split[1]
    59  	}
    60  
    61  	return retMap
    62  }
    63  
    64  // Given the Director response, create the ordered list of caches
    65  // and store it as namespace.SortedDirectorCaches
    66  func CreateNsFromDirectorResp(dirResp *http.Response) (namespace namespaces.Namespace, err error) {
    67  	pelicanNamespaceHdr := dirResp.Header.Values("X-Pelican-Namespace")
    68  	if len(pelicanNamespaceHdr) == 0 {
    69  		err = errors.New("Pelican director did not include mandatory X-Pelican-Namespace header in response")
    70  		return
    71  	}
    72  	xPelicanNamespace := HeaderParser(pelicanNamespaceHdr[0])
    73  	namespace.Path = xPelicanNamespace["namespace"]
    74  	namespace.UseTokenOnRead, _ = strconv.ParseBool(xPelicanNamespace["require-token"])
    75  	namespace.ReadHTTPS, _ = strconv.ParseBool(xPelicanNamespace["readhttps"])
    76  	namespace.DirListHost = xPelicanNamespace["collections-url"]
    77  
    78  	var xPelicanAuthorization map[string]string
    79  	if len(dirResp.Header.Values("X-Pelican-Authorization")) > 0 {
    80  		xPelicanAuthorization = HeaderParser(dirResp.Header.Values("X-Pelican-Authorization")[0])
    81  		namespace.Issuer = xPelicanAuthorization["issuer"]
    82  	}
    83  
    84  	var xPelicanTokenGeneration map[string]string
    85  	if len(dirResp.Header.Values("X-Pelican-Token-Generation")) > 0 {
    86  		xPelicanTokenGeneration = HeaderParser(dirResp.Header.Values("X-Pelican-Token-Generation")[0])
    87  
    88  		// Instantiate the cred gen struct
    89  		namespace.CredentialGen = &namespaces.CredentialGeneration{}
    90  
    91  		// We wind up with a duplicate issuer here as the encapsulating ns also encodes this
    92  		issuer := xPelicanTokenGeneration["issuer"]
    93  		namespace.CredentialGen.Issuer = &issuer
    94  
    95  		base_path := xPelicanTokenGeneration["base-path"]
    96  		namespace.CredentialGen.BasePath = &base_path
    97  
    98  		if max_scope_depth, exists := xPelicanTokenGeneration["max-scope-depth"]; exists {
    99  			max_scope_depth_int, err := strconv.Atoi(max_scope_depth)
   100  			if err != nil {
   101  				log.Debugln("Server sent an invalid max scope depth; ignoring:", max_scope_depth)
   102  			} else {
   103  				namespace.CredentialGen.MaxScopeDepth = &max_scope_depth_int
   104  			}
   105  		}
   106  
   107  		strategy := xPelicanTokenGeneration["strategy"]
   108  		namespace.CredentialGen.Strategy = &strategy
   109  
   110  		// The Director only returns a vault server if the strategy is vault.
   111  		if vs, exists := xPelicanTokenGeneration["vault-server"]; exists {
   112  			namespace.CredentialGen.VaultServer = &vs
   113  		}
   114  	}
   115  
   116  	// Create the caches slice
   117  	namespace.SortedDirectorCaches, err = GetCachesFromDirectorResponse(dirResp, namespace.UseTokenOnRead || namespace.ReadHTTPS)
   118  	if err != nil {
   119  		log.Errorln("Unable to construct ordered cache list:", err)
   120  		return
   121  	}
   122  	log.Debugln("Namespace path constructed from Director:", namespace.Path)
   123  
   124  	return
   125  }
   126  
   127  func QueryDirector(source string, directorUrl string) (resp *http.Response, err error) {
   128  	resourceUrl := directorUrl + source
   129  	// Here we use http.Transport to prevent the client from following the director's
   130  	// redirect. We use the Location url elsewhere (plus we still need to do the token
   131  	// dance!)
   132  	var client *http.Client
   133  	tr := config.GetTransport()
   134  	client = &http.Client{
   135  		Transport: tr,
   136  		CheckRedirect: func(req *http.Request, via []*http.Request) error {
   137  			return http.ErrUseLastResponse
   138  		},
   139  	}
   140  
   141  	req, err := http.NewRequest("GET", resourceUrl, nil)
   142  	if err != nil {
   143  		log.Errorln("Failed to create an HTTP request:", err)
   144  		return nil, err
   145  	}
   146  
   147  	// Include the Client's version as a User-Agent header. The Director will decide
   148  	// if it supports the version, and provide an error message in the case that it
   149  	// cannot.
   150  	userAgent := "pelican-client/" + ObjectClientOptions.Version
   151  	req.Header.Set("User-Agent", userAgent)
   152  
   153  	// Perform the HTTP request
   154  	resp, err = client.Do(req)
   155  
   156  	if err != nil {
   157  		log.Errorln("Failed to get response from the director:", err)
   158  		return
   159  	}
   160  
   161  	defer resp.Body.Close()
   162  	log.Debugln("Director's response:", resp)
   163  
   164  	// Check HTTP response -- should be 307 (redirect), else something went wrong
   165  	body, _ := io.ReadAll(resp.Body)
   166  
   167  	// If we get a 404, the director will hopefully tell us why. It might be that the namespace doesn't exist
   168  	if resp.StatusCode == 404 {
   169  		return nil, errors.New("404: " + string(body))
   170  	} else if resp.StatusCode != 307 {
   171  		var respErr directorResponse
   172  		if unmarshalErr := json.Unmarshal(body, &respErr); unmarshalErr != nil { // Error creating json
   173  			return nil, errors.Wrap(unmarshalErr, "Could not unmarshall the director's response")
   174  		}
   175  		return nil, errors.Errorf("The director reported an error: %s\n", respErr.Error)
   176  	}
   177  
   178  	return
   179  }
   180  
   181  func GetCachesFromDirectorResponse(resp *http.Response, needsToken bool) (caches []namespaces.DirectorCache, err error) {
   182  	// Get the Link header
   183  	linkHeader := resp.Header.Values("Link")
   184  	if len(linkHeader) == 0 {
   185  		return []namespaces.DirectorCache{}, nil
   186  	}
   187  
   188  	for _, linksStr := range strings.Split(linkHeader[0], ",") {
   189  		links := strings.Split(strings.ReplaceAll(linksStr, " ", ""), ";")
   190  
   191  		var endpoint string
   192  		// var rel string // "rel", as defined in the Metalink/HTTP RFC. Currently not being used by
   193  		// the OSDF Client, but is provided by the director. Will be useful in the future when
   194  		// we start looking at cases where we want to duplicate from caches if we're throttling
   195  		// connections to the origin.
   196  		var pri int
   197  		for _, val := range links {
   198  			if strings.HasPrefix(val, "<") {
   199  				endpoint = val[1 : len(val)-1]
   200  			} else if strings.HasPrefix(val, "pri") {
   201  				pri, _ = strconv.Atoi(val[4:])
   202  			}
   203  			// } else if strings.HasPrefix(val, "rel") {
   204  			// 	rel = val[5 : len(val)-1]
   205  			// }
   206  		}
   207  
   208  		// Construct the cache objects, getting endpoint and auth requirements from
   209  		// Director
   210  		var cache namespaces.DirectorCache
   211  		cache.AuthedReq = needsToken
   212  		cache.EndpointUrl = endpoint
   213  		cache.Priority = pri
   214  		caches = append(caches, cache)
   215  	}
   216  
   217  	// Making the assumption that the Link header doesn't already provide the caches
   218  	// in order (even though it probably does). This sorts the caches and ensures
   219  	// we're using the "pri" tag to order them
   220  	sort.Slice(caches, func(i, j int) bool {
   221  		val1 := caches[i].Priority
   222  		val2 := caches[j].Priority
   223  		return val1 < val2
   224  	})
   225  
   226  	return caches, err
   227  }
   228  
   229  // NewTransferDetails creates the TransferDetails struct with the given cache
   230  func NewTransferDetailsUsingDirector(cache namespaces.DirectorCache, opts TransferDetailsOptions) []TransferDetails {
   231  	details := make([]TransferDetails, 0)
   232  	cacheEndpoint := cache.EndpointUrl
   233  
   234  	// Form the URL
   235  	cacheURL, err := url.Parse(cacheEndpoint)
   236  	if err != nil {
   237  		log.Errorln("Failed to parse cache:", cache, "error:", err)
   238  		return nil
   239  	}
   240  	if cacheURL.Host == "" {
   241  		// Assume the cache is just a hostname
   242  		cacheURL.Host = cacheEndpoint
   243  		cacheURL.Path = ""
   244  		cacheURL.Scheme = ""
   245  		cacheURL.Opaque = ""
   246  	}
   247  	log.Debugf("Parsed Cache: %s\n", cacheURL.String())
   248  	if opts.NeedsToken {
   249  		cacheURL.Scheme = "https"
   250  		if !HasPort(cacheURL.Host) {
   251  			// Add port 8444 and 8443
   252  			cacheURL.Host += ":8444"
   253  			details = append(details, TransferDetails{
   254  				Url:        *cacheURL,
   255  				Proxy:      false,
   256  				PackOption: opts.PackOption,
   257  			})
   258  			// Strip the port off and add 8443
   259  			cacheURL.Host = cacheURL.Host[:len(cacheURL.Host)-5] + ":8443"
   260  		}
   261  		// Whether port is specified or not, add a transfer without proxy
   262  		details = append(details, TransferDetails{
   263  			Url:        *cacheURL,
   264  			Proxy:      false,
   265  			PackOption: opts.PackOption,
   266  		})
   267  	} else {
   268  		cacheURL.Scheme = "http"
   269  		if !HasPort(cacheURL.Host) {
   270  			cacheURL.Host += ":8000"
   271  		}
   272  		isProxyEnabled := IsProxyEnabled()
   273  		details = append(details, TransferDetails{
   274  			Url:        *cacheURL,
   275  			Proxy:      isProxyEnabled,
   276  			PackOption: opts.PackOption,
   277  		})
   278  		if isProxyEnabled && CanDisableProxy() {
   279  			details = append(details, TransferDetails{
   280  				Url:        *cacheURL,
   281  				Proxy:      false,
   282  				PackOption: opts.PackOption,
   283  			})
   284  		}
   285  	}
   286  
   287  	return details
   288  }