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

This commit is contained in:
upstream-mirror
2026-09-16 01:08:34 +05:00
commit 9456a4e922
2560 changed files with 120911 additions and 0 deletions

24
oryx/httpx/assert.go Normal file
View 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
}

View 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
View 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")),
}
}

View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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...)...)
}