package codit //import "io" import "context" import "crypto/sha256" import "crypto/tls" import "crypto/x509" import "database/sql" import "encoding/hex" import "encoding/pem" import "errors" import "fmt" import "io" import "log" import "net" import "net/http" import "net/netip" import "net/url" import "os" import "path/filepath" import "sync" import "strings" import "time" import "codit/internal/auth" import codit_config "codit/config" import "codit/internal/db" import "codit/internal/docker" import "codit/internal/git" import "codit/internal/handlers" import codit_http "codit/internal/http" import "codit/internal/middleware" import "codit/internal/models" import "codit/internal/pkiutil" import "codit/internal/rpm" import "codit/internal/storage" import "codit/internal/util" import "golang.org/x/text/transform" import _ "modernc.org/sqlite" const logIDAPI string = "api" type gitPathRewriteHandler struct { next http.Handler store *db.Store } type server_http_log_writer struct { l Logger id string depth int } func (hlw *server_http_log_writer) Write(p []byte) (n int, err error) { // the standard http.Server always requires *log.Logger // use this iowriter to create a logger to pass it to the http server. // since this is another log write wrapper, give adjustment value hlw.l.WriteWithCallDepth(hlw.id, LOG_INFO, hlw.depth, string(p)) return len(p), nil } func (h *gitPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var path string var parts []string var slug string var repoSegment string var rest string var repoName string var orgPath string var currentStore *db.Store var project models.Project var repo models.Repo var projectStorageID int64 var repoStorageID int64 var err error var newPath string currentStore = requestStore(r, h.store) path = strings.TrimPrefix(r.URL.Path, "/") if path == "" { h.next.ServeHTTP(w, r) return } parts = strings.SplitN(path, "/", 3) if len(parts) < 2 { h.next.ServeHTTP(w, r) return } slug = parts[0] repoSegment = parts[1] if len(parts) == 3 { rest = "/" + parts[2] } else { rest = "" } orgPath = r.URL.Path repoName = strings.TrimSuffix(repoSegment, ".git") project, err = currentStore.GetProjectBySlug(slug) if err != nil { http.NotFound(w, r) return } repo, err = currentStore.GetRepoByProjectNameType(project.ID, repoName, "git") if err != nil { http.NotFound(w, r) return } projectStorageID, repoStorageID, err = currentStore.GetRepoStorageIDs(repo.ID) if err != nil { http.NotFound(w, r) return } newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + ".git" + rest r = git.WithRequestLogInfo(r, git.RequestLogInfo{ OrgPath: orgPath, ProjectID: project.ID, ProjectSlug: project.Slug, RepoID: repo.ID, RepoName: repo.Name, }) r.URL.Path = newPath h.next.ServeHTTP(w, r) } type gitIDPathRewriteHandler struct { next http.Handler store *db.Store } func (h *gitIDPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var path string var parts []string var rest string var orgPath string var currentStore *db.Store var project models.Project var repo models.Repo var projectStorageID int64 var repoStorageID int64 var projectErr error var err error var newPath string var repoID string currentStore = requestStore(r, h.store) path = strings.TrimPrefix(r.URL.Path, "/") if path == "" { h.next.ServeHTTP(w, r) return } parts = strings.SplitN(path, "/", 3) repoID = strings.TrimSuffix(parts[0], ".git") orgPath = r.URL.Path repo, err = currentStore.GetRepo(repoID) if err != nil || repo.Type != "git" { http.NotFound(w, r) return } project, projectErr = currentStore.GetProject(repo.ProjectID) projectStorageID, repoStorageID, err = currentStore.GetRepoStorageIDs(repo.ID) if err != nil { http.NotFound(w, r) return } if len(parts) == 3 { rest = "/" + parts[1] + "/" + parts[2] } else if len(parts) == 2 { rest = "/" + parts[1] } else { rest = "" } newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + ".git" + rest if projectErr != nil { project.Slug = "" } r = git.WithRequestLogInfo(r, git.RequestLogInfo{ OrgPath: orgPath, ProjectID: repo.ProjectID, ProjectSlug: project.Slug, RepoID: repo.ID, RepoName: repo.Name, }) r.URL.Path = newPath h.next.ServeHTTP(w, r) } type rpmPathRewriteHandler struct { next http.Handler store *db.Store } func (h *rpmPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var path string var parts []string var slug string var repoName string var rest string var orgPath string var currentStore *db.Store var project models.Project var repo models.Repo var projectStorageID int64 var repoStorageID int64 var err error var newPath string currentStore = requestStore(r, h.store) path = strings.TrimPrefix(r.URL.Path, "/") if path == "" { h.next.ServeHTTP(w, r) return } parts = strings.SplitN(path, "/", 3) if len(parts) < 2 { h.next.ServeHTTP(w, r) return } slug = parts[0] repoName = parts[1] if len(parts) == 3 { rest = "/" + parts[2] } else { rest = "" } orgPath = r.URL.Path project, err = currentStore.GetProjectBySlug(slug) if err != nil { http.NotFound(w, r) return } repo, err = currentStore.GetRepoByProjectNameType(project.ID, repoName, "rpm") if err != nil { http.NotFound(w, r) return } projectStorageID, repoStorageID, err = currentStore.GetRepoStorageIDs(repo.ID) if err != nil { http.NotFound(w, r) return } newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + rest r = rpm.WithRequestLogInfo(r, rpm.RequestLogInfo{ OrgPath: orgPath, ProjectID: project.ID, ProjectSlug: project.Slug, RepoID: repo.ID, RepoName: repo.Name, }) r.URL.Path = newPath h.next.ServeHTTP(w, r) } type rpmIDPathRewriteHandler struct { next http.Handler store *db.Store } func (h *rpmIDPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var path string var parts []string var rest string var orgPath string var currentStore *db.Store var project models.Project var repo models.Repo var projectStorageID int64 var repoStorageID int64 var projectErr error var err error var newPath string var repoID string currentStore = requestStore(r, h.store) path = strings.TrimPrefix(r.URL.Path, "/") if path == "" { h.next.ServeHTTP(w, r) return } parts = strings.SplitN(path, "/", 3) repoID = parts[0] orgPath = r.URL.Path repo, err = currentStore.GetRepo(repoID) if err != nil || repo.Type != "rpm" { http.NotFound(w, r) return } project, projectErr = currentStore.GetProject(repo.ProjectID) projectStorageID, repoStorageID, err = currentStore.GetRepoStorageIDs(repo.ID) if err != nil { http.NotFound(w, r) return } if len(parts) == 3 { rest = "/" + parts[1] + "/" + parts[2] } else if len(parts) == 2 { rest = "/" + parts[1] } else { rest = "" } newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + rest if projectErr != nil { project.Slug = "" } r = rpm.WithRequestLogInfo(r, rpm.RequestLogInfo{ OrgPath: orgPath, ProjectID: repo.ProjectID, ProjectSlug: project.Slug, RepoID: repo.ID, RepoName: repo.Name, }) r.URL.Path = newPath h.next.ServeHTTP(w, r) } func storageIDSegment(id int64) string { return fmt.Sprintf("%016x", id) } type Server struct { cfg *codit_config.Config logger Logger serverId string siteName string store *db.Store docker_base_dir string git_base_dir string rpm_base_dir string uploads_base_dir string git_manager *git.RepoManager rpm_meta *rpm.MetaManager rpm_mirror *rpm.MirrorManager upload_store *storage.FileStore docker_server http.Handler git_server http.Handler rpm_server http.Handler } func NewServer(cfg *Config, logger Logger) (*Server, error) { return NewServerWithId(cfg, logger, "codit") } func NewServerWithId(cfg *Config, logger Logger, identifier string) (*Server, error) { if cfg == nil { return nil, errors.New("config is required") } if logger == nil { return nil, errors.New("logger is required") } return newServerWithInternalConfig(cfg, logger, identifier) } func auth_user(store *db.Store, username string, password string) (bool, error) { var user models.User var principal models.ServicePrincipal var hash string var err error var key string user, hash, err = store.GetUserByUsername(username) if err != nil || hash == "" { if password != "" { key = password user, err = store.GetUserByAPIKeyHash(util.HashToken(key)) if err == nil { return true, nil } principal, err = store.GetPrincipalByAPIKeyHash(util.HashToken(key)) if err == nil { return !principal.Disabled, nil } } if password == "" && username != "" { key = username user, err = store.GetUserByAPIKeyHash(util.HashToken(key)) if err == nil { return true, nil } principal, err = store.GetPrincipalByAPIKeyHash(util.HashToken(key)) if err == nil { return !principal.Disabled, nil } } return false, nil } err = auth.ComparePassword(hash, password) if err != nil { if password != "" { key = password user, err = store.GetUserByAPIKeyHash(util.HashToken(key)) if err == nil { return true, nil } principal, err = store.GetPrincipalByAPIKeyHash(util.HashToken(key)) if err == nil { return !principal.Disabled, nil } } return false, nil } if user.Disabled { return false, nil } return user.ID != "", nil } func newServerWithInternalConfig(cfg *codit_config.Config, logger Logger, identifier string) (*Server, error) { var app Server var dir string var err error cfg.DockerHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.DockerHTTPPrefix, "/v2") cfg.GitHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.GitHTTPPrefix, "/git") cfg.RPMHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.RPMHTTPPrefix, "/rpm") app.cfg = cfg app.logger = logger app.serverId = NormalizeServerId(identifier) app.siteName = strings.TrimSpace(cfg.App.SiteName) if app.siteName == "" { // get the host from the public base url if set. var tmp string tmp = strings.TrimSpace(cfg.PublicBaseURL) if tmp != "" { var parsed *url.URL parsed, err = url.Parse(tmp) if err == nil && parsed != nil { app.siteName = strings.TrimSpace(parsed.Host) } } if app.siteName == "" { app.siteName = TitleFromServerId(app.serverId) } } app.docker_base_dir = filepath.Join(cfg.DataDir, "docker") app.git_base_dir = filepath.Join(cfg.DataDir, "git") app.rpm_base_dir = filepath.Join(cfg.DataDir, "rpm") app.uploads_base_dir = filepath.Join(cfg.DataDir, "uploads") err = os.MkdirAll(cfg.DataDir, 0o755) if err != nil { err = fmt.Errorf("data dir error - %v", err) goto oops } app.store, err = db.Open(cfg.DBDriver, cfg.DBDSN) if err != nil { err = fmt.Errorf("db open error - %v", err) goto oops } err = app.store.ApplyMigrationsFS(migrationFiles, "migrations") if err != nil { err = fmt.Errorf("migrations error - %v", err) goto oops } err = app.store.EnsureDefaultTLSAuthPolicies() if err != nil { err = fmt.Errorf("tls auth policy init error - %v", err) goto oops } err = app.store.EnsureTLSAuthPolicyBindings() if err != nil { err = fmt.Errorf("tls auth binding init error - %v", err) goto oops } err = mergeTLSSettingsFromDB(cfg, app.store, EnvPrefixFromServerId(app.serverId)) if err != nil { err = fmt.Errorf("tls settings load error - %v", err) goto oops } err = bootstrapAdmin(app.store, EnvPrefixFromServerId(app.serverId)) if err != nil { err = fmt.Errorf("bootstrap admin error - %v", err) goto oops } app.git_manager = &git.RepoManager{BaseDir: app.git_base_dir} app.rpm_meta = rpm.NewMetaManager() app.rpm_mirror = rpm.NewMirrorManager(app.store, logger, app.rpm_meta) app.upload_store = &storage.FileStore{BaseDir: app.uploads_base_dir} for _, dir = range []string{app.docker_base_dir, app.git_base_dir, app.rpm_base_dir, app.uploads_base_dir} { err = os.MkdirAll(dir, 0o755) if err != nil { err = fmt.Errorf("dir error (%s) - %v", dir, err) goto oops } } app.docker_server = docker.NewHTTPServer(app.store, app.docker_base_dir, nil, logger, cfg.DockerHTTPPrefix) app.git_server, err = git.NewHTTPServer(app.git_base_dir, nil, logger) if err != nil { err = fmt.Errorf("git server error - %v", err) goto oops } app.rpm_server = rpm.NewHTTPServer(app.rpm_base_dir, nil, logger) return &app, nil oops: app.Close() return nil, err } func (app *Server) Close() { if app.store != nil { app.store.Close() } } func (app *Server) Serve() error { return app.ServeContext(context.Background()) } func (app *Server) ServeContext(ctx context.Context) error { var api *handlers.API var mux *http.ServeMux var mainEndpoints []listenerEndpoint var extraListenerManager *additionalListenerManager var shutdownCtx context.Context var cancel context.CancelFunc var servicesStarted bool var err error api, mux, err = app.initHandlers() if err != nil { app.logger.Write("", LOG_ERROR, "failed to initialize handlers - %v", err) return err } extraListenerManager = newAdditionalListenerManager(app.store, mux, app.logger) api.OnTLSListenersChanged = extraListenerManager.NotifyReload api.OnTLSListenerRuntimeStatus = extraListenerManager.ListenerEndpointCounts app.rpm_mirror.Start() servicesStarted = true err = extraListenerManager.Start() if err != nil { app.logger.Write("", LOG_ERROR, "additional listener manager error - %v", err) shutdownCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second) extraListenerManager.Stop(shutdownCtx) app.rpm_mirror.Stop() app.rpm_mirror.Wait() cancel() return err } mainEndpoints, err = buildListenerEndpoints("main", "main", "", util.TLSSettingsFromConfig(*app.cfg), app.store) if err != nil { app.logger.Write("", LOG_ERROR, "main listener config error - %v", err) shutdownCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second) extraListenerManager.Stop(shutdownCtx) app.rpm_mirror.Stop() app.rpm_mirror.Wait() cancel() return err } if len(mainEndpoints) == 0 && extraListenerManager.TotalEndpointCount() == 0 { err = errors.New("at least one main or additional listener is required") app.logger.Write("", LOG_ERROR, "listener config error - %v", err) shutdownCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second) extraListenerManager.Stop(shutdownCtx) app.rpm_mirror.Stop() app.rpm_mirror.Wait() cancel() return err } if len(mainEndpoints) == 0 { <-ctx.Done() shutdownCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second) extraListenerManager.Stop(shutdownCtx) if servicesStarted && app.rpm_mirror != nil { app.rpm_mirror.Stop() app.rpm_mirror.Wait() } cancel() return nil } err = serveListeners(ctx, mainEndpoints, mux, app.logger, app.serverId) shutdownCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second) if extraListenerManager != nil { extraListenerManager.Stop(shutdownCtx) } if servicesStarted && app.rpm_mirror != nil { app.rpm_mirror.Stop() app.rpm_mirror.Wait() } cancel() if err != nil { app.logger.Write("", LOG_ERROR, "server error - %v", err) return err } return nil } func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) { var cfg *codit_config.Config = app.cfg var logger Logger = app.logger var store *db.Store = app.store var api *handlers.API var router *codit_http.Router api = handlers.NewAPI(handlers.APIOptions{ ServerId: app.serverId, SiteName: app.siteName, Cfg: *cfg, Store: app.store, Repos: *app.git_manager, RpmBase: app.rpm_base_dir, RpmMeta: app.rpm_meta, RpmMirror: app.rpm_mirror, DockerBase: app.docker_base_dir, Uploads: *app.upload_store, Logger: logger, }) router = codit_http.NewRouter() router.Handle("GET", "/api/health", api.Health) router.Handle("GET", "/api/app-info", api.AppInfo) router.Handle("POST", "/api/login", api.Login) router.Handle("POST", "/api/logout", api.Logout) router.Handle("GET", "/api/auth/oidc/enabled", api.OIDCEnabled) router.Handle("GET", "/api/auth/oidc/login", api.OIDCLogin) router.Handle("GET", "/api/auth/oidc/callback", api.OIDCCallback) router.Handle("GET", "/api/me", api.Me) router.Handle("PATCH", "/api/me", api.UpdateMe) router.Handle("GET", "/api/me/totp", api.GetMyTOTP) router.Handle("POST", "/api/me/totp/setup", api.SetupMyTOTP) router.Handle("POST", "/api/me/totp/enable", api.EnableMyTOTP) router.Handle("POST", "/api/me/totp/verify", api.VerifyMyTOTP) router.Handle("POST", "/api/me/totp/disable", api.DisableMyTOTP) router.Handle("GET", "/api/users", api.ListUsers) router.Handle("POST", "/api/users", api.CreateUser) router.Handle("PATCH", "/api/users/:id", api.UpdateUser) router.Handle("DELETE", "/api/users/:id", api.DeleteUser) router.Handle("POST", "/api/users/:id/disable", api.DisableUser) router.Handle("POST", "/api/users/:id/enable", api.EnableUser) router.Handle("POST", "/api/users/:id/totp/reset", api.ResetUserTOTPAdmin) router.Handle("GET", "/api/admin/user-groups", api.ListUserGroups) router.Handle("POST", "/api/admin/user-groups", api.CreateUserGroup) router.Handle("PATCH", "/api/admin/user-groups/:id", api.UpdateUserGroup) router.Handle("DELETE", "/api/admin/user-groups/:id", api.DeleteUserGroup) router.Handle("GET", "/api/admin/user-groups/:id/members", api.ListUserGroupMembers) router.Handle("POST", "/api/admin/user-groups/:id/members", api.AddUserGroupMember) router.Handle("DELETE", "/api/admin/user-groups/:id/members/:userId", api.RemoveUserGroupMember) router.Handle("GET", "/api/admin/permissions", api.ListSubjectPermissions) router.Handle("POST", "/api/admin/permissions", api.CreateSubjectPermissions) router.Handle("DELETE", "/api/admin/permissions/:permission/:subjectType/:subjectID", api.DeleteSubjectPermission) router.Handle("GET", "/api/admin/auth", api.GetAuthSettings) router.Handle("PATCH", "/api/admin/auth", api.UpdateAuthSettings) router.Handle("POST", "/api/admin/auth/test", api.TestLDAPSettings) router.Handle("GET", "/api/admin/tls", api.GetTLSSettings) router.Handle("PATCH", "/api/admin/tls", api.UpdateTLSSettings) router.Handle("GET", "/api/admin/tls/auth-policies", api.ListTLSAuthPolicies) router.Handle("POST", "/api/admin/tls/auth-policies", api.CreateTLSAuthPolicy) router.Handle("PATCH", "/api/admin/tls/auth-policies/:id", api.UpdateTLSAuthPolicy) router.Handle("DELETE", "/api/admin/tls/auth-policies/:id", api.DeleteTLSAuthPolicy) router.Handle("GET", "/api/admin/tls/listeners", api.ListTLSListeners) router.Handle("GET", "/api/admin/tls/listeners/runtime", api.GetTLSListenerRuntimeStatus) router.Handle("POST", "/api/admin/tls/listeners", api.CreateTLSListener) router.Handle("PATCH", "/api/admin/tls/listeners/:id", api.UpdateTLSListener) router.Handle("DELETE", "/api/admin/tls/listeners/:id", api.DeleteTLSListener) router.Handle("GET", "/api/admin/service-principals", api.ListServicePrincipals) router.Handle("POST", "/api/admin/service-principals", api.CreateServicePrincipal) router.Handle("PATCH", "/api/admin/service-principals/:id", api.UpdateServicePrincipal) router.Handle("DELETE", "/api/admin/service-principals/:id", api.DeleteServicePrincipal) router.Handle("GET", "/api/admin/service-principals/:id/api-keys", api.ListServicePrincipalAPIKeys) router.Handle("POST", "/api/admin/service-principals/:id/api-keys", api.CreateServicePrincipalAPIKey) router.Handle("DELETE", "/api/admin/service-principals/:id/api-keys/:keyId", api.DeleteServicePrincipalAPIKey) router.Handle("POST", "/api/admin/service-principals/:id/api-keys/:keyId/disable", api.DisableServicePrincipalAPIKey) router.Handle("POST", "/api/admin/service-principals/:id/api-keys/:keyId/enable", api.EnableServicePrincipalAPIKey) router.Handle("GET", "/api/admin/service-principals/:id/roles", api.ListPrincipalProjectRoles) router.Handle("POST", "/api/admin/service-principals/:id/roles", api.UpsertPrincipalProjectRole) router.Handle("DELETE", "/api/admin/service-principals/:id/roles/:projectId", api.DeletePrincipalProjectRole) router.Handle("GET", "/api/admin/cert-principal-bindings", api.ListCertPrincipalBindings) router.Handle("POST", "/api/admin/cert-principal-bindings", api.UpsertCertPrincipalBinding) router.Handle("DELETE", "/api/admin/cert-principal-bindings/:fingerprint", api.DeleteCertPrincipalBinding) router.Handle("GET", "/api/admin/auth/ldap", api.GetAuthSettings) router.Handle("PATCH", "/api/admin/auth/ldap", api.UpdateAuthSettings) router.Handle("POST", "/api/admin/auth/ldap/test", api.TestLDAPSettings) router.Handle("GET", "/api/admin/pki/cas", api.ListPKICAs) router.Handle("GET", "/api/admin/pki/cas/:id", api.GetPKICA) router.Handle("PATCH", "/api/admin/pki/cas/:id", api.UpdatePKICA) router.Handle("GET", "/api/admin/pki/cas/:id/bundle", api.DownloadPKICABundle) router.Handle("POST", "/api/admin/pki/cas/root", api.CreatePKIRootCA) router.Handle("POST", "/api/admin/pki/cas/intermediate", api.CreatePKIIntermediateCA) router.Handle("GET", "/api/admin/pki/cas/:id/crl", api.GetPKICRL) router.Handle("DELETE", "/api/admin/pki/cas/:id", api.DeletePKICA) router.Handle("GET", "/api/admin/pki/certs", api.ListPKICerts) router.Handle("GET", "/api/admin/pki/certs/:id", api.GetPKICert) router.Handle("GET", "/api/admin/pki/certs/:id/inspect", api.GetPKICertInspect) router.Handle("POST", "/api/admin/pki/certs/:id/renew-acme", api.RenewPKICertWithACME) router.Handle("GET", "/api/admin/pki/certs/:id/bundle", api.DownloadPKICertBundle) router.Handle("POST", "/api/admin/pki/certs", api.IssuePKICert) router.Handle("POST", "/api/admin/pki/certs/import", api.ImportPKICert) router.Handle("POST", "/api/admin/pki/certs/:id/revoke", api.RevokePKICert) router.Handle("DELETE", "/api/admin/pki/certs/:id", api.DeletePKICert) router.Handle("GET", "/api/admin/pki/client/profiles", api.ListPKIClientProfiles) router.Handle("POST", "/api/admin/pki/client/profiles", api.CreatePKIClientProfile) router.Handle("PATCH", "/api/admin/pki/client/profiles/:id", api.UpdatePKIClientProfile) router.Handle("DELETE", "/api/admin/pki/client/profiles/:id", api.DeletePKIClientProfile) router.Handle("GET", "/api/admin/pki/acme/profiles", api.ListACMEProfiles) router.Handle("POST", "/api/admin/pki/acme/profiles", api.CreateACMEProfile) router.Handle("PATCH", "/api/admin/pki/acme/profiles/:id", api.UpdateACMEProfile) router.Handle("DELETE", "/api/admin/pki/acme/profiles/:id", api.DeleteACMEProfile) router.Handle("GET", "/api/admin/pki/acme/orders", api.ListACMEOrders) router.Handle("POST", "/api/admin/pki/acme/orders", api.CreateACMEOrder) router.Handle("POST", "/api/admin/pki/acme/orders/:id/finalize", api.FinalizeACMEOrder) router.Handle("DELETE", "/api/admin/pki/acme/orders/:id", api.DeleteACMEOrder) router.Handle("GET", "/api/admin/rpm/mirrors", api.ListAdminRPMMirrors) router.Handle("GET", "/api/admin/ssh/user-cas", api.ListSSHUserCAs) router.Handle("POST", "/api/admin/ssh/user-cas", api.CreateSSHUserCA) router.Handle("GET", "/api/admin/ssh/user-cas/:id", api.GetSSHUserCA) router.Handle("PATCH", "/api/admin/ssh/user-cas/:id", api.UpdateSSHUserCA) router.Handle("DELETE", "/api/admin/ssh/user-cas/:id", api.DeleteSSHUserCA) router.Handle("GET", "/api/admin/ssh/user-cas/:id/public-key", api.DownloadSSHUserCAPublicKey) router.Handle("GET", "/api/admin/ssh/user-cas/:id/private-key", api.DownloadSSHUserCAPrivateKey) router.Handle("POST", "/api/admin/ssh/user-cas/:id/sign", api.SignSSHUserKey) router.Handle("GET", "/api/admin/ssh/user-cas/:id/issuances", api.ListSSHUserCAIssuances) router.Handle("GET", "/api/admin/ssh/issuances", api.ListSSHUserCAIssuancesAll) router.Handle("GET", "/api/admin/ssh/principal-grants", api.ListSSHPrincipalGrants) router.Handle("GET", "/api/admin/ssh/principal-grants/:id", api.GetSSHPrincipalGrant) router.Handle("POST", "/api/admin/ssh/principal-grants", api.CreateSSHPrincipalGrant) router.Handle("PATCH", "/api/admin/ssh/principal-grants/:id", api.UpdateSSHPrincipalGrant) router.Handle("DELETE", "/api/admin/ssh/principal-grants/:id", api.DeleteSSHPrincipalGrant) router.Handle("GET", "/api/admin/ssh/servers", api.ListSSHServersAdmin) router.Handle("POST", "/api/admin/ssh/servers", api.CreateSSHServerAdmin) router.Handle("GET", "/api/admin/ssh/servers/:id", api.GetSSHServerAdmin) router.Handle("PATCH", "/api/admin/ssh/servers/:id", api.UpdateSSHServerAdmin) router.Handle("DELETE", "/api/admin/ssh/servers/:id", api.DeleteSSHServerAdmin) router.Handle("GET", "/api/admin/ssh/servers/:id/host-keys", api.ListSSHServerHostKeysAdmin) router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys", api.CreateSSHServerHostKeyAdmin) router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys/discover", api.DiscoverSSHServerHostKeyAdmin) router.Handle("DELETE", "/api/admin/ssh/servers/:id/host-keys/:hostKeyId", api.DeleteSSHServerHostKeyAdmin) router.Handle("GET", "/api/admin/ssh/server-groups", api.ListSSHServerGroupsAdmin) router.Handle("POST", "/api/admin/ssh/server-groups", api.CreateSSHServerGroupAdmin) router.Handle("PATCH", "/api/admin/ssh/server-groups/:id", api.UpdateSSHServerGroupAdmin) router.Handle("DELETE", "/api/admin/ssh/server-groups/:id", api.DeleteSSHServerGroupAdmin) router.Handle("GET", "/api/admin/ssh/server-groups/:id/servers", api.ListSSHServerGroupMembersAdmin) router.Handle("POST", "/api/admin/ssh/server-groups/:id/servers", api.AddSSHServerGroupMemberAdmin) router.Handle("DELETE", "/api/admin/ssh/server-groups/:id/servers/:serverId", api.DeleteSSHServerGroupMemberAdmin) router.Handle("GET", "/api/admin/ssh/credentials", api.ListSSHCredentialsAdmin) router.Handle("POST", "/api/admin/ssh/credentials", api.CreateSSHCredentialAdmin) router.Handle("PATCH", "/api/admin/ssh/credentials/:id", api.UpdateSSHCredentialAdmin) router.Handle("DELETE", "/api/admin/ssh/credentials/:id", api.DeleteSSHCredentialAdmin) router.Handle("GET", "/api/admin/ssh/access-profiles", api.ListSSHAccessProfilesAdmin) router.Handle("POST", "/api/admin/ssh/access-profiles", api.CreateSSHAccessProfileAdmin) router.Handle("GET", "/api/admin/ssh/access-profiles/:id", api.GetSSHAccessProfileAdmin) router.Handle("PATCH", "/api/admin/ssh/access-profiles/:id", api.UpdateSSHAccessProfileAdmin) router.Handle("DELETE", "/api/admin/ssh/access-profiles/:id", api.DeleteSSHAccessProfileAdmin) router.Handle("GET", "/api/admin/ssh/sessions", api.ListSSHSessionsAdmin) router.Handle("GET", "/api/admin/ssh/sessions/:id/transcript", api.GetSSHSessionTranscriptAdmin) router.Handle("GET", "/api/ssh/user-cas", api.ListSSHUserCAsForSelf) router.Handle("GET", "/api/ssh/user-cas/:id", api.GetSSHUserCAForSelf) router.Handle("GET", "/api/ssh/user-cas/:id/public-key", api.DownloadSSHUserCAPublicKeyForSelf) router.Handle("GET", "/api/ssh/principal-grants", api.ListSSHPrincipalGrantsForSelf) router.Handle("GET", "/api/ssh/servers", api.ListSSHServersForSelf) router.Handle("GET", "/api/ssh/servers/:id", api.GetSSHServerForSelf) router.Handle("POST", "/api/ssh/servers", api.CreateSSHServerForSelf) router.Handle("PATCH", "/api/ssh/servers/:id", api.UpdateSSHServerForSelf) router.Handle("DELETE", "/api/ssh/servers/:id", api.DeleteSSHServerForSelf) router.Handle("GET", "/api/ssh/servers/:id/host-keys", api.ListSSHServerHostKeysForSelf) router.Handle("POST", "/api/ssh/servers/:id/host-keys", api.CreateSSHServerHostKeyForSelf) router.Handle("POST", "/api/ssh/servers/:id/host-keys/discover", api.DiscoverSSHServerHostKeyForSelf) router.Handle("DELETE", "/api/ssh/servers/:id/host-keys/:hostKeyId", api.DeleteSSHServerHostKeyForSelf) router.Handle("GET", "/api/ssh/credentials", api.ListSSHCredentialsForSelf) router.Handle("POST", "/api/ssh/credentials", api.CreateSSHCredentialForSelf) router.Handle("PATCH", "/api/ssh/credentials/:id", api.UpdateSSHCredentialForSelf) router.Handle("DELETE", "/api/ssh/credentials/:id", api.DeleteSSHCredentialForSelf) router.Handle("GET", "/api/ssh/access-profiles", api.ListSSHAccessProfilesForSelf) router.Handle("POST", "/api/ssh/access-profiles", api.CreateSSHAccessProfileForSelf) router.Handle("GET", "/api/ssh/access-profiles/:id", api.GetSSHAccessProfileForSelf) router.Handle("GET", "/api/ssh/access-profiles/:id/credential", api.GetSSHAccessProfileCredentialForSelf) router.Handle("GET", "/api/ssh/access-profiles/:id/servers", api.ListSSHAccessProfileServersForSelf) router.Handle("PATCH", "/api/ssh/access-profiles/:id", api.UpdateSSHAccessProfileForSelf) router.Handle("DELETE", "/api/ssh/access-profiles/:id", api.DeleteSSHAccessProfileForSelf) router.Handle("POST", "/api/ssh/access-profiles/:id/connect", api.CreateSSHSessionForSelf) router.Handle("GET", "/api/ssh/sessions", api.ListSSHSessionsForSelf) router.Handle("GET", "/api/ssh/stream", api.StreamSSHWorkspaceForSelf) router.Handle("GET", "/api/ssh/sessions/:id", api.GetSSHSessionForSelf) router.Handle("GET", "/api/ssh/sessions/:id/transcript", api.GetSSHSessionTranscriptForSelf) router.Handle("POST", "/api/ssh/sessions/:id/disconnect", api.DisconnectSSHSessionForSelf) router.Handle("POST", "/api/ssh/sessions/:id/files/upload", api.UploadSSHSessionFilesForSelf) router.Handle("POST", "/api/ssh/sessions/:id/files/download", api.DownloadSSHSessionFilesForSelf) router.Handle("POST", "/api/ssh/sessions/:id/files/copy-to", api.CopySSHSessionFilesForSelf) router.Handle("POST", "/api/ssh/cert/inspect", api.InspectSSHCertificate) router.Handle("POST", "/api/ssh/user-cas/:id/sign", api.SignSSHUserKeyForSelf) router.Handle("GET", "/api/ssh/issuances", api.ListSSHUserCAIssuancesForSelf) router.Handle("GET", "/api/pki/cas/:id/crl", api.GetPKICRLForSelf) router.Handle("GET", "/api/pki/cas/:id", api.GetPKICAForSelf) router.Handle("GET", "/api/pki/client/profiles", api.ListPKIClientProfilesForSelf) router.Handle("GET", "/api/pki/client/issuances", api.ListPKIClientIssuancesForSelf) router.Handle("POST", "/api/pki/client/certs", api.IssuePKIClientCertForSelf) router.Handle("GET", "/api/pki/client/certs/:id", api.GetPKIClientCertForSelf) router.Handle("GET", "/api/pki/client/certs/:id/inspect", api.GetPKIClientCertInspectForSelf) router.Handle("GET", "/api/pki/client/certs/:id/bundle", api.DownloadPKIClientCertBundleForSelf) router.Handle("POST", "/api/pki/client/certs/:id/revoke", api.RevokePKIClientCertForSelf) router.Handle("GET", "/api/projects", api.ListProjects) router.Handle("POST", "/api/projects", api.CreateProject) router.Handle("GET", "/api/projects/:id", api.GetProject) router.Handle("PATCH", "/api/projects/:id", api.UpdateProject) router.Handle("DELETE", "/api/projects/:id", api.DeleteProject) router.Handle("GET", "/api/projects/:projectId/members", api.ListProjectMembers) router.Handle("GET", "/api/projects/:projectId/member-candidates", api.ListProjectMemberCandidates) router.Handle("GET", "/api/projects/:projectId/group-candidates", api.ListProjectGroupCandidates) router.Handle("POST", "/api/projects/:projectId/members", api.AddProjectMember) router.Handle("PATCH", "/api/projects/:projectId/members", api.UpdateProjectMember) router.Handle("DELETE", "/api/projects/:projectId/members/:userId", api.RemoveProjectMember) router.Handle("GET", "/api/projects/:projectId/group-roles", api.ListProjectGroupRoles) router.Handle("POST", "/api/projects/:projectId/group-roles", api.UpsertProjectGroupRole) router.Handle("DELETE", "/api/projects/:projectId/group-roles/:groupId", api.RemoveProjectGroupRole) router.Handle("GET", "/api/projects/:projectId/repos", api.ListRepos) router.Handle("POST", "/api/projects/:projectId/repos", api.CreateRepo) router.Handle("GET", "/api/repos/types", api.RepoTypes) router.Handle("GET", "/api/repos", api.ListAllRepos) router.Handle("GET", "/api/projects/:projectId/foreign-repos/available", api.ListAvailableRepos) router.Handle("POST", "/api/projects/:projectId/foreign-repos", api.AttachForeignRepo) router.Handle("DELETE", "/api/projects/:projectId/foreign-repos/:repoId", api.DetachForeignRepo) router.Handle("GET", "/api/repos/:id", api.GetRepo) router.Handle("PATCH", "/api/repos/:id", api.UpdateRepo) router.Handle("DELETE", "/api/repos/:id", api.DeleteRepo) router.Handle("GET", "/api/repos/:id/branches", api.RepoBranches) router.Handle("GET", "/api/repos/:id/branches/info", api.RepoBranchesInfo) router.Handle("PUT", "/api/repos/:id/branches/default", api.RepoSetDefaultBranch) router.Handle("POST", "/api/repos/:id/branches/rename", api.RepoRenameBranch) router.Handle("POST", "/api/repos/:id/branches/delete", api.RepoDeleteBranch) router.Handle("POST", "/api/repos/:id/branches/create", api.RepoCreateBranch) router.Handle("GET", "/api/repos/:id/commits", api.RepoCommits) router.Handle("GET", "/api/repos/:id/tree", api.RepoTree) router.Handle("GET", "/api/repos/:id/blob", api.RepoBlob) router.Handle("GET", "/api/repos/:id/blob/raw", api.RepoBlobRaw) router.Handle("GET", "/api/repos/:id/history", api.RepoFileHistory) router.Handle("GET", "/api/repos/:id/diff", api.RepoFileDiff) router.Handle("GET", "/api/repos/:id/commit", api.RepoCommitDetail) router.Handle("GET", "/api/repos/:id/commit/diff", api.RepoCommitDiff) router.Handle("GET", "/api/repos/:id/compare", api.RepoCompare) router.Handle("GET", "/api/repos/:id/stats", api.RepoStats) router.Handle("GET", "/api/repos/:id/rpm/packages", api.RepoRPMPackages) router.Handle("GET", "/api/repos/:id/rpm/package", api.RepoRPMPackage) router.Handle("POST", "/api/repos/:id/rpm/subdirs", api.RepoRPMCreateSubdir) router.Handle("GET", "/api/repos/:id/rpm/subdir", api.RepoRPMGetSubdir) router.Handle("POST", "/api/repos/:id/rpm/subdir/update", api.RepoRPMRenameSubdir) router.Handle("POST", "/api/repos/:id/rpm/subdir/rename", api.RepoRPMRenameSubdir) router.Handle("POST", "/api/repos/:id/rpm/subdir/sync", api.RepoRPMSyncSubdir) router.Handle("POST", "/api/repos/:id/rpm/subdir/suspend", api.RepoRPMSuspendSubdir) router.Handle("POST", "/api/repos/:id/rpm/subdir/resume", api.RepoRPMResumeSubdir) router.Handle("POST", "/api/repos/:id/rpm/subdir/rebuild-metadata", api.RepoRPMRebuildSubdirMetadata) router.Handle("POST", "/api/repos/:id/rpm/subdir/cancel", api.RepoRPMCancelSubdirSync) router.Handle("GET", "/api/repos/:id/rpm/subdir/runs", api.RepoRPMMirrorRuns) router.Handle("DELETE", "/api/repos/:id/rpm/subdir/runs", api.RepoRPMClearMirrorRuns) router.Handle("DELETE", "/api/repos/:id/rpm/subdir", api.RepoRPMDeleteSubdir) router.Handle("DELETE", "/api/repos/:id/rpm/file", api.RepoRPMDeleteFile) router.Handle("GET", "/api/repos/:id/rpm/file", api.RepoRPMFile) router.Handle("GET", "/api/repos/:id/rpm/tree", api.RepoRPMTree) router.Handle("POST", "/api/repos/:id/rpm/upload", api.RepoRPMUpload) router.Handle("GET", "/api/repos/:id/docker/images", api.RepoDockerImages) router.Handle("GET", "/api/repos/:id/docker/tags", api.RepoDockerTags) router.Handle("GET", "/api/repos/:id/docker/manifest", api.RepoDockerManifest) router.Handle("DELETE", "/api/repos/:id/docker/tag", api.RepoDockerDeleteTag) router.Handle("DELETE", "/api/repos/:id/docker/image", api.RepoDockerDeleteImage) router.Handle("POST", "/api/repos/:id/docker/tag/rename", api.RepoDockerRenameTag) router.Handle("POST", "/api/repos/:id/docker/image/rename", api.RepoDockerRenameImage) router.Handle("GET", "/api/me/keys", api.ListAPIKeys) router.Handle("POST", "/api/me/keys", api.CreateAPIKey) router.Handle("DELETE", "/api/me/keys/:id", api.DeleteAPIKey) router.Handle("POST", "/api/me/keys/:id/disable", api.DisableAPIKey) router.Handle("POST", "/api/me/keys/:id/enable", api.EnableAPIKey) router.Handle("GET", "/api/admin/api-keys", api.ListAdminAPIKeys) router.Handle("DELETE", "/api/admin/api-keys/:id", api.DeleteAdminAPIKey) router.Handle("POST", "/api/admin/api-keys/:id/disable", api.DisableAdminAPIKey) router.Handle("POST", "/api/admin/api-keys/:id/enable", api.EnableAdminAPIKey) router.Handle("GET", "/api/projects/:projectId/issues", api.ListIssues) router.Handle("POST", "/api/projects/:projectId/issues", api.CreateIssue) router.Handle("PATCH", "/api/issues/:id", api.UpdateIssue) router.Handle("POST", "/api/issues/:id/comments", api.AddIssueComment) router.Handle("GET", "/api/projects/:projectId/wiki/pages", api.ListWikiPages) router.Handle("POST", "/api/projects/:projectId/wiki/pages", api.CreateWikiPage) router.Handle("PATCH", "/api/wiki/pages/:id", api.UpdateWikiPage) router.Handle("POST", "/api/projects/:projectId/uploads", api.UploadFile) router.Handle("GET", "/api/projects/:projectId/uploads", api.ListUploads) router.Handle("GET", "/api/uploads/:id", api.DownloadFile) // --------------------------------------------------------------- var mux *http.ServeMux var apiPublicHandler http.Handler var apiHandler http.Handler var gitHandler http.Handler var gitIDHandler http.Handler var rpmHandler http.Handler var rpmIDHandler http.Handler var dockerHandler http.Handler var gitRewrite http.Handler var gitIDRewrite http.Handler var rpmRewrite http.Handler var rpmIDRewrite http.Handler mux = http.NewServeMux() gitRewrite = &gitPathRewriteHandler{next: app.git_server, store: store} gitHandler = withServiceAuth(serviceGit, cfg.GitHTTPPrefix, http.StripPrefix(cfg.GitHTTPPrefix, gitRewrite), auth_user, store, logger) gitHandler = middleware.WithStoreTransactionUnbuffered(store, gitHandler) gitHandler = middleware.WithRequestStore(store, gitHandler) mux.Handle(cfg.GitHTTPPrefix+"/", gitHandler) gitIDRewrite = &gitIDPathRewriteHandler{next: app.git_server, store: store} gitIDHandler = withServiceAuth(serviceGit, cfg.GitHTTPPrefix+"-id", http.StripPrefix(cfg.GitHTTPPrefix+"-id", gitIDRewrite), auth_user, store, logger) gitIDHandler = middleware.WithStoreTransactionUnbuffered(store, gitIDHandler) gitIDHandler = middleware.WithRequestStore(store, gitIDHandler) mux.Handle(cfg.GitHTTPPrefix+"-id/", gitIDHandler) rpmRewrite = &rpmPathRewriteHandler{next: app.rpm_server, store: store} rpmHandler = withServiceAuth(serviceRPM, cfg.RPMHTTPPrefix, http.StripPrefix(cfg.RPMHTTPPrefix, rpmRewrite), auth_user, store, logger) rpmHandler = middleware.WithStoreTransactionUnbuffered(store, rpmHandler) rpmHandler = middleware.WithRequestStore(store, rpmHandler) mux.Handle(cfg.RPMHTTPPrefix+"/", rpmHandler) rpmIDRewrite = &rpmIDPathRewriteHandler{next: app.rpm_server, store: store} rpmIDHandler = withServiceAuth(serviceRPM, cfg.RPMHTTPPrefix+"-id", http.StripPrefix(cfg.RPMHTTPPrefix+"-id", rpmIDRewrite), auth_user, store, logger) rpmIDHandler = middleware.WithStoreTransactionUnbuffered(store, rpmIDHandler) rpmIDHandler = middleware.WithRequestStore(store, rpmIDHandler) mux.Handle(cfg.RPMHTTPPrefix+"-id/", rpmIDHandler) dockerHandler = withServiceAuth(serviceV2, cfg.DockerHTTPPrefix, app.docker_server, auth_user, store, logger) dockerHandler = middleware.WithStoreTransactionUnbuffered(store, dockerHandler) dockerHandler = middleware.WithRequestStore(store, dockerHandler) mux.Handle(cfg.DockerHTTPPrefix, dockerHandler) mux.Handle(cfg.DockerHTTPPrefix+"/", dockerHandler) mux.HandleFunc("/pki/crl/", api.ServePKICRL) // apiHandler = middleware.WithRequestStore(store, // middleware.AccessLog(logger, // middleware.WithStoreTransaction(store, // middleware.WithUser(store, // withAPIAuth(router, auth_user, store, logger))))) apiHandler = withAPIAuth(router, auth_user, store, logger) apiHandler = middleware.RequireInteractiveTOTP(apiHandler) apiHandler = middleware.WithUserCookie(store, sessionCookieNameFromServerId(app.serverId), apiHandler) apiHandler = middleware.WithStoreTransaction(store, apiHandler) apiHandler = middleware.AccessLog(logger, apiHandler) apiHandler = middleware.WithRequestStore(store, apiHandler) apiHandler = middleware.WithCors(apiHandler) mux.Handle("/api/", apiHandler) mux.Handle("/api/health", middleware.AccessLog(logger, router)) // apiPublicHandler = middleware.WithRequestStore(store, // middleware.AccessLog(logger, // middleware.WithStoreTransaction(store, // middleware.WithUser(store, router)))) apiPublicHandler = middleware.WithUserCookie(store, sessionCookieNameFromServerId(app.serverId), router) apiPublicHandler = middleware.WithStoreTransaction(store, apiPublicHandler) apiPublicHandler = middleware.AccessLog(logger, apiPublicHandler) apiPublicHandler = middleware.WithRequestStore(store, apiPublicHandler) apiPublicHandler = middleware.WithCors(apiPublicHandler) mux.Handle("/api/login", apiPublicHandler) mux.Handle("/api/logout", apiPublicHandler) mux.Handle("/api/auth/oidc/enabled", apiPublicHandler) mux.Handle("/api/auth/oidc/login", apiPublicHandler) mux.Handle("/api/auth/oidc/callback", apiPublicHandler) mux.Handle("/api/app-info", apiPublicHandler) mux.Handle("/", middleware.WithUserCookie(store, sessionCookieNameFromServerId(app.serverId), spaHandler(cfg.FrontendDir, logger, app.serverId, TitleFromServerId(app.serverId), app.siteName))) return api, mux, nil } type listenerEndpoint struct { Name string Key string Addr string IsHTTPS bool TLSConfig *tls.Config ListenerKind string ListenerID string } type ctxKey string const listenerIdentityKey ctxKey = "listener_identity" type serviceKind string const ( serviceAPI serviceKind = "api" serviceGit serviceKind = "git" serviceRPM serviceKind = "rpm" serviceV2 serviceKind = "v2" // docker registry ) type opKind string const ( opRead opKind = "read" opWrite opKind = "write" ) type authOutcome string const ( authRequireAuth authOutcome = "require_auth" authAllow authOutcome = "allow" authRequireCert authOutcome = "require_cert" authCertOrAuth authOutcome = "require_cert_or_auth" authDeny authOutcome = "deny" ) type listenerAuthPolicy struct { ReadMode string WriteMode string RequirePrincipalForWrite bool CertAllowlist map[string]bool ClientCertRequiredPermissions []string ClientCertPermissionMatch string ClientCertRequiredScope string } type listenerIdentity struct { Kind string ID string } type runningListener struct { Endpoint listenerEndpoint Server *http.Server } type additionalListenerManager struct { Store *db.Store Handler http.Handler Logger Logger mu sync.Mutex Running map[string]runningListener Reload chan struct{} StopCh chan struct{} StopOnce sync.Once wg sync.WaitGroup } func newAdditionalListenerManager(store *db.Store, handler http.Handler, logger Logger) *additionalListenerManager { var manager *additionalListenerManager manager = &additionalListenerManager{ Store: store, Handler: handler, Logger: logger, Running: make(map[string]runningListener), Reload: make(chan struct{}, 1), StopCh: make(chan struct{}), } return manager } func (m *additionalListenerManager) NotifyReload() { select { case m.Reload <- struct{}{}: default: } } func (m *additionalListenerManager) ListenerEndpointCounts() map[string]int { var out map[string]int var key string var parts []string var listenerID string out = make(map[string]int) m.mu.Lock() for key = range m.Running { parts = strings.SplitN(key, ":", 5) if len(parts) < 3 { continue } listenerID = strings.TrimSpace(parts[1]) if listenerID == "" { continue } out[listenerID] = out[listenerID] + 1 } m.mu.Unlock() return out } func (m *additionalListenerManager) TotalEndpointCount() int { var count int if m == nil { return 0 } m.mu.Lock() count = len(m.Running) m.mu.Unlock() return count } func (m *additionalListenerManager) Start() error { var err error var i int for i = 0; i < 30; i++ { err = m.reconcile() if err == nil { break } if !isSQLiteBusyError(err) { return err } time.Sleep(100 * time.Millisecond) } if err != nil { return err } m.wg.Add(1) go func() { var reconcileErr error defer m.wg.Done() for { select { case <-m.StopCh: return case <-m.Reload: reconcileErr = m.reconcile() if reconcileErr != nil { m.Logger.Write("", LOG_ERROR, "additional listener reconcile error - %v", reconcileErr) } } } }() return nil } func (m *additionalListenerManager) Stop(ctx context.Context) { var done chan struct{} var key string var running runningListener var shutdownCtx context.Context var cancel context.CancelFunc if m == nil { return } m.StopOnce.Do(func() { close(m.StopCh) }) m.mu.Lock() for key, running = range m.Running { shutdownCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second) _ = running.Server.Shutdown(shutdownCtx) cancel() delete(m.Running, key) } m.mu.Unlock() done = make(chan struct{}) go func() { m.wg.Wait() close(done) }() select { case <-done: case <-ctx.Done(): } } func isSQLiteBusyError(err error) bool { var msg string if err == nil { return false } msg = strings.ToLower(err.Error()) return strings.Contains(msg, "database is locked") || strings.Contains(msg, "sqlite_busy") } func (m *additionalListenerManager) reconcile() error { var listeners []models.TLSListener var desired map[string]listenerEndpoint var err error var i int var key string var configToken string var endpoint listenerEndpoint var running runningListener var shutdownCtx context.Context var cancel context.CancelFunc var more []listenerEndpoint var j int var settings models.TLSSettings listeners, err = m.Store.ListTLSListeners() if err != nil { return err } desired = make(map[string]listenerEndpoint) for i = 0; i < len(listeners); i++ { if !listeners[i].Enabled { continue } settings = models.TLSSettings{ HTTPAddrs: listeners[i].HTTPAddrs, HTTPSAddrs: listeners[i].HTTPSAddrs, TLSServerCertSource: listeners[i].TLSServerCertSource, TLSCertFile: listeners[i].TLSCertFile, TLSKeyFile: listeners[i].TLSKeyFile, TLSPKIServerCertID: listeners[i].TLSPKIServerCertID, TLSClientAuth: listeners[i].TLSClientAuth, TLSClientCAFile: listeners[i].TLSClientCAFile, TLSPKIClientCAID: listeners[i].TLSPKIClientCAID, TLSMinVersion: listeners[i].TLSMinVersion, } more, err = buildListenerEndpoints(listeners[i].Name, "extra", listeners[i].ID, settings, m.Store) if err != nil { return err } for j = 0; j < len(more); j++ { configToken = listenerEndpointConfigToken(settings, more[j].IsHTTPS) key = "extra:" + listeners[i].ID + ":" + configToken + ":" + endpointScheme(more[j]) + ":" + more[j].Addr more[j].Key = key desired[key] = more[j] } } m.mu.Lock() // among the running listeners, stop all disabled/invalid listeners. for key, running = range m.Running { var exists bool _, exists = desired[key] if exists { continue } // found in configuration shutdownCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second) _ = running.Server.Shutdown(shutdownCtx) cancel() delete(m.Running, key) m.Logger.Write("", LOG_INFO, "additional listener stopped name=%s addr=%s", running.Endpoint.Name, running.Endpoint.Addr) } // start the listeners if they are not already running for key, endpoint = range desired { var exists bool _, exists = m.Running[key] if exists { continue } // already running running = m.startEndpoint(endpoint) m.Running[key] = running } m.mu.Unlock() return nil } func (m *additionalListenerManager) startEndpoint(endpoint listenerEndpoint) runningListener { var server *http.Server var running runningListener server = &http.Server{ Addr: endpoint.Addr, Handler: m.Handler, ConnContext: connContextWithListenerIdentity(endpoint.ListenerKind, endpoint.ListenerID), ErrorLog: log.New(&server_http_log_writer{l: m.Logger, id: endpoint.Name, depth: +2}, "", 0), } if endpoint.IsHTTPS { server.TLSConfig = endpoint.TLSConfig } running = runningListener{ Endpoint: endpoint, Server: server, } if endpoint.IsHTTPS { m.Logger.Write(endpoint.Name, LOG_INFO, "additional listener started https://%s", endpoint.Addr) } else { m.Logger.Write(endpoint.Name, LOG_INFO, "additional listener started %s", endpoint.Addr) } m.wg.Add(1) go func(ep listenerEndpoint, srv *http.Server) { var err error var listener net.Listener var network string defer m.wg.Done() network = tcpAddrStrClass(ep.Addr) listener, err = net.Listen(network, ep.Addr) if err != nil { m.Logger.Write(ep.Name, LOG_ERROR, "additional listener failed network=%s addr=%s err=%v", network, ep.Addr, err) return } if ep.IsHTTPS { err = srv.ServeTLS(listener, "", "") } else { err = srv.Serve(listener) } if err != nil && !errors.Is(err, http.ErrServerClosed) { m.Logger.Write(ep.Name, LOG_ERROR, "additional listener failed network=%s addr=%s err=%v", network, ep.Addr, err) } }(endpoint, server) return running } func endpointScheme(endpoint listenerEndpoint) string { if endpoint.IsHTTPS { return "https" } return "http" } func listenerEndpointConfigToken(settings models.TLSSettings, isHTTPS bool) string { var values []string var sum [32]byte if !isHTTPS { return "http" } values = append(values, strings.TrimSpace(settings.TLSServerCertSource)) values = append(values, strings.TrimSpace(settings.TLSCertFile)) values = append(values, strings.TrimSpace(settings.TLSKeyFile)) values = append(values, strings.TrimSpace(settings.TLSPKIServerCertID)) values = append(values, strings.TrimSpace(settings.TLSClientAuth)) values = append(values, strings.TrimSpace(settings.TLSClientCAFile)) values = append(values, strings.TrimSpace(settings.TLSPKIClientCAID)) values = append(values, strings.TrimSpace(settings.TLSMinVersion)) sum = sha256.Sum256([]byte(strings.Join(values, "\n"))) return hex.EncodeToString(sum[:8]) } func connContextWithListenerIdentity(kind string, id string) func(ctx context.Context, conn net.Conn) context.Context { return func(ctx context.Context, _ net.Conn) context.Context { return context.WithValue(ctx, listenerIdentityKey, listenerIdentity{Kind: kind, ID: id}) } } func listenerIdentityFromRequest(r *http.Request) listenerIdentity { var identity listenerIdentity var value any var ok bool identity = listenerIdentity{Kind: "main", ID: ""} if r == nil { return identity } value = r.Context().Value(listenerIdentityKey) identity, ok = value.(listenerIdentity) if !ok { return identity } if strings.TrimSpace(identity.Kind) == "" { identity.Kind = "main" } return identity } func listenerPolicyFromRequest(r *http.Request, service serviceKind, store *db.Store) (listenerAuthPolicy, error) { var identity listenerIdentity var item models.TLSAuthPolicy var policy listenerAuthPolicy var err error identity = listenerIdentityFromRequest(r) item, err = store.ResolveTLSAuthPolicy(identity.Kind, identity.ID, string(service)) if err != nil { return defaultListenerPolicy(), err } policy, err = compileListenerAuthPolicy(item, store) if err != nil { return defaultListenerPolicy(), err } return policy, nil } func evaluateAuthOutcome(policy listenerAuthPolicy, operation opKind) authOutcome { var mode string mode = policy.ReadMode if operation == opWrite { mode = policy.WriteMode } switch strings.ToLower(strings.TrimSpace(mode)) { case "public": return authAllow case "cert": return authRequireCert case "cert_or_auth": return authCertOrAuth case "deny": return authDeny default: return authRequireAuth } } func requestClientCertFingerprint(r *http.Request) string { var cert *x509.Certificate var hash [32]byte if r == nil || r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { return "" } cert = r.TLS.PeerCertificates[0] hash = sha256.Sum256(cert.Raw) return strings.ToLower(hex.EncodeToString(hash[:])) } func isClientCertAllowed(fp string, policy listenerAuthPolicy) bool { if len(policy.CertAllowlist) == 0 { return true } return policy.CertAllowlist[fp] } func requestStore(r *http.Request, store *db.Store) *db.Store { var requestStore *db.Store var ok bool if r != nil { requestStore, ok = middleware.StoreFromContext(r.Context()) if ok && requestStore != nil { return requestStore } } return store } func authenticateByBasic(r *http.Request, auth func(store *db.Store, username string, password string) (bool, error), store *db.Store) bool { var username string var password string var ok bool var allowed bool var err error if auth == nil { return false } username, password, ok = r.BasicAuth() if !ok { return false } allowed, err = auth(store, username, password) if err != nil { return false } return allowed } func withServiceAuth(service serviceKind, servicePrefix string, next http.Handler, auth func(store *db.Store, username string, password string) (bool, error), store *db.Store, logger Logger) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var policy listenerAuthPolicy var operation opKind var outcome authOutcome var currentStore *db.Store var ok bool var certReason string var mtlsInfo middleware.ClientCertLogInfo var principal models.ServicePrincipal var principalOK bool var err error currentStore = requestStore(r, store) policy, err = listenerPolicyFromRequest(r, service, currentStore) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } operation = classifyServiceOperation(service, r) outcome = evaluateAuthOutcome(policy, operation) if outcome == authDeny { middleware.RememberAuthReason(r.Context(), "policy_denied") mtlsInfo = middleware.ClientCertLogFields(r) logger.Write(string(service), LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, "policy_denied") w.WriteHeader(http.StatusForbidden) return } if outcome == authAllow { next.ServeHTTP(w, r) return } if outcome == authRequireCert { ok, principal, principalOK, certReason, err = authorizeByClientCert(service, servicePrefix, r, policy, operation, currentStore) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if ok { if principalOK { r = middleware.WithPrincipal(r, principal) } next.ServeHTTP(w, r) return } middleware.RememberAuthReason(r.Context(), certReason) mtlsInfo = middleware.ClientCertLogFields(r) logger.Write(string(service), LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, certReason) w.WriteHeader(http.StatusForbidden) return } if outcome == authCertOrAuth { ok, principal, principalOK, certReason, err = authorizeByClientCert(service, servicePrefix, r, policy, operation, currentStore) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if ok { if principalOK { r = middleware.WithPrincipal(r, principal) } next.ServeHTTP(w, r) return } } ok = authenticateByBasic(r, auth, currentStore) if ok { next.ServeHTTP(w, r) return } if service == serviceGit { w.Header().Set("WWW-Authenticate", `Basic realm="git"`) } else if service == serviceRPM { w.Header().Set("WWW-Authenticate", `Basic realm="rpm"`) } else if service == serviceV2 { w.Header().Set("WWW-Authenticate", `Basic realm="docker"`) } if strings.TrimSpace(certReason) == "" { certReason = "auth_failed" } middleware.RememberAuthReason(r.Context(), certReason) mtlsInfo = middleware.ClientCertLogFields(r) logger.Write(string(service), LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, certReason) w.WriteHeader(http.StatusUnauthorized) }) } func withAPIAuth(next http.Handler, auth func(store *db.Store, username string, password string) (bool, error), store *db.Store, logger Logger) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var policy listenerAuthPolicy var operation opKind var outcome authOutcome var currentStore *db.Store var certReason string var mtlsInfo middleware.ClientCertLogInfo var user models.User var hasUser bool var hasPrincipal bool var ok bool var principal models.ServicePrincipal var principalOK bool var err error currentStore = requestStore(r, store) policy, err = listenerPolicyFromRequest(r, serviceAPI, currentStore) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } operation = classifyServiceOperation(serviceAPI, r) outcome = evaluateAuthOutcome(policy, operation) user, hasUser = middleware.UserFromContext(r.Context()) if user.Disabled { hasUser = false } principal, hasPrincipal = middleware.PrincipalFromContext(r.Context()) if principal.Disabled { hasPrincipal = false } if outcome == authDeny { middleware.RememberAuthReason(r.Context(), "policy_denied") mtlsInfo = middleware.ClientCertLogFields(r) logger.Write(logIDAPI, LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, "policy_denied") w.WriteHeader(http.StatusForbidden) return } if outcome == authAllow { next.ServeHTTP(w, r) return } if outcome == authRequireCert { ok, principal, principalOK, certReason, err = authorizeByClientCert(serviceAPI, "", r, policy, operation, currentStore) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if ok { if principalOK { r = middleware.WithPrincipal(r, principal) } next.ServeHTTP(w, r) return } middleware.RememberAuthReason(r.Context(), certReason) mtlsInfo = middleware.ClientCertLogFields(r) logger.Write(logIDAPI, LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, certReason) w.WriteHeader(http.StatusForbidden) return } if outcome == authCertOrAuth { ok, principal, principalOK, certReason, err = authorizeByClientCert(serviceAPI, "", r, policy, operation, currentStore) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if ok || hasUser || hasPrincipal { if principalOK { r = middleware.WithPrincipal(r, principal) } next.ServeHTTP(w, r) return } if authenticateByBasic(r, auth, currentStore) { next.ServeHTTP(w, r) return } if strings.TrimSpace(certReason) == "" { certReason = "auth_failed" } middleware.RememberAuthReason(r.Context(), certReason) w.WriteHeader(http.StatusUnauthorized) return } if hasUser || hasPrincipal || authenticateByBasic(r, auth, currentStore) { next.ServeHTTP(w, r) return } if strings.TrimSpace(certReason) == "" { certReason = "auth_failed" } middleware.RememberAuthReason(r.Context(), certReason) mtlsInfo = middleware.ClientCertLogFields(r) logger.Write(logIDAPI, LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, certReason) w.WriteHeader(http.StatusUnauthorized) }) } func classifyServiceOperation(service serviceKind, r *http.Request) opKind { var serviceName string serviceName = "" if r != nil { serviceName = r.URL.Query().Get("service") } if service == serviceGit { if strings.Contains(r.URL.Path, "git-receive-pack") || serviceName == "git-receive-pack" { return opWrite } return opRead } if service == serviceAPI { if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions { return opRead } return opWrite } if service == serviceRPM { if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions { return opRead } return opWrite } if service == serviceV2 { if r.Method == http.MethodGet || r.Method == http.MethodHead { return opRead } if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch || r.Method == http.MethodDelete { return opWrite } } return opRead } func authorizeByClientCert(service serviceKind, servicePrefix string, r *http.Request, policy listenerAuthPolicy, operation opKind, store *db.Store) (bool, models.ServicePrincipal, bool, string, error) { var fp string var cert *x509.Certificate var info pkiutil.ClientAuthorizationInfo var hasInfo bool var principal models.ServicePrincipal var bound bool var allowed bool var reason string var err error fp = requestClientCertFingerprint(r) if fp == "" { return false, principal, false, "missing_client_cert", nil } if !isClientCertAllowed(fp, policy) { return false, principal, false, "client_cert_not_allowed", nil } cert = requestClientCertificate(r) info, hasInfo, err = pkiutil.ParseClientAuthorizationFromCertificate(cert) if err != nil { return false, principal, false, "client_cert_extension_error", err } allowed, reason, err = isClientCertAuthorizationAllowed(info, hasInfo, policy) if err != nil { return false, principal, false, reason, err } if !allowed { return false, principal, false, reason, nil } if operation == opRead { return true, principal, false, "", nil } if !policy.RequirePrincipalForWrite { return true, principal, false, "", nil } principal, bound, err = store.GetPrincipalByCertFingerprint(fp) if err != nil || !bound { if err != nil { return false, principal, false, "principal_binding_lookup_failed", err } return false, principal, false, "client_cert_principal_not_bound", nil } if service == serviceAPI { return true, principal, true, "", nil } allowed, err = principalCanWriteService(service, servicePrefix, principal, r, store) if err != nil || !allowed { if err != nil { return false, principal, false, "principal_write_check_failed", err } return false, principal, false, "principal_write_denied", nil } return true, principal, true, "", nil } func requestClientCertificate(r *http.Request) *x509.Certificate { if r == nil || r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { return nil } return r.TLS.PeerCertificates[0] } func isClientCertAuthorizationAllowed(info pkiutil.ClientAuthorizationInfo, hasInfo bool, policy listenerAuthPolicy) (bool, string, error) { var needsExtension bool needsExtension = len(policy.ClientCertRequiredPermissions) > 0 || strings.TrimSpace(policy.ClientCertRequiredScope) != "" if !needsExtension { return true, "", nil } if !hasInfo { return false, "client_cert_extension_missing", nil } if !clientCertPermissionsAllowed(info.Permissions, policy.ClientCertRequiredPermissions, policy.ClientCertPermissionMatch) { return false, "client_cert_permission_mismatch", nil } if strings.TrimSpace(policy.ClientCertRequiredScope) != "" && strings.TrimSpace(info.Scope) != strings.TrimSpace(policy.ClientCertRequiredScope) { return false, "client_cert_scope_mismatch", nil } return true, "", nil } func clientCertPermissionsAllowed(actual []string, required []string, matchMode string) bool { var normalized map[string]bool var i int var item string if len(required) == 0 { return true } normalized = make(map[string]bool) for i = 0; i < len(actual); i++ { item = strings.ToLower(strings.TrimSpace(actual[i])) if item == "" { continue } normalized[item] = true } if strings.TrimSpace(matchMode) == "any" { for i = 0; i < len(required); i++ { item = strings.ToLower(strings.TrimSpace(required[i])) if item == "" { continue } if normalized[item] { return true } } return false } for i = 0; i < len(required); i++ { item = strings.ToLower(strings.TrimSpace(required[i])) if item == "" { continue } if !normalized[item] { return false } } return true } func principalCanWriteService(service serviceKind, servicePrefix string, principal models.ServicePrincipal, r *http.Request, store *db.Store) (bool, error) { var projectID string var role string var err error if principal.IsAdmin { return true, nil } projectID, err = resolveProjectIDForServiceWrite(service, servicePrefix, r, store) if err != nil || strings.TrimSpace(projectID) == "" { return false, nil } role, err = store.GetPrincipalProjectRole(principal.ID, projectID) if err != nil { return false, nil } if !roleAllowsWrite(role) { return false, nil } return true, nil } func roleAllowsWrite(role string) bool { var r string r = strings.ToLower(strings.TrimSpace(role)) return r == "writer" || r == "admin" } func resolveProjectIDForServiceWrite(service serviceKind, servicePrefix string, r *http.Request, store *db.Store) (string, error) { var path string var parts []string var first string var second string var repo models.Repo var project models.Project var err error path = strings.Trim(strings.TrimSpace(r.URL.Path), "/") servicePrefix = strings.Trim(codit_config.NormalizeHTTPPrefix(servicePrefix, ""), "/") if servicePrefix != "" && strings.HasPrefix(path, servicePrefix + "/") { path = strings.TrimPrefix(path, servicePrefix + "/") } if service == serviceV2 { parts = strings.Split(path, "/") if len(parts) < 2 { return "", errors.New("invalid v2 path") } project, err = store.GetProjectBySlug(parts[0]) if err != nil { return "", err } repo, err = store.GetRepoByProjectNameType(project.ID, parts[1], "docker") if err != nil { return "", err } return repo.ProjectID, nil } if service != serviceGit && service != serviceRPM { return "", errors.New("unsupported service") } parts = strings.Split(path, "/") if len(parts) < 1 { return "", errors.New("invalid path") } first = strings.TrimSuffix(parts[0], ".git") repo, err = store.GetRepo(first) if err == nil { return repo.ProjectID, nil } if len(parts) < 2 { return "", errors.New("invalid path") } second = strings.TrimSuffix(parts[1], ".git") project, err = store.GetProjectBySlug(parts[0]) if err != nil { return "", err } repo, err = store.GetRepoByProjectNameType(project.ID, second, serviceRepoType(service, parts[1])) if err != nil { return "", err } return repo.ProjectID, nil } func serviceRepoType(service serviceKind, segment string) string { if service == serviceGit || strings.HasSuffix(segment, ".git") { return "git" } return "rpm" } func defaultListenerPolicy() listenerAuthPolicy { var policy listenerAuthPolicy policy = listenerAuthPolicy{ ReadMode: "auth", WriteMode: "auth", RequirePrincipalForWrite: true, CertAllowlist: make(map[string]bool), ClientCertPermissionMatch: "all", ClientCertRequiredPermissions: []string{}, } return policy } func compileListenerAuthPolicy(item models.TLSAuthPolicy, store *db.Store) (listenerAuthPolicy, error) { var policy listenerAuthPolicy var i int var fp string var cert models.PKICert var err error policy = defaultListenerPolicy() policy.ReadMode = strings.ToLower(strings.TrimSpace(item.ReadMode)) if policy.ReadMode == "" { policy.ReadMode = "auth" } policy.WriteMode = strings.ToLower(strings.TrimSpace(item.WriteMode)) if policy.WriteMode == "" { policy.WriteMode = "auth" } policy.RequirePrincipalForWrite = item.RequirePrincipalForWrite policy.ClientCertRequiredPermissions = append(policy.ClientCertRequiredPermissions, item.RequiredPermissions...) policy.ClientCertPermissionMatch = strings.TrimSpace(item.PermissionMatch) if policy.ClientCertPermissionMatch == "" { policy.ClientCertPermissionMatch = "all" } policy.ClientCertRequiredScope = strings.TrimSpace(item.RequiredScope) for i = 0; i < len(item.AllowedCertFingerprints); i++ { fp = strings.ToLower(strings.TrimSpace(item.AllowedCertFingerprints[i])) if fp == "" { continue } policy.CertAllowlist[fp] = true } for i = 0; i < len(item.AllowedPKIClientCertIDs); i++ { cert, err = store.GetPKICert(strings.TrimSpace(item.AllowedPKIClientCertIDs[i])) if err != nil { if err == sql.ErrNoRows { continue } return policy, err } fp, err = certFingerprintFromPEM(cert.CertPEM) if err != nil { return policy, err } if fp == "" { continue } policy.CertAllowlist[fp] = fp != "" } return policy, nil } func certFingerprintFromPEM(certPEM string) (string, error) { var block *pem.Block var cert *x509.Certificate var err error var hash [32]byte block, _ = pem.Decode([]byte(certPEM)) if block == nil { return "", errors.New("failed to decode certificate pem") } cert, err = x509.ParseCertificate(block.Bytes) if err != nil { return "", err } hash = sha256.Sum256(cert.Raw) return strings.ToLower(hex.EncodeToString(hash[:])), nil } func listenerPolicyLogLabel(policy listenerAuthPolicy) string { return policy.ReadMode + "/" + policy.WriteMode } func sessionCookieNameFromServerId(value string) string { return strings.ReplaceAll(NormalizeServerId(value), "-", "_") + "_session" } func oidcStateCookieNameFromServerId(value string) string { return strings.ReplaceAll(NormalizeServerId(value), "-", "_") + "_oidc_state" } func NormalizeServerId(value string) string { return codit_config.NormalizeServerId(value) } func EnvPrefixFromServerId(value string) string { return codit_config.EnvPrefixFromServerId(value) } func TitleFromServerId(value string) string { return codit_config.TitleFromServerId(value) } func tcpAddrStrClass(addr string) string { var ap netip.AddrPort var err error if len(addr) > 0 { ap, err = netip.ParseAddrPort(addr) if err == nil { if ap.Addr().Is6() { return "tcp6" } if ap.Addr().Is4() || ap.Addr().Is4In6() { return "tcp4" } } } return "tcp" } func serveListeners(ctx context.Context, endpoints []listenerEndpoint, handler http.Handler, logger Logger, serverId string) error { var listenCtx context.Context var cancel context.CancelFunc var wg sync.WaitGroup var errs chan error var servers []*http.Server var serversMu sync.Mutex var i int var ep listenerEndpoint var listenErr error var shutdown func() serverId = NormalizeServerId(serverId) if len(endpoints) == 0 { return http.ErrServerClosed } listenCtx, cancel = context.WithCancel(ctx) defer cancel() errs = make(chan error, len(endpoints)) shutdown = func() { var copied []*http.Server var server *http.Server var shutdownCtx context.Context var shutdownCancel context.CancelFunc serversMu.Lock() copied = append(copied, servers...) serversMu.Unlock() for _, server = range copied { shutdownCtx, shutdownCancel = context.WithTimeout(context.Background(), 5*time.Second) _ = server.Shutdown(shutdownCtx) shutdownCancel() } } for i = 0; i < len(endpoints); i++ { ep = endpoints[i] if ep.IsHTTPS { logger.Write("", LOG_INFO, "%s server listener=%s https://%s", serverId, ep.Name, ep.Addr) } else { logger.Write("", LOG_INFO, "%s server listener=%s %s", serverId, ep.Name, ep.Addr) } wg.Add(1) go func(endpoint listenerEndpoint) { var err error var server *http.Server var l *log.Logger var listener net.Listener var network string defer wg.Done() l = log.New(&server_http_log_writer{l: logger, id: "", depth: +2}, "", 0) network = tcpAddrStrClass(endpoint.Addr) if endpoint.IsHTTPS { server = &http.Server{ Addr: endpoint.Addr, Handler: handler, TLSConfig: endpoint.TLSConfig, ErrorLog: l, ConnContext: connContextWithListenerIdentity(endpoint.ListenerKind, endpoint.ListenerID), } } else { server = &http.Server{ Addr: endpoint.Addr, Handler: handler, ErrorLog: l, ConnContext: connContextWithListenerIdentity(endpoint.ListenerKind, endpoint.ListenerID), } } serversMu.Lock() servers = append(servers, server) serversMu.Unlock() listener, err = net.Listen(network, endpoint.Addr) if err != nil { errs <- fmt.Errorf("listen %s %s: %w", network, endpoint.Addr, err) return } if endpoint.IsHTTPS { err = server.ServeTLS(listener, "", "") } else { err = server.Serve(listener) } errs <- err }(ep) } select { case <-listenCtx.Done(): shutdown() wg.Wait() return nil case listenErr = <-errs: cancel() shutdown() wg.Wait() if errors.Is(listenErr, http.ErrServerClosed) { return nil } } return listenErr } func buildListenerEndpoints(name string, listenerKind string, listenerID string, settings models.TLSSettings, store *db.Store) ([]listenerEndpoint, error) { var out []listenerEndpoint var tlsConfig *tls.Config var i int var err error settings.HTTPAddrs = codit_config.NormalizeHTTPAddrs(settings.HTTPAddrs) settings.HTTPSAddrs = codit_config.NormalizeHTTPAddrs(settings.HTTPSAddrs) if len(settings.HTTPAddrs) == 0 && len(settings.HTTPSAddrs) == 0 { return out, nil } if len(settings.HTTPSAddrs) > 0 { tlsConfig, err = buildServerTLSConfig(settings, store) if err != nil { return nil, err } } for i = 0; i < len(settings.HTTPAddrs); i++ { out = append(out, listenerEndpoint{Name: name, Addr: settings.HTTPAddrs[i], IsHTTPS: false, TLSConfig: nil, ListenerKind: listenerKind, ListenerID: listenerID}) } for i = 0; i < len(settings.HTTPSAddrs); i++ { out = append(out, listenerEndpoint{Name: name, Addr: settings.HTTPSAddrs[i], IsHTTPS: true, TLSConfig: tlsConfig, ListenerKind: listenerKind, ListenerID: listenerID}) } return out, nil } func buildServerTLSConfig(settings models.TLSSettings, store *db.Store) (*tls.Config, error) { var cert tls.Certificate var caPool *x509.CertPool var clientAuth tls.ClientAuthType var minVersion uint16 var err error cert, err = loadServerCertificate(settings, store) if err != nil { return nil, err } clientAuth = parseTLSClientAuth(settings.TLSClientAuth) caPool, err = loadClientCAPool(settings, store) if err != nil { return nil, err } if (clientAuth == tls.RequireAndVerifyClientCert || clientAuth == tls.VerifyClientCertIfGiven) && caPool == nil { return nil, errors.New("client auth is enabled but no client CA configured") } minVersion = parseTLSMinVersion(settings.TLSMinVersion) return &tls.Config{ MinVersion: minVersion, Certificates: []tls.Certificate{cert}, ClientAuth: clientAuth, ClientCAs: caPool, }, nil } func loadServerCertificate(settings models.TLSSettings, store *db.Store) (tls.Certificate, error) { var cert tls.Certificate var certData []byte var keyData []byte var pkiCert models.PKICert var err error var source string var certFile string var keyFile string source = strings.ToLower(strings.TrimSpace(settings.TLSServerCertSource)) if source == "" { source = "pki" } switch source { case "files": certFile = strings.TrimSpace(settings.TLSCertFile) keyFile = strings.TrimSpace(settings.TLSKeyFile) if certFile == "" || keyFile == "" { return cert, errors.New("ctl.tls.cert-file and ctl.tls.key-file are required when ctl.tls.server-cert-source is files") } cert, err = tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return cert, err } return cert, nil case "pki": if strings.TrimSpace(settings.TLSPKIServerCertID) == "" { return cert, errors.New("ctl.tls.pki-server-cert-id is required when ctl.tls.server-cert-source is pki") } default: return cert, errors.New("tls_server_cert_source must be files or pki") } pkiCert, err = store.GetPKICert(strings.TrimSpace(settings.TLSPKIServerCertID)) if err != nil { return cert, err } certData = []byte(pkiCert.CertPEM) keyData = []byte(pkiCert.KeyPEM) cert, err = tls.X509KeyPair(certData, keyData) if err != nil { return cert, err } return cert, nil } func loadClientCAPool(settings models.TLSSettings, store *db.Store) (*x509.CertPool, error) { var pool *x509.CertPool var pkiCA models.PKICA var ok bool var err error pool = nil if strings.TrimSpace(settings.TLSPKIClientCAID) != "" { pkiCA, err = store.GetPKICA(strings.TrimSpace(settings.TLSPKIClientCAID)) if err != nil { return nil, err } if pool == nil { pool = x509.NewCertPool() } ok = pool.AppendCertsFromPEM([]byte(pkiCA.CertPEM)) if !ok { return nil, errors.New("failed to parse tls_pki_client_ca_id certificate") } } return pool, nil } func parseTLSClientAuth(value string) tls.ClientAuthType { var v string v = strings.ToLower(strings.TrimSpace(value)) switch v { case "request": return tls.RequestClientCert case "require": return tls.RequireAnyClientCert case "verify_if_given": return tls.VerifyClientCertIfGiven case "require_and_verify": return tls.RequireAndVerifyClientCert default: return tls.NoClientCert } } func parseTLSMinVersion(value string) uint16 { var v string v = strings.ToLower(strings.TrimSpace(value)) switch v { case "1.0", "tls1.0": return tls.VersionTLS10 case "1.1", "tls1.1": return tls.VersionTLS11 case "1.3", "tls1.3": return tls.VersionTLS13 default: return tls.VersionTLS12 } } func mergeTLSSettingsFromDB(cfg *codit_config.Config, store *db.Store, envPrefix string) error { var settings models.TLSSettings var envAddrs string var envTLSEnabled string var envServerSource string var envCertFile string var envKeyFile string var envPKIServerCertID string var envClientAuth string var envClientCAFile string var envPKIClientCAID string var envTLSMinVersion string var hasConfiguredAddrs bool var err error hasConfiguredAddrs = len(codit_config.NormalizeHTTPAddrs(cfg.CTL.Service.Addrs)) > 0 settings, err = store.GetTLSSettings() if err != nil { return err } envAddrs = strings.TrimSpace(os.Getenv(envPrefix + "CTL_SERVICE_ADDRESSES")) envTLSEnabled = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_ENABLED")) envServerSource = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_SERVER_CERT_SOURCE")) envCertFile = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_CERT_FILE")) envKeyFile = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_KEY_FILE")) envPKIServerCertID = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_PKI_SERVER_CERT_ID")) envClientAuth = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_CLIENT_AUTH")) envClientCAFile = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_CLIENT_CA_FILE")) envPKIClientCAID = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_PKI_CLIENT_CA_ID")) envTLSMinVersion = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_MIN_VERSION")) if len(settings.HTTPSAddrs) > 0 && envAddrs == "" && !hasConfiguredAddrs { cfg.CTL.Service.Addrs = settings.HTTPSAddrs } else if len(settings.HTTPAddrs) > 0 && envAddrs == "" && !hasConfiguredAddrs { cfg.CTL.Service.Addrs = settings.HTTPAddrs } if envTLSEnabled == "" && !hasConfiguredAddrs { cfg.CTL.TLS.Enabled = len(settings.HTTPSAddrs) > 0 } if strings.TrimSpace(settings.TLSServerCertSource) != "" && envServerSource == "" { cfg.CTL.TLS.ServerCertSource = settings.TLSServerCertSource } if strings.TrimSpace(settings.TLSCertFile) != "" && envCertFile == "" { cfg.CTL.TLS.CertFile = settings.TLSCertFile } if strings.TrimSpace(settings.TLSKeyFile) != "" && envKeyFile == "" { cfg.CTL.TLS.KeyFile = settings.TLSKeyFile } if strings.TrimSpace(settings.TLSPKIServerCertID) != "" && envPKIServerCertID == "" { cfg.CTL.TLS.PKIServerCertID = settings.TLSPKIServerCertID } if strings.TrimSpace(settings.TLSClientAuth) != "" && envClientAuth == "" { cfg.CTL.TLS.ClientAuth = settings.TLSClientAuth } if strings.TrimSpace(settings.TLSClientCAFile) != "" && envClientCAFile == "" { cfg.CTL.TLS.ClientCAFile = settings.TLSClientCAFile } if strings.TrimSpace(settings.TLSPKIClientCAID) != "" && envPKIClientCAID == "" { cfg.CTL.TLS.PKIClientCAID = settings.TLSPKIClientCAID } if strings.TrimSpace(settings.TLSMinVersion) != "" && envTLSMinVersion == "" { cfg.CTL.TLS.MinVersion = settings.TLSMinVersion } return nil } func serveIndexFile(w http.ResponseWriter, r *http.Request, path string, logger Logger, serverId string, serverTitle string, siteName string) { var f *os.File var err error f, err = os.Open(path) if err != nil { http.Error(w, "file not found", http.StatusNotFound) return } defer f.Close() w.Header().Set("Content-Type", "text/html; charset=utf-8") // the content may get modified. the actual content-length after transformation may // not match the length of the original response. // remove the Content-Length header to drive the http library to switch to // the Chunked encoding. w.Header().Del("Content-Length") _, err = io.Copy(w, transform.NewReader(f, transform.Chain( NewStringTransformer("%SERVER_ID%", serverId), NewStringTransformer("%SERVER_TITLE%", serverTitle), NewStringTransformer("%SITE_NAME%", siteName)))) if err != nil { if logger != nil { logger.Write("", LOG_WARN, "stream response failed path=%s remote=%s err=%v", path, r.RemoteAddr, err) } } } func spaHandler(root string, logger Logger, serverId string, serverTitle string, siteName string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var path string var reqPath string var cleaned string var info os.FileInfo var indexPath string var err error reqPath = strings.TrimPrefix(r.URL.Path, "/") cleaned = filepath.Clean(reqPath) if cleaned == "." { cleaned = "" } if strings.HasPrefix(cleaned, "..") { w.WriteHeader(http.StatusNotFound) return } indexPath = filepath.Join(root, "index.html") path = filepath.Join(root, cleaned) info, err = os.Stat(path) if err == nil && !info.IsDir() { if path == indexPath { serveIndexFile(w, r, indexPath, logger, serverId, serverTitle, siteName) } else { http.ServeFile(w, r, path) } return } if filepath.Ext(cleaned) != "" { w.WriteHeader(http.StatusNotFound) return } _, err = os.Stat(indexPath) if err == nil { serveIndexFile(w, r, indexPath, logger, serverId, serverTitle, siteName) return } w.WriteHeader(http.StatusNotFound) } } func bootstrapAdmin(store *db.Store, envPrefix string) error { var txStore *db.Store var bootstrap string var parts []string var username string var password string var hash string var err error bootstrap = os.Getenv(envPrefix + "BOOTSTRAP_ADMIN") if bootstrap == "" { return nil } parts = strings.SplitN(bootstrap, ":", 2) if len(parts) != 2 { return nil } username = strings.TrimSpace(parts[0]) password = strings.TrimSpace(parts[1]) if username == "" || password == "" { return nil } txStore, err = store.BeginStore(context.Background()) if err != nil { return err } _, _, err = txStore.GetUserByUsername(username) if err == nil { _ = txStore.Rollback() return nil } if err != sql.ErrNoRows { _ = txStore.Rollback() return err } hash, err = auth.HashPassword(password) if err != nil { _ = txStore.Rollback() return err } _, err = txStore.CreateUser(models.User{ Username: username, DisplayName: username, Email: username + "@local", IsAdmin: true, AuthSource: "db", }, hash) if err != nil { _ = txStore.Rollback() return err } err = txStore.Commit() if err != nil { return err } return nil }