github.com/htcondor/osdf-client/v6@v6.13.0-rc1.0.20231009141709-766e7b4d1dc8/director.go (about)

     1  package stashcp
     2  
     3  import (
     4  	"net/http"
     5  	"net/url"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  
    10  	namespaces "github.com/htcondor/osdf-client/v6/namespaces"
    11  	log "github.com/sirupsen/logrus"
    12  )
    13  
    14  // Simple parser to that takes a "values" string from a header and turns it
    15  // into a map of key/value pairs
    16  func HeaderParser(values string) (retMap map[string]string) {
    17  	retMap = map[string]string{}
    18  
    19  	// Some headers might not have values, such as the
    20  	// X-OSDF-Authorization header when the resource is public
    21  	if values == "" {
    22  		return
    23  	}
    24  
    25  	mapPairs := strings.Split(values, ",")
    26  	for _, pair := range mapPairs {
    27  		// Remove any unwanted spaces
    28  		pair = strings.ReplaceAll(pair, " ", "")
    29  
    30  		// Break out key/value pairs and put in the map
    31  		split := strings.Split(pair, "=")
    32  		retMap[split[0]] = split[1]
    33  	}
    34  
    35  	return retMap
    36  }
    37  
    38  // Given the Director response, create the ordered list of caches
    39  // and store it as namespace.SortedDirectorCaches
    40  func CreateNsFromDirectorResp(dirResp *http.Response, namespace *namespaces.Namespace) (err error) {
    41  	xOsdfNamespace := HeaderParser(dirResp.Header.Values("X-Osdf-Namespace")[0])
    42  	namespace.Path = xOsdfNamespace["namespace"]
    43  	namespace.UseTokenOnRead, _ = strconv.ParseBool(xOsdfNamespace["use-token-on-read"])
    44  	namespace.ReadHTTPS, _ = strconv.ParseBool(xOsdfNamespace["readhttps"])
    45  
    46  	var xOsdfAuthorization map[string]string
    47  	if len(dirResp.Header.Values("X-Osdf-Authorization")) > 0 {
    48  		xOsdfAuthorization = HeaderParser(dirResp.Header.Values("X-Osdf-Authorization")[0])
    49  		namespace.Issuer = xOsdfAuthorization["issuer"]
    50  	}
    51  
    52  	var xOsdfTokenGeneration map[string]string
    53  	if len(dirResp.Header.Values("X-Osdf-Token-Generation")) > 0 {
    54  		xOsdfTokenGeneration = HeaderParser(dirResp.Header.Values("X-Osdf-Token-Generation")[0])
    55  
    56  		// Instantiate the cred gen struct
    57  		namespace.CredentialGen = &namespaces.CredentialGeneration{}
    58  
    59  		// We wind up with a duplicate issuer here as the encapsulating ns also encodes this
    60  		issuer := xOsdfTokenGeneration["issuer"]
    61  		namespace.CredentialGen.Issuer = &issuer
    62  
    63  		base_path := xOsdfTokenGeneration["base-path"]
    64  		namespace.CredentialGen.BasePath = &base_path
    65  
    66  		max_scope_depth, _ := strconv.Atoi(xOsdfTokenGeneration["max-scope-depth"])
    67  		namespace.CredentialGen.MaxScopeDepth = &max_scope_depth
    68  
    69  		strategy := xOsdfTokenGeneration["strategy"]
    70  		namespace.CredentialGen.Strategy = &strategy
    71  		
    72  		// The Director only returns a vault server if the strategy is vault.
    73  		if vs, exists := xOsdfTokenGeneration["vault-server"]; exists {
    74  			namespace.CredentialGen.VaultServer = &vs
    75  		}
    76  	}
    77  
    78  	// Create the caches slice
    79  	namespace.SortedDirectorCaches, err = GetCachesFromDirectorResponse(dirResp, namespace.UseTokenOnRead || namespace.ReadHTTPS)
    80  	if err != nil {
    81  		log.Errorln("Unable to construct ordered cache list:", err)
    82  		return
    83  	}
    84  	log.Debugln("Namespace path constructed from Director:", namespace.Path)
    85  
    86  	return
    87  }
    88  
    89  func QueryDirector(source string, directorUrl string) (resp *http.Response, err error) {
    90  	resourceUrl := directorUrl + source
    91  
    92  	// Prevent following the Director's redirect
    93  	client := &http.Client{
    94  		CheckRedirect: func(req *http.Request, via []*http.Request) error {
    95  			return http.ErrUseLastResponse
    96  		},
    97  	}
    98  
    99  	log.Debugln("Querying OSDF Director at", resourceUrl)
   100  	resp, err = client.Get(resourceUrl)
   101  	log.Debugln("Director's response:", resp)
   102  
   103  	if err != nil {
   104  		log.Errorln("Failed to get response from OSDF Director:", err)
   105  		return
   106  	}
   107  
   108  	defer resp.Body.Close()
   109  	return
   110  }
   111  
   112  func GetCachesFromDirectorResponse(resp *http.Response, needsToken bool) (caches []namespaces.DirectorCache, err error) {
   113  	// Get the Link header
   114  	linkHeader := resp.Header.Values("Link")
   115  
   116  	for _, linksStr := range strings.Split(linkHeader[0], ",") {
   117  		links := strings.Split(strings.ReplaceAll(linksStr, " ", ""), ";")
   118  
   119  		var endpoint string
   120  		// var rel string // "rel", as defined in the Metalink/HTTP RFC. Currently not being used by
   121  		// the OSDF Client, but is provided by the director. Will be useful in the future when 
   122  		// we start looking at cases where we want to duplicate from caches if we're throttling
   123  		// connections to the origin.
   124  		var pri int
   125  		for _, val := range links {
   126  			if strings.HasPrefix(val, "<") {
   127  				endpoint = val[1 : len(val)-1]
   128  			} else if strings.HasPrefix(val, "pri") {
   129  				pri, _ = strconv.Atoi(val[4:])
   130  			}
   131  			// } else if strings.HasPrefix(val, "rel") {
   132  			// 	rel = val[5 : len(val)-1]
   133  			// }
   134  		}
   135  
   136  		// Construct the cache objects, getting endpoint and auth requirements from
   137  		// Director
   138  		var cache namespaces.DirectorCache
   139  		cache.AuthedReq = needsToken
   140  		cache.EndpointUrl = endpoint
   141  		cache.Priority = pri
   142  		caches = append(caches, cache)
   143  	}
   144  
   145  	// Making the assumption that the Link header doesn't already provide the caches
   146  	// in order (even though it probably does). This sorts the caches and ensures
   147  	// we're using the "pri" tag to order them
   148  	sort.Slice(caches, func(i, j int) bool {
   149  		val1 := caches[i].Priority
   150  		val2 := caches[j].Priority
   151  		return val1 < val2
   152  	})
   153  
   154  	return caches, err
   155  }
   156  
   157  // NewTransferDetails creates the TransferDetails struct with the given cache
   158  func NewTransferDetailsUsingDirector(cache namespaces.DirectorCache, https bool) []TransferDetails {
   159  	details := make([]TransferDetails, 0)
   160  	cacheEndpoint := cache.EndpointUrl
   161  
   162  	// Form the URL
   163  	cacheURL, err := url.Parse(cacheEndpoint)
   164  	if err != nil {
   165  		log.Errorln("Failed to parse cache:", cache, "error:", err)
   166  		return nil
   167  	}
   168  	if cacheURL.Host == "" {
   169  		// Assume the cache is just a hostname
   170  		cacheURL.Host = cacheEndpoint
   171  		cacheURL.Path = ""
   172  		cacheURL.Scheme = ""
   173  		cacheURL.Opaque = ""
   174  	}
   175  	log.Debugf("Parsed Cache: %s\n", cacheURL.String())
   176  	if https {
   177  		cacheURL.Scheme = "https"
   178  		if !HasPort(cacheURL.Host) {
   179  			// Add port 8444 and 8443
   180  			cacheURL.Host += ":8444"
   181  			details = append(details, TransferDetails{
   182  				Url:   *cacheURL,
   183  				Proxy: false,
   184  			})
   185  			// Strip the port off and add 8443
   186  			cacheURL.Host = cacheURL.Host[:len(cacheURL.Host)-5] + ":8443"
   187  		}
   188  		// Whether port is specified or not, add a transfer without proxy
   189  		details = append(details, TransferDetails{
   190  			Url:   *cacheURL,
   191  			Proxy: false,
   192  		})
   193  	} else {
   194  		cacheURL.Scheme = "http"
   195  		if !HasPort(cacheURL.Host) {
   196  			cacheURL.Host += ":8000"
   197  		}
   198  		isProxyEnabled := IsProxyEnabled()
   199  		details = append(details, TransferDetails{
   200  			Url:   *cacheURL,
   201  			Proxy: isProxyEnabled,
   202  		})
   203  		if isProxyEnabled && CanDisableProxy() {
   204  			details = append(details, TransferDetails{
   205  				Url:   *cacheURL,
   206  				Proxy: false,
   207  			})
   208  		}
   209  	}
   210  
   211  	return details
   212  }