github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/registry/remote/auth/challenge.go (about)

     1  /*
     2  Copyright The ORAS Authors.
     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  
    16  package auth
    17  
    18  import (
    19  	"strconv"
    20  	"strings"
    21  )
    22  
    23  // Scheme define the authentication method.
    24  type Scheme byte
    25  
    26  const (
    27  	// SchemeUnknown represents unknown or unsupported schemes
    28  	SchemeUnknown Scheme = iota
    29  
    30  	// SchemeBasic represents the "Basic" HTTP authentication scheme.
    31  	// Reference: https://tools.ietf.org/html/rfc7617
    32  	SchemeBasic
    33  
    34  	// SchemeBearer represents the Bearer token in OAuth 2.0.
    35  	// Reference: https://tools.ietf.org/html/rfc6750
    36  	SchemeBearer
    37  )
    38  
    39  // parseScheme parse the authentication scheme from the given string
    40  // case-insensitively.
    41  func parseScheme(scheme string) Scheme {
    42  	switch {
    43  	case strings.EqualFold(scheme, "basic"):
    44  		return SchemeBasic
    45  	case strings.EqualFold(scheme, "bearer"):
    46  		return SchemeBearer
    47  	}
    48  	return SchemeUnknown
    49  }
    50  
    51  // String return the string for the scheme.
    52  func (s Scheme) String() string {
    53  	switch s {
    54  	case SchemeBasic:
    55  		return "Basic"
    56  	case SchemeBearer:
    57  		return "Bearer"
    58  	}
    59  	return "Unknown"
    60  }
    61  
    62  // parseChallenge parses the "WWW-Authenticate" header returned by the remote
    63  // registry, and extracts parameters if scheme is Bearer.
    64  // References:
    65  // - https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate
    66  // - https://tools.ietf.org/html/rfc7235#section-2.1
    67  func parseChallenge(header string) (scheme Scheme, params map[string]string) {
    68  	// as defined in RFC 7235 section 2.1, we have
    69  	//     challenge   = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
    70  	//     auth-scheme = token
    71  	//     auth-param  = token BWS "=" BWS ( token / quoted-string )
    72  	//
    73  	// since we focus parameters only on Bearer, we have
    74  	//     challenge   = auth-scheme [ 1*SP #auth-param ]
    75  	schemeString, rest := parseToken(header)
    76  	scheme = parseScheme(schemeString)
    77  
    78  	// fast path for non bearer challenge
    79  	if scheme != SchemeBearer {
    80  		return
    81  	}
    82  
    83  	// parse params for bearer auth.
    84  	// combining RFC 7235 section 2.1 with RFC 7230 section 7, we have
    85  	//     #auth-param => auth-param *( OWS "," OWS auth-param )
    86  	var key, value string
    87  	for {
    88  		key, rest = parseToken(skipSpace(rest))
    89  		if key == "" {
    90  			return
    91  		}
    92  
    93  		rest = skipSpace(rest)
    94  		if rest == "" || rest[0] != '=' {
    95  			return
    96  		}
    97  		rest = skipSpace(rest[1:])
    98  		if rest == "" {
    99  			return
   100  		}
   101  
   102  		if rest[0] == '"' {
   103  			prefix, err := strconv.QuotedPrefix(rest)
   104  			if err != nil {
   105  				return
   106  			}
   107  			value, err = strconv.Unquote(prefix)
   108  			if err != nil {
   109  				return
   110  			}
   111  			rest = rest[len(prefix):]
   112  		} else {
   113  			value, rest = parseToken(rest)
   114  			if value == "" {
   115  				return
   116  			}
   117  		}
   118  		if params == nil {
   119  			params = map[string]string{
   120  				key: value,
   121  			}
   122  		} else {
   123  			params[key] = value
   124  		}
   125  
   126  		rest = skipSpace(rest)
   127  		if rest == "" || rest[0] != ',' {
   128  			return
   129  		}
   130  		rest = rest[1:]
   131  	}
   132  }
   133  
   134  // isNotTokenChar reports whether rune is not a `tchar` defined in RFC 7230
   135  // section 3.2.6.
   136  func isNotTokenChar(r rune) bool {
   137  	// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
   138  	//       / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
   139  	//       / DIGIT / ALPHA
   140  	//       ; any VCHAR, except delimiters
   141  	return (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') &&
   142  		(r < '0' || r > '9') && !strings.ContainsRune("!#$%&'*+-.^_`|~", r)
   143  }
   144  
   145  // parseToken finds the next token from the given string. If no token found,
   146  // an empty token is returned and the whole of the input is returned in rest.
   147  // Note: Since token = 1*tchar, empty string is not a valid token.
   148  func parseToken(s string) (token, rest string) {
   149  	if i := strings.IndexFunc(s, isNotTokenChar); i != -1 {
   150  		return s[:i], s[i:]
   151  	}
   152  	return s, ""
   153  }
   154  
   155  // skipSpace skips "bad" whitespace (BWS) defined in RFC 7230 section 3.2.3.
   156  func skipSpace(s string) string {
   157  	// OWS = *( SP / HTAB )
   158  	//     ; optional whitespace
   159  	// BWS = OWS
   160  	//     ; "bad" whitespace
   161  	if i := strings.IndexFunc(s, func(r rune) bool {
   162  		return r != ' ' && r != '\t'
   163  	}); i != -1 {
   164  		return s[i:]
   165  	}
   166  	return s
   167  }