git.iko.soy/typeable/oauth2-proxy
on master help sign in
257 lines 6.6 kB history raw
1package util
3import (
4 "crypto/rand"
5 "crypto/rsa"
6 "crypto/x509"
7 "crypto/x509/pkix"
8 "encoding/json"
9 "fmt"
10 "math/big"
11 "net"
12 "net/url"
13 "os"
14 "strings"
15 "time"
17 "github.com/spf13/cast"
20func GetCertPool(paths []string, useSystemPool bool) (*x509.CertPool, error) {
21 if len(paths) == 0 {
22 return nil, fmt.Errorf("invalid empty list of Root CAs file paths")
23 }
25 var pool *x509.CertPool
26 if useSystemPool {
27 rootPool, err := getSystemCertPool()
28 if err != nil {
29 return nil, fmt.Errorf("unable to get SystemCertPool when append is true - #{err}")
30 }
31 pool = rootPool
32 } else {
33 pool = x509.NewCertPool()
34 }
36 return loadCertsFromPaths(paths, pool)
40func getSystemCertPool() (*x509.CertPool, error) {
41 rootPool, err := x509.SystemCertPool()
42 if err != nil {
43 return nil, err
44 }
46 if rootPool == nil {
47 return nil, fmt.Errorf("SystemCertPool is empty")
48 }
50 return rootPool, nil
53func loadCertsFromPaths(paths []string, pool *x509.CertPool) (*x509.CertPool, error) {
54 for _, path := range paths {
55 // Cert paths are a configurable option
56 data, err := os.ReadFile(path) // #nosec G304
57 if err != nil {
58 return nil, fmt.Errorf("certificate authority file (%s) could not be read - %s", path, err)
59 }
60 if !pool.AppendCertsFromPEM(data) {
61 return nil, fmt.Errorf("loading certificate authority (%s) failed", path)
62 }
63 }
64 return pool, nil
67// https://golang.org/src/crypto/tls/generate_cert.go as a function
68func GenerateCert(ipaddr string) ([]byte, []byte, error) {
69 var err error
71 priv, err := rsa.GenerateKey(rand.Reader, 2048)
72 if err != nil {
73 return nil, nil, err
74 }
76 keyBytes, err := x509.MarshalPKCS8PrivateKey(priv)
77 if err != nil {
78 return nil, keyBytes, err
79 }
81 serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
82 if err != nil {
83 return nil, keyBytes, err
84 }
86 notBefore := time.Now()
87 template := x509.Certificate{
88 SerialNumber: serialNumber,
89 Subject: pkix.Name{
90 Organization: []string{"OAuth2 Proxy Test Suite"},
91 },
92 NotBefore: notBefore,
93 NotAfter: notBefore.Add(time.Hour),
94 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
96 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
98 IPAddresses: []net.IP{net.ParseIP(ipaddr)},
99 }
100 certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
101 return certBytes, keyBytes, err
104// SplitHostPort separates host and port. If the port is not valid, it returns
105// the entire input as host, and it doesn't check the validity of the host.
106// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
107// *** taken from net/url, modified validOptionalPort() to accept ":*"
108func SplitHostPort(hostport string) (host, port string) {
109 host = hostport
111 colon := strings.LastIndexByte(host, ':')
112 if colon != -1 && validOptionalPort(host[colon:]) {
113 host, port = host[:colon], host[colon+1:]
114 }
116 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
117 host = host[1 : len(host)-1]
118 }
120 return
123// validOptionalPort reports whether port is either an empty string
124// or matches /^:\d*$/
125// *** taken from net/url, modified to accept ":*"
126func validOptionalPort(port string) bool {
127 if port == "" || port == ":*" {
128 return true
129 }
130 if port[0] != ':' {
131 return false
132 }
133 for _, b := range port[1:] {
134 if b < '0' || b > '9' {
135 return false
136 }
137 }
138 return true
141// IsEndpointAllowed checks whether the endpoint URL is allowed based
142// on an allowed domains list.
143func IsEndpointAllowed(endpoint *url.URL, allowedDomains []string) bool {
144 hostname := endpoint.Hostname()
146 for _, allowedDomain := range allowedDomains {
147 allowedHost, allowedPort := SplitHostPort(allowedDomain)
148 if allowedHost == "" {
149 continue
150 }
152 if isHostnameAllowed(hostname, allowedHost) {
153 // the domain names match, now validate the ports
154 // if the allowed domain's port is '*', allow all ports
155 // if the allowed domain contains a specific port, only allow that port
156 // if the allowed domain doesn't contain a port at all, only allow empty redirect ports ie http and https
157 redirectPort := endpoint.Port()
158 if allowedPort == "*" ||
159 allowedPort == redirectPort ||
160 (allowedPort == "" && redirectPort == "") {
161 return true
162 }
163 }
164 }
166 return false
169func isHostnameAllowed(hostname, allowedHost string) bool {
170 // check if we have a perfect match between hostname and allowedHost
171 if hostname == strings.TrimPrefix(allowedHost, ".") ||
172 hostname == strings.TrimPrefix(allowedHost, "*.") {
173 return true
174 }
176 // check if hostname is a sub domain of the allowedHost
177 if (strings.HasPrefix(allowedHost, ".") && strings.HasSuffix(hostname, allowedHost)) ||
178 (strings.HasPrefix(allowedHost, "*.") && strings.HasSuffix(hostname, allowedHost[1:])) {
179 return true
180 }
182 return false
185// RemoveDuplicateStr removes duplicates from a slice of strings.
186func RemoveDuplicateStr(strSlice []string) []string {
187 allKeys := make(map[string]struct{})
188 var list []string
189 for _, item := range strSlice {
190 if _, ok := allKeys[item]; !ok {
191 allKeys[item] = struct{}{}
192 list = append(list, item)
193 }
194 }
195 return list
198// CoerceClaim tries to convert the value into the destination interface type.
199// If it can convert the value, it will then store the value in the destination
200// interface.
201func CoerceClaim(value, dst any) error {
202 switch d := dst.(type) {
203 case *string:
204 str, err := toString(value)
205 if err != nil {
206 return fmt.Errorf("could not convert value to string: %v", err)
207 }
208 *d = str
209 case *[]string:
210 strSlice, err := toStringSlice(value)
211 if err != nil {
212 return fmt.Errorf("could not convert value to string slice: %v", err)
213 }
214 *d = strSlice
215 case *bool:
216 *d = cast.ToBool(value)
217 default:
218 return fmt.Errorf("unknown type for destination: %T", dst)
219 }
220 return nil
223// toStringSlice converts an interface (either a slice or single value) into
224// a slice of strings.
225func toStringSlice(value any) ([]string, error) {
226 var sliceValues []any
227 switch v := value.(type) {
228 case []any:
229 sliceValues = v
230 default:
231 sliceValues = []any{v}
232 }
234 out := []string{}
235 for _, v := range sliceValues {
236 str, err := toString(v)
237 if err != nil {
238 return nil, fmt.Errorf("could not convert slice entry to string %v: %v", v, err)
239 }
240 out = append(out, str)
241 }
242 return out, nil
245// toString coerces a value into a string.
246// If it is non-string, marshal it into JSON.
247func toString(value any) (string, error) {
248 if str, err := cast.ToStringE(value); err == nil {
249 return str, nil
250 }
252 jsonStr, err := json.Marshal(value)
253 if err != nil {
254 return "", err
255 }
256 return string(jsonStr), nil