snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (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-15 21:19:12 +05:00
commit c60cc7ca75
8092 changed files with 394160 additions and 0 deletions

321
oryx/proxy/proxy.go Normal file
View File

@@ -0,0 +1,321 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package proxy
import (
"context"
"log"
"net/http"
"net/http/httputil"
"github.com/pkg/errors"
"github.com/rs/cors"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
type (
RespMiddleware func(resp *http.Response, config *HostConfig, body []byte) ([]byte, error)
ReqMiddleware func(req *httputil.ProxyRequest, config *HostConfig, body []byte) ([]byte, error)
HostMapper func(ctx context.Context, r *http.Request) (context.Context, *HostConfig, error)
options struct {
hostMapper HostMapper
onResError func(*http.Response, error) error
onReqError func(*http.Request, error)
respMiddlewares []RespMiddleware
reqMiddlewares []ReqMiddleware
transport http.RoundTripper
errHandler func(http.ResponseWriter, *http.Request, error)
}
HostConfig struct {
// CorsEnabled is a flag to enable or disable CORS
// Default: false
CorsEnabled bool
// CorsOptions allows to configure CORS
// If left empty, no CORS headers will be set even when CorsEnabled is true
CorsOptions *cors.Options
// CookieDomain is the host under which cookies are set.
// If left empty, no cookie domain will be set
CookieDomain string
// UpstreamHost is the next upstream host the proxy will pass the request to.
// e.g. fluffy-bear-afiu23iaysd.oryapis.com
UpstreamHost string
// UpstreamScheme is the protocol used by the upstream service.
UpstreamScheme string
// TargetHost is the final target of the request. Should be the same as UpstreamHost
// if the request is directly passed to the target service.
TargetHost string
// TargetScheme is the final target's scheme
// (i.e. the scheme the target thinks it is running under)
TargetScheme string
// PathPrefix is a prefix that is prepended on the original host,
// but removed before forwarding.
PathPrefix string
// TrustForwardedHosts is a flag that indicates whether the proxy should trust the
// X-Forwarded-* headers or not.
TrustForwardedHeaders bool
// originalHost the original hostname the request is coming from.
// This value will be maintained internally by the proxy.
originalHost string
// originalScheme is the original scheme of the request.
// This value will be maintained internally by the proxy.
originalScheme string
// ForceOriginalSchemeHTTP forces the original scheme to be https if enabled.
ForceOriginalSchemeHTTPS bool
}
Options func(*options)
contextKey string
)
const (
hostConfigKey contextKey = "host config"
serverSpanKey contextKey = "server span"
)
func (c *HostConfig) setScheme(r *httputil.ProxyRequest) {
if c.ForceOriginalSchemeHTTPS {
c.originalScheme = "https"
} else if forwardedProto := r.In.Header.Get("X-Forwarded-Proto"); forwardedProto != "" {
c.originalScheme = forwardedProto
} else if r.In.TLS == nil {
c.originalScheme = "http"
} else {
c.originalScheme = "https"
}
}
func (c *HostConfig) setHost(r *httputil.ProxyRequest) {
if forwardedHost := r.In.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
c.originalHost = forwardedHost
} else {
c.originalHost = r.In.Host
}
}
// rewriter is a custom internal function for altering a http.Request
func rewriter(o *options) func(*httputil.ProxyRequest) {
return func(r *httputil.ProxyRequest) {
// Stash the server-side span so that modifyResponse can restore it as
// the active span before the response middlewares run. Without a valid
// span there is nothing worth restoring: replacing the transport's
// client span with a noop span would orphan the middleware spans.
ctx := r.Out.Context()
if s := trace.SpanFromContext(ctx); s.SpanContext().IsValid() {
ctx = context.WithValue(ctx, serverSpanKey, s)
r.Out = r.Out.WithContext(ctx)
}
ctx, span := otel.GetTracerProvider().Tracer("").Start(ctx, "x.proxy")
defer span.End()
_, c, err := o.getHostConfig(ctx, r.In)
if err != nil {
o.onReqError(r.Out, err)
return
}
if c.TrustForwardedHeaders {
headers := []string{
"X-Forwarded-Host",
"X-Forwarded-Proto",
"X-Forwarded-For",
}
for _, h := range headers {
if v := r.In.Header.Get(h); v != "" {
r.Out.Header.Set(h, v)
}
}
}
c.setScheme(r)
c.setHost(r)
headerRequestRewrite(r.Out, c)
var body []byte
var cb *compressableBody
if r.Out.ContentLength != 0 {
body, cb, err = readBody(r.Out.Header, r.Out.Body)
if err != nil {
o.onReqError(r.Out, err)
return
}
}
for _, m := range o.reqMiddlewares {
if body, err = m(r, c, body); err != nil {
o.onReqError(r.Out, err)
return
}
}
n, err := cb.Write(body)
if err != nil {
o.onReqError(r.Out, err)
return
}
r.Out.Header.Del("Content-Length")
r.Out.ContentLength = int64(n)
r.Out.Body = cb
}
}
// modifyResponse is a custom internal function for altering a http.Response
func modifyResponse(o *options) func(*http.Response) error {
return func(r *http.Response) error {
// An instrumented transport (otelhttp) clones the outbound request with
// its HTTP client span as the active span and ends that span once the
// response body is consumed. Restore the server-side span stashed in
// rewriter so that spans started by the response middlewares are
// parented to the still-open server span instead of the ended client
// span.
if span, ok := r.Request.Context().Value(serverSpanKey).(trace.Span); ok {
r.Request = r.Request.WithContext(trace.ContextWithSpan(r.Request.Context(), span))
}
_, c, err := o.getHostConfig(r.Request.Context(), r.Request)
if err != nil {
return err
}
if err := headerResponseRewrite(r, c); err != nil {
return o.onResError(r, err)
}
body, cb, err := bodyResponseRewrite(r, c)
if err != nil {
return o.onResError(r, err)
}
for _, m := range o.respMiddlewares {
if body, err = m(r, c, body); err != nil {
return o.onResError(r, err)
}
}
n, err := cb.Write(body)
if err != nil {
return o.onResError(r, err)
}
n, t, err := handleWebsocketResponse(n, cb, r.Body)
if err != nil {
return err
}
r.Header.Del("Content-Length")
r.ContentLength = int64(n)
r.Body = t
return nil
}
}
func WithOnError(onReqErr func(*http.Request, error), onResErr func(*http.Response, error) error) Options {
return func(o *options) {
o.onReqError = onReqErr
o.onResError = onResErr
}
}
func WithReqMiddleware(middlewares ...ReqMiddleware) Options {
return func(o *options) {
o.reqMiddlewares = append(o.reqMiddlewares, middlewares...)
}
}
func WithRespMiddleware(middlewares ...RespMiddleware) Options {
return func(o *options) {
o.respMiddlewares = append(o.respMiddlewares, middlewares...)
}
}
func WithTransport(t http.RoundTripper) Options {
return func(o *options) {
o.transport = t
}
}
func WithErrorHandler(eh func(w http.ResponseWriter, r *http.Request, err error)) Options {
return func(o *options) {
o.errHandler = eh
}
}
func (o *options) getHostConfig(ctx context.Context, r *http.Request) (context.Context, *HostConfig, error) {
if cached, ok := ctx.Value(hostConfigKey).(*HostConfig); ok && cached != nil {
return ctx, cached, nil
}
ctx, c, err := o.hostMapper(ctx, r)
if err != nil {
return nil, nil, err
}
// cache the host config in the request context
// this will be passed on to the request and response proxy functions
ctx = context.WithValue(ctx, hostConfigKey, c)
return ctx, c, nil
}
func (o *options) beforeProxyMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
// get the hostmapper configurations before the request is proxied
ctx, c, err := o.getHostConfig(request.Context(), request)
if err != nil {
o.onReqError(request, err)
return
}
// Add our Cors middleware.
// This middleware will only trigger if the host config has cors enabled on that request.
if c.CorsEnabled && c.CorsOptions != nil {
cors.New(*c.CorsOptions).HandlerFunc(writer, request)
}
h.ServeHTTP(writer, request.WithContext(ctx))
})
}
func defaultErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
switch {
case errors.Is(err, context.Canceled):
w.WriteHeader(499) // http://nginx.org/en/docs/dev/development_guide.html
case isTimeoutError(err):
w.WriteHeader(http.StatusGatewayTimeout)
default:
log.Printf("http: proxy error: %v", err)
w.WriteHeader(http.StatusBadGateway)
}
}
func isTimeoutError(err error) bool {
var te interface{ Timeout() bool } = nil
return errors.As(err, &te) && te.Timeout() || errors.Is(err, context.DeadlineExceeded)
}
// New creates a new Proxy
// A Proxy sets up a middleware with custom request and response modification handlers
func New(hostMapper HostMapper, opts ...Options) http.Handler {
o := &options{
hostMapper: hostMapper,
onReqError: func(*http.Request, error) {},
onResError: func(_ *http.Response, err error) error { return err },
transport: http.DefaultTransport,
errHandler: defaultErrorHandler,
}
for _, op := range opts {
op(o)
}
rp := &httputil.ReverseProxy{
Rewrite: rewriter(o),
ModifyResponse: modifyResponse(o),
Transport: o.transport,
ErrorHandler: o.errHandler,
}
return o.beforeProxyMiddleware(rp)
}

163
oryx/proxy/rewrites.go Normal file
View File

@@ -0,0 +1,163 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package proxy
import (
"bytes"
"compress/gzip"
"io"
"net/http"
"net/url"
"path"
"strings"
"github.com/pkg/errors"
)
type compressableBody struct {
buf bytes.Buffer
w io.WriteCloser
}
// we require a read and write for websocket connections
var _ io.ReadWriteCloser = new(compressableBody)
func (b *compressableBody) Close() error {
if b != nil {
b.buf.Reset()
if b.w != nil {
return b.w.Close()
}
}
return nil
}
func (b *compressableBody) Write(d []byte) (int, error) {
if b == nil {
// this happens when the body is empty
return 0, nil
}
var w io.Writer = &b.buf
if b.w != nil {
w = b.w
defer b.w.Close()
}
return w.Write(d)
}
func (b *compressableBody) Read(p []byte) (n int, err error) {
if b == nil {
// this happens when the body is empty
return 0, io.EOF
}
return b.buf.Read(p)
}
func headerRequestRewrite(req *http.Request, c *HostConfig) {
req.URL.Scheme = c.UpstreamScheme
req.URL.Host = c.UpstreamHost
req.URL.Path = strings.TrimPrefix(req.URL.Path, c.PathPrefix)
if _, ok := req.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
}
}
func headerResponseRewrite(resp *http.Response, c *HostConfig) error {
redir, err := resp.Location()
if err != nil {
if !errors.Is(err, http.ErrNoLocation) {
return errors.WithStack(err)
}
} else if redir.Host == c.TargetHost {
redir.Scheme = c.originalScheme
redir.Host = c.originalHost
redir.Path = path.Join(c.PathPrefix, redir.Path)
resp.Header.Set("Location", redir.String())
}
ReplaceCookieDomainAndSecure(resp, c.TargetHost, c.CookieDomain, c.originalScheme == "https")
return nil
}
// ReplaceCookieDomainAndSecure replaces the domain of all matching Set-Cookie headers in the response.
func ReplaceCookieDomainAndSecure(resp *http.Response, original, replacement string, secure bool) {
original, replacement = stripPort(original), stripPort(replacement) // cookies don't distinguish ports
cookies := resp.Cookies()
resp.Header.Del("Set-Cookie")
for _, co := range cookies {
co.Domain = replacement
co.Secure = secure
if !secure {
co.SameSite = http.SameSiteLaxMode
}
resp.Header.Add("Set-Cookie", co.String())
}
}
func bodyResponseRewrite(resp *http.Response, c *HostConfig) ([]byte, *compressableBody, error) {
if resp.ContentLength == 0 {
return nil, nil, nil
}
body, cb, err := readBody(resp.Header, resp.Body)
if err != nil {
return nil, nil, err
}
if c.TargetScheme == "" {
c.TargetScheme = "https"
}
return bytes.ReplaceAll(body, []byte(c.TargetScheme+"://"+c.TargetHost), []byte(c.originalScheme+"://"+c.originalHost+c.PathPrefix)), cb, nil
}
func readBody(h http.Header, body io.ReadCloser) ([]byte, *compressableBody, error) {
defer body.Close()
cb := &compressableBody{}
switch h.Get("Content-Encoding") {
case "gzip":
var err error
body, err = gzip.NewReader(body)
if err != nil {
return nil, nil, errors.WithStack(err)
}
cb.w = gzip.NewWriter(&cb.buf)
default:
// do nothing, we can read directly
}
b, err := io.ReadAll(body)
if err != nil {
return nil, nil, errors.WithStack(err)
}
return b, cb, nil
}
func handleWebsocketResponse(n int, cb *compressableBody, body io.ReadCloser) (int, io.ReadWriteCloser, error) {
var err error
readWriteCloser, ok := body.(io.ReadWriteCloser)
if ok {
if cb != nil {
n, err = readWriteCloser.Write(cb.buf.Bytes())
if err != nil {
return 0, nil, errors.WithStack(err)
}
}
return n, readWriteCloser, nil
}
return n, cb, nil
}
// stripPort removes the optional port from the host.
func stripPort(host string) string {
return (&url.URL{Host: host}).Hostname()
}

View File

@@ -0,0 +1,9 @@
{
"ui": {
"action": "https://auth.example.com"
},
"callbacks": [
"https://auth.example.com/path/to/resource",
"https://auth.example.com/path?q=https://localhost:8000"
]
}