github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/cmd/authn/aisreq.go (about)

     1  // Package authn is authentication server for AIStore.
     2  /*
     3   * Copyright (c) 2018-2023, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/NVIDIA/aistore/api/apc"
    16  	"github.com/NVIDIA/aistore/api/authn"
    17  	"github.com/NVIDIA/aistore/cmn"
    18  	"github.com/NVIDIA/aistore/cmn/cos"
    19  	"github.com/NVIDIA/aistore/cmn/nlog"
    20  	jsoniter "github.com/json-iterator/go"
    21  )
    22  
    23  const (
    24  	retryCount = 4
    25  	retrySleep = 3 * time.Second
    26  	retry503   = time.Minute
    27  )
    28  
    29  func (m *mgr) validateSecret(clu *authn.CluACL) (err error) {
    30  	const tag = "validate-secret"
    31  	var (
    32  		secret = Conf.Secret()
    33  		cksum  = cos.NewCksumHash(cos.ChecksumSHA256)
    34  	)
    35  	cksum.H.Write([]byte(secret))
    36  	cksum.Finalize()
    37  
    38  	body := cos.MustMarshal(&authn.ServerConf{Secret: cksum.Val()})
    39  	for _, u := range clu.URLs {
    40  		if err = m.call(http.MethodPost, u, apc.Tokens, body, tag); err == nil {
    41  			return
    42  		}
    43  		err = fmt.Errorf("failed to %s with %s: %v", tag, clu, err)
    44  	}
    45  	return
    46  }
    47  
    48  // update list of revoked token on all clusters
    49  func (m *mgr) broadcastRevoked(token string) {
    50  	tokenList := authn.TokenList{Tokens: []string{token}}
    51  	body := cos.MustMarshal(tokenList)
    52  	m.broadcast(http.MethodDelete, apc.Tokens, body, "broadcast-revoked")
    53  }
    54  
    55  // broadcast the request to all clusters. If a cluster has a few URLS,
    56  // it sends to the first working one. Clusters are processed in parallel.
    57  func (m *mgr) broadcast(method, path string, body []byte, tag string) {
    58  	clus, err := m.clus()
    59  	if err != nil {
    60  		nlog.Errorf("Failed to read cluster list: %v", err)
    61  		return
    62  	}
    63  	wg := &sync.WaitGroup{}
    64  	for _, clu := range clus {
    65  		wg.Add(1)
    66  		go func(clu *authn.CluACL) {
    67  			var err error
    68  			for _, u := range clu.URLs {
    69  				if err = m.call(method, u, path, body, tag); err == nil {
    70  					break
    71  				}
    72  			}
    73  			if err != nil {
    74  				nlog.Errorf("failed to %s with %s: %v", tag, clu, err)
    75  			}
    76  			wg.Done()
    77  		}(clu)
    78  	}
    79  	wg.Wait()
    80  }
    81  
    82  // Send valid and non-expired revoked token list to a cluster.
    83  func (m *mgr) syncTokenList(clu *authn.CluACL) {
    84  	const tag = "sync-tokens"
    85  	tokenList, err := m.generateRevokedTokenList()
    86  	if err != nil {
    87  		nlog.Errorf("failed to sync token list with %q(%q): %v", clu.ID, clu.Alias, err)
    88  		return
    89  	}
    90  	if len(tokenList) == 0 {
    91  		return
    92  	}
    93  	body := cos.MustMarshal(authn.TokenList{Tokens: tokenList})
    94  	for _, u := range clu.URLs {
    95  		if err = m.call(http.MethodDelete, u, apc.Tokens, body, tag); err == nil {
    96  			break
    97  		}
    98  		err = fmt.Errorf("failed to %s with %s: %v", tag, clu, err)
    99  	}
   100  	if err != nil {
   101  		nlog.Errorln(err)
   102  	}
   103  }
   104  
   105  // TODO: reuse api/client.go reqParams.do()
   106  func (m *mgr) call(method, proxyURL, path string, injson []byte, tag string) error {
   107  	var (
   108  		rerr    error
   109  		msg     []byte
   110  		retries = retryCount
   111  		sleep   = retrySleep
   112  		url     = proxyURL + cos.JoinWords(apc.Version, path)
   113  		client  = m.clientH
   114  	)
   115  	if cos.IsHTTPS(proxyURL) {
   116  		client = m.clientTLS
   117  	}
   118  	// while cos.IsRetriableConnErr()
   119  	for i := 1; i <= retries; i++ {
   120  		req, nerr := http.NewRequest(method, url, bytes.NewBuffer(injson))
   121  		if nerr != nil {
   122  			return nerr
   123  		}
   124  		req.Header.Set(cos.HdrContentType, cos.ContentJSON)
   125  		resp, err := client.Do(req)
   126  		if resp != nil {
   127  			if resp.Body != nil {
   128  				msg, _ = io.ReadAll(resp.Body)
   129  				resp.Body.Close()
   130  			}
   131  		}
   132  		if err == nil {
   133  			if resp.StatusCode < http.StatusBadRequest {
   134  				return nil
   135  			}
   136  		} else {
   137  			if cos.IsRetriableConnErr(err) {
   138  				continue
   139  			}
   140  			if resp == nil {
   141  				return err
   142  			}
   143  		}
   144  		if resp.StatusCode == http.StatusServiceUnavailable {
   145  			if retries == retryCount {
   146  				retries = int(retry503 / retrySleep)
   147  			}
   148  		} else {
   149  			var herr *cmn.ErrHTTP
   150  			if jsonErr := jsoniter.Unmarshal(msg, &herr); jsonErr == nil {
   151  				return herr
   152  			}
   153  		}
   154  		if i < retries {
   155  			nlog.Warningf("failed to %q %s: %v - retrying...", tag, url, err)
   156  			time.Sleep(sleep)
   157  			if i > retries/2+1 && sleep == retrySleep {
   158  				sleep *= 2
   159  			}
   160  		}
   161  		rerr = err
   162  	}
   163  	return rerr
   164  }