Created
July 10, 2024 15:40
-
-
Save mjudeikis/4773b47c2fcd11c6b21c92b74d809979 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package server | |
import ( | |
"context" | |
"errors" | |
"fmt" | |
"log" | |
"net/http" | |
"strings" | |
"time" | |
"connectrpc.com/connect" | |
"connectrpc.com/grpchealth" | |
"connectrpc.com/grpcreflect" | |
"github.com/rs/cors" | |
"golang.org/x/net/http2" | |
"golang.org/x/net/http2/h2c" | |
"github.com/faroshq/cluster-proxy/apiserver/config" | |
"github.com/faroshq/cluster-proxy/apiserver/grpc/gen/faros/tenancy/v1alpha1/tenancyv1alpha1connect" | |
"github.com/faroshq/cluster-proxy/apiserver/grpc/gen/private/faros/quota/v1alpha1/quotav1alpha1connect" | |
"github.com/faroshq/cluster-proxy/apiserver/grpc/swagger" | |
authjwt "github.com/faroshq/cluster-proxy/apiserver/server/oidc/jwt" | |
quotaserver "github.com/faroshq/cluster-proxy/apiserver/server/quota" | |
tenancyserver "github.com/faroshq/cluster-proxy/apiserver/server/tenancy" | |
"github.com/faroshq/cluster-proxy/apiserver/store" | |
) | |
type Server interface { | |
// Run starts the server. | |
Run(context.Context) | |
} | |
type server struct { | |
cfg *config.Config | |
srv *http.Server | |
} | |
var ( | |
base = "/faros.tenancy.v1alpha1.OIDCService" | |
login = base + "/Login" | |
callback = base + "/Callback" | |
swaggerPath = "/swagger/" | |
excludedPaths = []string{swaggerPath, login, callback} | |
) | |
type handler struct { | |
a authjwt.Authenticator | |
} | |
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
switch { | |
case strings.HasPrefix(r.URL.Path, login): | |
h.a.OIDCLogin(w, r) | |
case strings.HasPrefix(r.URL.Path, callback): | |
h.a.OIDCCallback(w, r) | |
default: | |
http.RedirectHandler("https://faros.sh", http.StatusFound).ServeHTTP(w, r) | |
} | |
} | |
func New(ctx context.Context, cfg *config.Config) (Server, error) { | |
store, err := store.New(cfg.APIConfig.KubernetesTenancyRestConfig) | |
if err != nil { | |
return nil, err | |
} | |
// handler to login via OIDC | |
jwtAuthenticator, err := authjwt.New(ctx, cfg, store, callback) | |
if err != nil { | |
return nil, err | |
} | |
loginHandler := handler{a: jwtAuthenticator} | |
mux := http.NewServeMux() | |
mux.Handle(login, loginHandler) | |
mux.Handle(callback, loginHandler) | |
mux.Handle(swaggerPath, swagger.GetSwaggerHandlerFunction(cfg.APIConfig.ControllerExternalURL)) | |
compress := connect.WithCompressMinBytes(1024) | |
interceptor := connect.WithInterceptors(jwtAuthenticator.NewAuthInterceptor()) | |
opts := connect.WithOptions(compress, interceptor) | |
optsInternal := connect.WithOptions(compress) | |
// TODO: Add helpers for these. | |
mux.Handle(grpchealth.NewHandler( | |
grpchealth.NewStaticChecker( | |
quotav1alpha1connect.QuotaServiceName, | |
tenancyv1alpha1connect.IAMServiceName, | |
), | |
opts, | |
)) | |
mux.Handle(grpcreflect.NewHandlerV1( | |
grpcreflect.NewStaticReflector( | |
quotav1alpha1connect.QuotaServiceName, | |
tenancyv1alpha1connect.IAMServiceName, | |
), | |
opts, | |
)) | |
mux.Handle(grpcreflect.NewHandlerV1Alpha( | |
grpcreflect.NewStaticReflector( | |
quotav1alpha1connect.QuotaServiceName, | |
tenancyv1alpha1connect.IAMServiceName, | |
), | |
opts, | |
)) | |
// Quota | |
serverQuota, err := quotaserver.NewQuotaServer(ctx, cfg, store) | |
if err != nil { | |
return nil, err | |
} | |
tenancyServer, err := tenancyserver.NewTenancyServer(ctx, cfg, store) | |
if err != nil { | |
return nil, err | |
} | |
mux.Handle(quotav1alpha1connect.NewQuotaServiceHandler( | |
serverQuota, | |
optsInternal, // internal is not authenticated | |
)) | |
mux.Handle(tenancyv1alpha1connect.NewIAMServiceHandler( | |
tenancyServer, | |
opts, | |
)) | |
addr := cfg.APIConfig.Addr | |
handler := newCORS().Handler(mux) | |
srv := &http.Server{ | |
Addr: addr, | |
Handler: h2c.NewHandler( | |
handler, | |
&http2.Server{}, | |
), | |
ReadHeaderTimeout: time.Second, | |
ReadTimeout: 5 * time.Minute, | |
WriteTimeout: 5 * time.Minute, | |
MaxHeaderBytes: 8 * 1024, // 8KiB | |
} | |
return &server{ | |
srv: srv, | |
cfg: cfg, | |
}, nil | |
} | |
func (s *server) Run(ctx context.Context) { | |
fmt.Printf("Starting server on %s \n", s.cfg.APIConfig.Addr) | |
go func() { | |
fmt.Printf("HTTP server starting: %s \n", s.srv.ListenAndServe()) | |
if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { | |
log.Fatalf("HTTP listen and serve: %v", err) | |
} | |
}() | |
<-ctx.Done() | |
ctxT, cancel := context.WithTimeout(context.Background(), time.Second) | |
defer cancel() | |
if err := s.srv.Shutdown(ctxT); err != nil { | |
log.Fatalf("HTTP shutdown: %v", err) //nolint:gocritic | |
} | |
} | |
func newCORS() *cors.Cors { | |
// To let web developers play with the demo service from browsers, we need a | |
// very permissive CORS setup. | |
return cors.New(cors.Options{ | |
AllowedMethods: []string{ | |
http.MethodHead, | |
http.MethodGet, | |
http.MethodPost, | |
http.MethodPut, | |
http.MethodPatch, | |
http.MethodDelete, | |
}, | |
AllowOriginFunc: func(origin string) bool { | |
// Allow all origins, which effectively disables CORS. | |
return true | |
}, | |
AllowedHeaders: []string{"*"}, | |
ExposedHeaders: []string{ | |
// Content-Type is in the default safelist. | |
"Accept", | |
"Accept-Encoding", | |
"Accept-Post", | |
"Connect-Accept-Encoding", | |
"Connect-Content-Encoding", | |
"Content-Encoding", | |
"Grpc-Accept-Encoding", | |
"Grpc-Encoding", | |
"Grpc-Message", | |
"Grpc-Status", | |
"Grpc-Status-Details-Bin", | |
}, | |
// Let browsers cache CORS information for longer, which reduces the number | |
// of preflight requests. Any changes to ExposedHeaders won't take effect | |
// until the cached data expires. FF caps this value at 24h, and modern | |
// Chrome caps it at 2h. | |
MaxAge: int(2 * time.Hour / time.Second), | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment