...

Source file src/oras.land/oras-go/pkg/registry/remote/auth/challenge.go

Documentation: oras.land/oras-go/pkg/registry/remote/auth

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

View as plain text