snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
24
oryx/httpx/assert.go
Normal file
24
oryx/httpx/assert.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetResponseMeta(w http.ResponseWriter) (status, size int) {
|
||||
switch t := w.(type) {
|
||||
case interface{ Status() int }:
|
||||
status = t.Status()
|
||||
}
|
||||
|
||||
switch t := w.(type) {
|
||||
case interface{ Size() int }:
|
||||
size = t.Size()
|
||||
case interface{ Written() int64 }:
|
||||
size = int(t.Written())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
21
oryx/httpx/chan_handler.go
Normal file
21
oryx/httpx/chan_handler.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import "net/http"
|
||||
|
||||
type chanHandler <-chan http.HandlerFunc
|
||||
|
||||
var _ http.Handler = chanHandler(nil)
|
||||
|
||||
func (c chanHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
(<-c)(w, r)
|
||||
}
|
||||
|
||||
// NewChanHandler returns a new handler and corresponding channel for sending handler funcs.
|
||||
// Useful for testing. The argument buf specifies the channel capacity, so pass 0 for a sync handler.
|
||||
func NewChanHandler(buf int) (http.Handler, chan<- http.HandlerFunc) {
|
||||
c := make(chan http.HandlerFunc, buf)
|
||||
return chanHandler(c), c
|
||||
}
|
||||
72
oryx/httpx/client_info.go
Normal file
72
oryx/httpx/client_info.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GeoLocation struct {
|
||||
City string
|
||||
Region string
|
||||
Country string
|
||||
Latitude *float64
|
||||
Longitude *float64
|
||||
}
|
||||
|
||||
func GetClientIPAddressesWithoutInternalIPs(ipAddresses []string) (string, error) {
|
||||
var res string
|
||||
|
||||
for i := len(ipAddresses) - 1; i >= 0; i-- {
|
||||
ip := strings.TrimSpace(ipAddresses[i])
|
||||
|
||||
if !net.ParseIP(ip).IsPrivate() {
|
||||
res = ip
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func ClientIP(r *http.Request) string {
|
||||
if trueClientIP := r.Header.Get("True-Client-IP"); trueClientIP != "" {
|
||||
return trueClientIP
|
||||
} else if cfConnectingIP := r.Header.Get("Cf-Connecting-IP"); cfConnectingIP != "" {
|
||||
return cfConnectingIP
|
||||
} else if realClientIP := r.Header.Get("X-Real-IP"); realClientIP != "" {
|
||||
return realClientIP
|
||||
} else if forwardedIP := r.Header.Get("X-Forwarded-For"); forwardedIP != "" {
|
||||
ip, _ := GetClientIPAddressesWithoutInternalIPs(strings.Split(forwardedIP, ","))
|
||||
return ip
|
||||
} else {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
}
|
||||
|
||||
func parseFloatHeaderValue(headerValue string) *float64 {
|
||||
if headerValue == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
val, err := strconv.ParseFloat(headerValue, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &val
|
||||
}
|
||||
|
||||
func ClientGeoLocation(r *http.Request) *GeoLocation {
|
||||
return &GeoLocation{
|
||||
City: r.Header.Get("Cf-Ipcity"),
|
||||
Region: r.Header.Get("Cf-Region-Code"),
|
||||
Country: r.Header.Get("Cf-Ipcountry"),
|
||||
Longitude: parseFloatHeaderValue(r.Header.Get("Cf-Iplongitude")),
|
||||
Latitude: parseFloatHeaderValue(r.Header.Get("Cf-Iplatitude")),
|
||||
}
|
||||
}
|
||||
28
oryx/httpx/content_type.go
Normal file
28
oryx/httpx/content_type.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HasContentType determines whether the request `content-type` includes a
|
||||
// server-acceptable mime-type
|
||||
//
|
||||
// Failure should yield an HTTP 415 (`http.StatusUnsupportedMediaType`)
|
||||
func HasContentType(r *http.Request, mimetypes ...string) bool {
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
return slices.Contains(mimetypes, "application/octet-stream")
|
||||
}
|
||||
|
||||
mediaType, _, err := mime.ParseMediaType(strings.TrimSpace(contentType))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return slices.Contains(mimetypes, mediaType)
|
||||
}
|
||||
50
oryx/httpx/gzip_server.go
Normal file
50
oryx/httpx/gzip_server.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CompressionRequestReader struct {
|
||||
ErrHandler func(w http.ResponseWriter, r *http.Request, err error)
|
||||
}
|
||||
|
||||
func defaultCompressionErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func NewCompressionRequestReader(eh func(w http.ResponseWriter, r *http.Request, err error)) *CompressionRequestReader {
|
||||
if eh == nil {
|
||||
eh = defaultCompressionErrorHandler
|
||||
}
|
||||
|
||||
return &CompressionRequestReader{
|
||||
ErrHandler: eh,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CompressionRequestReader) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
for _, enc := range strings.Split(r.Header.Get("Content-Encoding"), ",") {
|
||||
switch enc = strings.TrimSpace(enc); enc {
|
||||
case "gzip":
|
||||
reader, err := gzip.NewReader(r.Body)
|
||||
if err != nil {
|
||||
c.ErrHandler(w, r, err)
|
||||
return
|
||||
}
|
||||
r.Body = io.NopCloser(reader)
|
||||
case "identity", "":
|
||||
// nothing to do
|
||||
default:
|
||||
c.ErrHandler(w, r, fmt.Errorf("%s content encoding not supported", enc))
|
||||
}
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
51
oryx/httpx/request.go
Normal file
51
oryx/httpx/request.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// NewRequestJSON returns a new JSON *http.Request.
|
||||
func NewRequestJSON(method, url string, data interface{}) (*http.Request, error) {
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(data); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
req, err := http.NewRequest(method, url, &b)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewRequestForm returns a new POST Form *http.Request.
|
||||
func NewRequestForm(method, url string, data url.Values) (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, url, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// MustNewRequest returns a new *http.Request or fatals.
|
||||
func MustNewRequest(method, url string, body io.Reader, contentType string) *http.Request {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
return req
|
||||
}
|
||||
140
oryx/httpx/resilient_client.go
Normal file
140
oryx/httpx/resilient_client.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
"github.com/ory/x/logrusx"
|
||||
)
|
||||
|
||||
type resilientOptions struct {
|
||||
c *http.Client
|
||||
l interface{}
|
||||
retryWaitMin time.Duration
|
||||
retryWaitMax time.Duration
|
||||
retryMax int
|
||||
noInternalIPs bool
|
||||
internalIPExceptions []string
|
||||
}
|
||||
|
||||
func newResilientOptions() *resilientOptions {
|
||||
return &resilientOptions{
|
||||
c: &http.Client{Timeout: time.Minute},
|
||||
retryWaitMin: 1 * time.Second,
|
||||
retryWaitMax: 30 * time.Second,
|
||||
retryMax: 4,
|
||||
l: log.New(io.Discard, "", log.LstdFlags),
|
||||
}
|
||||
}
|
||||
|
||||
// ResilientOptions is a set of options for the ResilientClient.
|
||||
type ResilientOptions func(o *resilientOptions)
|
||||
|
||||
// ResilientClientWithMaxRetry sets the maximum number of retries.
|
||||
func ResilientClientWithMaxRetry(retryMax int) ResilientOptions {
|
||||
return func(o *resilientOptions) { o.retryMax = retryMax }
|
||||
}
|
||||
|
||||
// ResilientClientWithMinRetryWait sets the minimum wait time between retries.
|
||||
func ResilientClientWithMinRetryWait(retryWaitMin time.Duration) ResilientOptions {
|
||||
return func(o *resilientOptions) { o.retryWaitMin = retryWaitMin }
|
||||
}
|
||||
|
||||
// ResilientClientWithMaxRetryWait sets the maximum wait time for a retry.
|
||||
func ResilientClientWithMaxRetryWait(retryWaitMax time.Duration) ResilientOptions {
|
||||
return func(o *resilientOptions) { o.retryWaitMax = retryWaitMax }
|
||||
}
|
||||
|
||||
// ResilientClientWithConnectionTimeout sets the connection timeout for the client.
|
||||
func ResilientClientWithConnectionTimeout(connTimeout time.Duration) ResilientOptions {
|
||||
return func(o *resilientOptions) { o.c.Timeout = connTimeout }
|
||||
}
|
||||
|
||||
// ResilientClientWithLogger sets the logger to be used by the client.
|
||||
func ResilientClientWithLogger(l *logrusx.Logger) ResilientOptions {
|
||||
return func(o *resilientOptions) { o.l = l }
|
||||
}
|
||||
|
||||
// ResilientClientDisallowInternalIPs disallows internal IPs from being used.
|
||||
func ResilientClientDisallowInternalIPs() ResilientOptions {
|
||||
return func(o *resilientOptions) { o.noInternalIPs = true }
|
||||
}
|
||||
|
||||
// ResilientClientAllowInternalIPRequestsTo allows requests to the glob-matching URLs even
|
||||
// if they are internal IPs.
|
||||
func ResilientClientAllowInternalIPRequestsTo(urlGlobs ...string) ResilientOptions {
|
||||
return func(o *resilientOptions) { o.internalIPExceptions = urlGlobs }
|
||||
}
|
||||
|
||||
// ResilientClientNoFollowRedirects configures the client to not follow redirects.
|
||||
func ResilientClientNoFollowRedirects() ResilientOptions {
|
||||
return func(o *resilientOptions) {
|
||||
o.c.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse // don't follow redirects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewResilientClient creates a new ResilientClient.
|
||||
func NewResilientClient(opts ...ResilientOptions) *retryablehttp.Client {
|
||||
o := newResilientOptions()
|
||||
for _, f := range opts {
|
||||
f(o)
|
||||
}
|
||||
|
||||
if o.noInternalIPs {
|
||||
o.c.Transport = &noInternalIPRoundTripper{
|
||||
onWhitelist: allowInternalAllowIPv6,
|
||||
notOnWhitelist: prohibitInternalAllowIPv6,
|
||||
internalIPExceptions: o.internalIPExceptions,
|
||||
}
|
||||
} else {
|
||||
o.c.Transport = allowInternalAllowIPv6
|
||||
}
|
||||
|
||||
cl := retryablehttp.NewClient()
|
||||
cl.HTTPClient = o.c
|
||||
cl.Logger = o.l
|
||||
cl.RetryWaitMin = o.retryWaitMin
|
||||
cl.RetryWaitMax = o.retryWaitMax
|
||||
cl.RetryMax = o.retryMax
|
||||
cl.CheckRetry = retryablehttp.DefaultRetryPolicy
|
||||
cl.Backoff = retryablehttp.DefaultBackoff
|
||||
return cl
|
||||
}
|
||||
|
||||
// SetOAuth2 modifies the given client to enable OAuth2 authentication. Requests
|
||||
// with the client should always use the returned context.
|
||||
//
|
||||
// client := http.NewResilientClient(opts...)
|
||||
// ctx, client = httpx.SetOAuth2(ctx, client, oauth2Config, oauth2Token)
|
||||
// req, err := retryablehttp.NewRequestWithContext(ctx, ...)
|
||||
// if err != nil { /* ... */ }
|
||||
// res, err := client.Do(req)
|
||||
func SetOAuth2(ctx context.Context, cl *retryablehttp.Client, c OAuth2Config, t *oauth2.Token) (context.Context, *retryablehttp.Client) {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, cl.HTTPClient)
|
||||
cl.HTTPClient = c.Client(ctx, t)
|
||||
return ctx, cl
|
||||
}
|
||||
|
||||
type (
|
||||
OAuth2Config interface {
|
||||
Client(context.Context, *oauth2.Token) *http.Client
|
||||
}
|
||||
ClientProvider interface {
|
||||
HTTPClient(ctx context.Context, opts ...ResilientOptions) *retryablehttp.Client
|
||||
}
|
||||
WriterProvider interface {
|
||||
Writer() herodot.Writer
|
||||
}
|
||||
)
|
||||
67
oryx/httpx/ssrf.go
Normal file
67
oryx/httpx/ssrf.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
|
||||
"github.com/ory/x/ipx"
|
||||
)
|
||||
|
||||
var _ http.RoundTripper = (*noInternalIPRoundTripper)(nil)
|
||||
|
||||
type noInternalIPRoundTripper struct {
|
||||
onWhitelist, notOnWhitelist http.RoundTripper
|
||||
internalIPExceptions []string
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper.
|
||||
func (n noInternalIPRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
incoming := IncomingRequestURL(request)
|
||||
incoming.RawQuery = ""
|
||||
incoming.RawFragment = ""
|
||||
for _, exception := range n.internalIPExceptions {
|
||||
compiled, err := glob.Compile(exception, '.', '/')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if compiled.Match(incoming.String()) {
|
||||
return n.onWhitelist.RoundTrip(request)
|
||||
}
|
||||
}
|
||||
|
||||
return n.notOnWhitelist.RoundTrip(request)
|
||||
}
|
||||
|
||||
var (
|
||||
prohibitInternalAllowIPv6 http.RoundTripper = OTELTraceTransport(ssrfTransport(ipx.ProhibitInternalDialFunc))
|
||||
allowInternalAllowIPv6 http.RoundTripper = OTELTraceTransport(ssrfTransport(ipx.AllowInternalDialFunc))
|
||||
)
|
||||
|
||||
func ssrfTransport(dial func(ctx context.Context, network, address string) (net.Conn, error)) *http.Transport {
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: dial,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// OTELTraceTransport wraps the given http.Transport with OpenTelemetry instrumentation.
|
||||
func OTELTraceTransport(t *http.Transport) http.RoundTripper {
|
||||
return otelhttp.NewTransport(t, otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
|
||||
return otelhttptrace.NewClientTrace(ctx, otelhttptrace.WithoutHeaders(), otelhttptrace.WithoutSubSpans())
|
||||
}))
|
||||
}
|
||||
68
oryx/httpx/transports.go
Normal file
68
oryx/httpx/transports.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import "net/http"
|
||||
|
||||
// WrapTransportWithHeader wraps a http.Transport to always use the values from the given header.
|
||||
func WrapTransportWithHeader(parent http.RoundTripper, h http.Header) *TransportWithHeader {
|
||||
return &TransportWithHeader{
|
||||
RoundTripper: parent,
|
||||
h: h,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTransportWithHeader returns a new http.Transport that always uses the values from the given header.
|
||||
func NewTransportWithHeader(h http.Header) *TransportWithHeader {
|
||||
return &TransportWithHeader{
|
||||
RoundTripper: http.DefaultTransport,
|
||||
h: h,
|
||||
}
|
||||
}
|
||||
|
||||
// TransportWithHeader is an http.RoundTripper that always uses the values from the given header.
|
||||
type TransportWithHeader struct {
|
||||
http.RoundTripper
|
||||
h http.Header
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper.
|
||||
func (ct *TransportWithHeader) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
for k := range ct.h {
|
||||
req.Header.Set(k, ct.h.Get(k))
|
||||
}
|
||||
return ct.RoundTripper.RoundTrip(req)
|
||||
}
|
||||
|
||||
// NewTransportWithHost returns a new http.Transport that always uses the given host.
|
||||
func NewTransportWithHost(host string) *TransportWithHost {
|
||||
return &TransportWithHost{
|
||||
RoundTripper: http.DefaultTransport,
|
||||
host: host,
|
||||
}
|
||||
}
|
||||
|
||||
// WrapRoundTripperWithHost wraps a http.RoundTripper that always uses the given host.
|
||||
func WrapRoundTripperWithHost(parent http.RoundTripper, host string) *TransportWithHost {
|
||||
return &TransportWithHost{
|
||||
RoundTripper: parent,
|
||||
host: host,
|
||||
}
|
||||
}
|
||||
|
||||
// TransportWithHost is an http.RoundTripper that always uses the given host.
|
||||
type TransportWithHost struct {
|
||||
http.RoundTripper
|
||||
host string
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper.
|
||||
func (ct *TransportWithHost) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Host = ct.host
|
||||
return ct.RoundTripper.RoundTrip(req)
|
||||
}
|
||||
|
||||
type TransportFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f TransportFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }
|
||||
29
oryx/httpx/url.go
Normal file
29
oryx/httpx/url.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// IncomingRequestURL returns the URL of the incoming HTTP request by looking at the host, TLS, and X-Forwarded-* headers.
|
||||
func IncomingRequestURL(r *http.Request) *url.URL {
|
||||
source := *r.URL
|
||||
source.Host = cmp.Or(source.Host, r.Header.Get("X-Forwarded-Host"), r.Host)
|
||||
|
||||
if proto := r.Header.Get("X-Forwarded-Proto"); len(proto) > 0 {
|
||||
source.Scheme = proto
|
||||
}
|
||||
|
||||
if source.Scheme == "" {
|
||||
source.Scheme = "https"
|
||||
if r.TLS == nil {
|
||||
source.Scheme = "http"
|
||||
}
|
||||
}
|
||||
|
||||
return &source
|
||||
}
|
||||
55
oryx/httpx/wait_for.go
Normal file
55
oryx/httpx/wait_for.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// WaitForEndpoint waits for the endpoint to be available.
|
||||
func WaitForEndpoint(ctx context.Context, endpoint string, opts ...retry.Option) error {
|
||||
return WaitForEndpointWithClient(ctx, http.DefaultClient, endpoint, opts...)
|
||||
}
|
||||
|
||||
// WaitForEndpointWithClient waits for the endpoint to be available while using the given http.Client.
|
||||
func WaitForEndpointWithClient(ctx context.Context, client *http.Client, endpoint string, opts ...retry.Option) error {
|
||||
return retry.Do(func() error {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gjson.GetBytes(body, "status").String() != "ok" {
|
||||
return errors.Errorf("status is not yet ok: %s", body)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
append([]retry.Option{
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.Delay(time.Second),
|
||||
retry.MaxDelay(time.Second * 2),
|
||||
retry.Attempts(20),
|
||||
}, opts...)...)
|
||||
}
|
||||
Reference in New Issue
Block a user