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
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:
321
oryx/proxy/proxy.go
Normal file
321
oryx/proxy/proxy.go
Normal 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
163
oryx/proxy/rewrites.go
Normal 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()
|
||||
}
|
||||
9
oryx/proxy/stubs/auth.example.com.json
Normal file
9
oryx/proxy/stubs/auth.example.com.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user