package main import "flag" //import "io" import "context" import "crypto/sha256" import "crypto/tls" import "crypto/x509" import "encoding/hex" import "errors" import "fmt" import "log" import "net" import "net/http" import "os" import "path/filepath" import "sync" import "strings" import "time" import "codit/internal/auth" import "codit/internal/config" import "codit/internal/db" import "codit/internal/docker" import "codit/internal/git" import "codit/internal/handlers" import httpx "codit/internal/http" import "codit/internal/middleware" import "codit/internal/models" import "codit/internal/rpm" import "codit/internal/storage" import "codit/internal/util" import _ "modernc.org/sqlite" type gitPathRewriteHandler struct { next http.Handler store *db.Store } type server_http_log_writer struct { l *util.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, util.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 project models.Project var repo models.Repo var projectStorageID int64 var repoStorageID int64 var err error var newPath string 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 = "" } repoName = strings.TrimSuffix(repoSegment, ".git") project, err = h.store.GetProjectBySlug(slug) if err != nil { http.NotFound(w, r) return } repo, err = h.store.GetRepoByProjectNameType(project.ID, repoName, "git") if err != nil { http.NotFound(w, r) return } projectStorageID, repoStorageID, err = h.store.GetRepoStorageIDs(repo.ID) if err != nil { http.NotFound(w, r) return } newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + ".git" + rest 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 repo models.Repo var projectStorageID int64 var repoStorageID int64 var err error var newPath string var repoID string 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") repo, err = h.store.GetRepo(repoID) if err != nil || repo.Type != "git" { http.NotFound(w, r) return } projectStorageID, repoStorageID, err = h.store.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 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 project models.Project var repo models.Repo var projectStorageID int64 var repoStorageID int64 var err error var newPath string 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 = "" } project, err = h.store.GetProjectBySlug(slug) if err != nil { http.NotFound(w, r) return } repo, err = h.store.GetRepoByProjectNameType(project.ID, repoName, "rpm") if err != nil { http.NotFound(w, r) return } projectStorageID, repoStorageID, err = h.store.GetRepoStorageIDs(repo.ID) if err != nil { http.NotFound(w, r) return } newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + rest 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 repo models.Repo var projectStorageID int64 var repoStorageID int64 var err error var newPath string var repoID string path = strings.TrimPrefix(r.URL.Path, "/") if path == "" { h.next.ServeHTTP(w, r) return } parts = strings.SplitN(path, "/", 3) repoID = parts[0] repo, err = h.store.GetRepo(repoID) if err != nil || repo.Type != "rpm" { http.NotFound(w, r) return } projectStorageID, repoStorageID, err = h.store.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 r.URL.Path = newPath h.next.ServeHTTP(w, r) } func storageIDSegment(id int64) string { return fmt.Sprintf("%016x", id) } func main() { var configPath string var cfg config.Config var store *db.Store var err error var gitRewrite http.Handler var gitIDRewrite http.Handler var rpmRewrite http.Handler var rpmIDRewrite http.Handler var logger *util.Logger var mainEndpoints []listenerEndpoint var extraListenerManager *additionalListenerManager flag.StringVar(&configPath, "config", "", "path to config json") flag.Parse() cfg, err = config.Load(configPath) if err != nil { log.Fatalf("config error: %v", err) } // log.SetOutput(io.Discard) //if cfg.APP.LogFile == "" { logger = util.NewLogger("codit", os.Stderr, util.LogStrsToMask(nil)) //} else { // logger, err = util.NewLoggerToFile("codit", cfg.APP.LogFile, cfg.APP.LogMaxSize, cfg.APP.LogRotate, util.LogStrsToMask(cfg.APP.LogMask)) // if err != nil { // log.Fatalf("failed to initialize logger - %s", err.Error()) // } //} err = os.MkdirAll(cfg.DataDir, 0o755) if err != nil { //log.Fatalf("data dir error: %v", err) logger.Write("", util.LOG_ERROR, "data dir error: %v", err) } store, err = db.Open(cfg.DBDriver, cfg.DBDSN) if err != nil { log.Fatalf("db open error: %v", err) } defer store.Close() err = store.ApplyMigrations(filepath.Join("migrations")) if err != nil { log.Fatalf("migrations error: %v", err) } err = mergeTLSSettingsFromDB(&cfg, store) if err != nil { log.Fatalf("tls settings load error: %v", err) } err = bootstrapAdmin(store) if err != nil { log.Fatalf("bootstrap admin error: %v", err) } var repoManager git.RepoManager repoManager = git.RepoManager{BaseDir: filepath.Join(cfg.DataDir, "git")} var rpmBase string var dockerBase string rpmBase = filepath.Join(cfg.DataDir, "rpm") dockerBase = filepath.Join(cfg.DataDir, "docker") var rpmMeta *rpm.MetaManager var rpmMirror *rpm.MirrorManager rpmMeta = rpm.NewMetaManager() rpmMirror = rpm.NewMirrorManager(store, logger, rpmMeta) var uploadStore storage.FileStore uploadStore = storage.FileStore{BaseDir: filepath.Join(cfg.DataDir, "uploads")} err = os.MkdirAll(repoManager.BaseDir, 0o755) if err != nil { log.Fatalf("git dir error: %v", err) } err = os.MkdirAll(rpmBase, 0o755) if err != nil { log.Fatalf("rpm dir error: %v", err) } err = os.MkdirAll(dockerBase, 0o755) if err != nil { log.Fatalf("docker dir error: %v", err) } var api *handlers.API api = &handlers.API{ Cfg: cfg, Store: store, Repos: repoManager, RpmBase: rpmBase, RpmMeta: rpmMeta, RpmMirror: rpmMirror, DockerBase: dockerBase, Uploads: uploadStore, Logger: logger, } rpmMirror.Start() var graphqlHandler http.Handler graphqlHandler, err = handlers.NewGraphQL(store) if err != nil { log.Fatalf("graphql init error: %v", err) } var gitServer http.Handler var authFunc func(username string, password string) (bool, error) authFunc = func(username string, password string) (bool, error) { var user models.User 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 } } if password == "" && username != "" { key = username user, err = store.GetUserByAPIKeyHash(util.HashToken(key)) if err == nil { return true, 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 } } return false, nil } if user.Disabled { return false, nil } return user.ID != "", nil } gitServer, err = git.NewHTTPServer(repoManager.BaseDir, nil, logger) if err != nil { log.Fatalf("git server error: %v", err) } var rpmServer http.Handler rpmServer = rpm.NewHTTPServer(rpmBase, nil, logger) var dockerServer http.Handler dockerServer = docker.NewHTTPServer(store, dockerBase, nil, logger) var router *httpx.Router router = httpx.NewRouter() router.Handle("GET", "/api/health", api.Health) 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/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("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/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/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/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/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("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/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 mux = http.NewServeMux() gitRewrite = &gitPathRewriteHandler{next: gitServer, store: store} mux.Handle(cfg.GitHTTPPrefix+"/", withServiceAuth(serviceGit, http.StripPrefix(cfg.GitHTTPPrefix, gitRewrite), authFunc, store, logger)) gitIDRewrite = &gitIDPathRewriteHandler{next: gitServer, store: store} mux.Handle(cfg.GitHTTPPrefix+"-id/", withServiceAuth(serviceGit, http.StripPrefix(cfg.GitHTTPPrefix+"-id", gitIDRewrite), authFunc, store, logger)) rpmRewrite = &rpmPathRewriteHandler{next: rpmServer, store: store} mux.Handle(cfg.RPMHTTPPrefix+"/", withServiceAuth(serviceRPM, http.StripPrefix(cfg.RPMHTTPPrefix, rpmRewrite), authFunc, store, logger)) rpmIDRewrite = &rpmIDPathRewriteHandler{next: rpmServer, store: store} mux.Handle(cfg.RPMHTTPPrefix+"-id/", withServiceAuth(serviceRPM, http.StripPrefix(cfg.RPMHTTPPrefix+"-id", rpmIDRewrite), authFunc, store, logger)) mux.Handle("/v2", withServiceAuth(serviceV2, dockerServer, authFunc, store, logger)) mux.Handle("/v2/", withServiceAuth(serviceV2, dockerServer, authFunc, store, logger)) mux.HandleFunc("/pki/crl/", api.ServePKICRL) mux.Handle("/api/graphql", middleware.WithUser(store, middleware.RequireAuth(graphqlHandler))) mux.Handle("/api/", middleware.WithUser(store, middleware.AccessLog(logger, withAPIAuth(router, authFunc, store, logger)))) mux.Handle("/api/login", middleware.WithUser(store, middleware.AccessLog(logger, router))) mux.Handle("/api/logout", middleware.WithUser(store, middleware.AccessLog(logger, router))) mux.Handle("/api/auth/oidc/enabled", middleware.WithUser(store, middleware.AccessLog(logger, router))) mux.Handle("/api/auth/oidc/login", middleware.WithUser(store, middleware.AccessLog(logger, router))) mux.Handle("/api/auth/oidc/callback", middleware.WithUser(store, middleware.AccessLog(logger, router))) mux.Handle("/api/health", middleware.AccessLog(logger, router)) mux.Handle("/", middleware.WithUser(store, spaHandler(cfg.FrontendDir))) extraListenerManager = newAdditionalListenerManager(store, mux, logger) api.OnTLSListenersChanged = extraListenerManager.NotifyReload api.OnTLSListenerRuntimeStatus = extraListenerManager.ListenerEndpointCounts err = extraListenerManager.Start() if err != nil { logger.Write("", util.LOG_ERROR, "additional listener manager error: %v", err) os.Exit(1) } mainEndpoints, err = buildListenerEndpoints("main", tlsSettingsFromConfig(cfg), defaultListenerPolicy(), store) if err != nil { log.Fatalf("main listener config error: %v", err) } err = serveListeners(mainEndpoints, mux, logger) if err != nil { log.Fatalf("server error: %v", err) } logger.Close() } type listenerEndpoint struct { Name string Key string Addr string IsHTTPS bool TLSConfig *tls.Config Policy listenerAuthPolicy } type ctxKey string const listenerPolicyKey ctxKey = "listener_policy" type serviceKind string const ( serviceAPI serviceKind = "api" serviceGit serviceKind = "git" serviceRPM serviceKind = "rpm" serviceV2 serviceKind = "v2" ) 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 { Mode string ApplyAPI bool ApplyGit bool ApplyRPM bool ApplyV2 bool CertAllowlist map[string]bool } type runningListener struct { Endpoint listenerEndpoint Server *http.Server } type additionalListenerManager struct { Store *db.Store Handler http.Handler Logger *util.Logger mu sync.Mutex Running map[string]runningListener Reload chan struct{} } func newAdditionalListenerManager(store *db.Store, handler http.Handler, logger *util.Logger) *additionalListenerManager { var manager *additionalListenerManager manager = &additionalListenerManager{ Store: store, Handler: handler, Logger: logger, Running: make(map[string]runningListener), Reload: make(chan struct{}, 1), } 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) 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 } go func() { var reconcileErr error for range m.Reload { reconcileErr = m.reconcile() if reconcileErr != nil && m.Logger != nil { m.Logger.Write("", util.LOG_ERROR, "additional listener reconcile error: %v", reconcileErr) } } }() return nil } 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 endpoint listenerEndpoint var running runningListener var shutdownCtx context.Context var cancel context.CancelFunc var more []listenerEndpoint var j int var settings models.TLSSettings var policy listenerAuthPolicy var keyPrefix string 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, } policy = listenerPolicyFromTLSListener(listeners[i]) more, err = buildListenerEndpoints(listeners[i].Name, settings, policy, m.Store) if err != nil { return err } keyPrefix = fmt.Sprintf("extra:%s:%d", listeners[i].ID, listeners[i].UpdatedAt) for j = 0; j < len(more); j++ { key = keyPrefix + ":" + 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) if m.Logger != nil { m.Logger.Write("", util.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: connContextWithListenerPolicy(endpoint.Policy), 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 m.Logger != nil { if endpoint.IsHTTPS { m.Logger.Write(endpoint.Name, util.LOG_INFO, "additional listener started https://%s", endpoint.Addr) } else { m.Logger.Write(endpoint.Name, util.LOG_INFO, "additional listener started %s", endpoint.Addr) } } go func(ep listenerEndpoint, srv *http.Server) { var err error if ep.IsHTTPS { err = srv.ListenAndServeTLS("", "") } else { err = srv.ListenAndServe() } if err != nil && !errors.Is(err, http.ErrServerClosed) && m.Logger != nil { m.Logger.Write(ep.Name, util.LOG_ERROR, "additional listener failed addr=%s err=%v", ep.Addr, err) } }(endpoint, server) return running } func endpointScheme(endpoint listenerEndpoint) string { if endpoint.IsHTTPS { return "https" } return "http" } func connContextWithListenerPolicy(policy listenerAuthPolicy) func(ctx context.Context, conn net.Conn) context.Context { return func(ctx context.Context, _ net.Conn) context.Context { return context.WithValue(ctx, listenerPolicyKey, policy) } } func listenerPolicyFromRequest(r *http.Request) listenerAuthPolicy { var policy listenerAuthPolicy var ok bool policy = defaultListenerPolicy() if r == nil { return policy } policy, ok = r.Context().Value(listenerPolicyKey).(listenerAuthPolicy) if !ok { return defaultListenerPolicy() } return policy } func isPolicyApplied(policy listenerAuthPolicy, service serviceKind) bool { if service == serviceAPI { return policy.ApplyAPI } if service == serviceGit { return policy.ApplyGit } if service == serviceRPM { return policy.ApplyRPM } if service == serviceV2 { return policy.ApplyV2 } return false } func evaluateAuthOutcome(policy listenerAuthPolicy, service serviceKind, operation opKind) authOutcome { if !isPolicyApplied(policy, service) { return authRequireAuth } switch strings.ToLower(strings.TrimSpace(policy.Mode)) { case "read_open_write_cert": if operation == opRead { return authAllow } return authRequireCert case "read_open_write_cert_or_auth": if operation == opRead { return authAllow } return authCertOrAuth case "cert_only": return authRequireCert case "read_only_public": if operation == opRead { return authAllow } 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(r *http.Request, policy listenerAuthPolicy) bool { var fp string if len(policy.CertAllowlist) == 0 { return false } fp = requestClientCertFingerprint(r) if fp == "" { return false } return policy.CertAllowlist[fp] } func authenticateByBasic(r *http.Request, auth func(username string, password string) (bool, error)) 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(username, password) if err != nil { return false } return allowed } func withServiceAuth(service serviceKind, next http.Handler, auth func(username string, password string) (bool, error), store *db.Store, logger *util.Logger) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var policy listenerAuthPolicy var operation opKind var outcome authOutcome var ok bool var principal models.ServicePrincipal var principalOK bool var err error policy = listenerPolicyFromRequest(r) operation = classifyServiceOperation(service, r) outcome = evaluateAuthOutcome(policy, service, operation) if outcome == authDeny { w.WriteHeader(http.StatusForbidden) return } if outcome == authAllow { next.ServeHTTP(w, r) return } if outcome == authRequireCert { ok, principal, principalOK, err = authorizeByClientCert(service, r, policy, operation, store) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if ok { if principalOK { r = middleware.WithPrincipal(r, principal) } next.ServeHTTP(w, r) return } w.WriteHeader(http.StatusForbidden) return } if outcome == authCertOrAuth { ok, principal, principalOK, err = authorizeByClientCert(service, r, policy, operation, store) 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) 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 logger != nil { logger.Write(string(service), util.LOG_INFO, "auth denied policy=%s method=%s path=%s remote=%s", policy.Mode, r.Method, r.URL.Path, r.RemoteAddr) } w.WriteHeader(http.StatusUnauthorized) }) } func withAPIAuth(next http.Handler, auth func(username string, password string) (bool, error), store *db.Store, logger *util.Logger) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var policy listenerAuthPolicy var operation opKind var outcome authOutcome var user models.User var hasUser bool var ok bool var principal models.ServicePrincipal var principalOK bool var err error policy = listenerPolicyFromRequest(r) operation = classifyServiceOperation(serviceAPI, r) outcome = evaluateAuthOutcome(policy, serviceAPI, operation) user, hasUser = middleware.UserFromContext(r.Context()) if user.Disabled { hasUser = false } if outcome == authDeny { w.WriteHeader(http.StatusForbidden) return } if outcome == authAllow { next.ServeHTTP(w, r) return } if outcome == authRequireCert { ok, principal, principalOK, err = authorizeByClientCert(serviceAPI, r, policy, operation, store) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if ok { if principalOK { r = middleware.WithPrincipal(r, principal) } next.ServeHTTP(w, r) return } w.WriteHeader(http.StatusForbidden) return } if outcome == authCertOrAuth { ok, principal, principalOK, err = authorizeByClientCert(serviceAPI, r, policy, operation, store) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if ok || hasUser { if principalOK { r = middleware.WithPrincipal(r, principal) } next.ServeHTTP(w, r) return } if authenticateByBasic(r, auth) { next.ServeHTTP(w, r) return } w.WriteHeader(http.StatusUnauthorized) return } if hasUser || authenticateByBasic(r, auth) { next.ServeHTTP(w, r) return } if logger != nil { logger.Write("api", util.LOG_INFO, "auth denied policy=%s method=%s path=%s remote=%s", policy.Mode, r.Method, r.URL.Path, r.RemoteAddr) } 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, r *http.Request, policy listenerAuthPolicy, operation opKind, store *db.Store) (bool, models.ServicePrincipal, bool, error) { var fp string var principal models.ServicePrincipal var bound bool var allowed bool var err error if !isClientCertAllowed(r, policy) { return false, principal, false, nil } if operation == opRead { return true, principal, false, nil } fp = requestClientCertFingerprint(r) if fp == "" { return false, principal, false, nil } principal, bound, err = store.GetPrincipalByCertFingerprint(fp) if err != nil { return false, principal, false, err } if !bound { return false, principal, false, nil } if service == serviceAPI { return true, principal, true, nil } allowed, err = principalCanWriteService(service, principal, r, store) if err != nil { return false, principal, false, err } if !allowed { return false, principal, false, nil } return true, principal, true, nil } func principalCanWriteService(service serviceKind, 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, 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, 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), "/") if service == serviceV2 { path = strings.TrimPrefix(path, "v2/") 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 tlsSettingsFromConfig(cfg config.Config) models.TLSSettings { var settings models.TLSSettings settings = models.TLSSettings{ HTTPAddrs: normalizeAddrList(cfg.HTTPAddrs), HTTPSAddrs: normalizeAddrList(cfg.HTTPSAddrs), TLSServerCertSource: strings.TrimSpace(cfg.TLSServerCertSource), TLSCertFile: strings.TrimSpace(cfg.TLSCertFile), TLSKeyFile: strings.TrimSpace(cfg.TLSKeyFile), TLSPKIServerCertID: strings.TrimSpace(cfg.TLSPKIServerCertID), TLSClientAuth: strings.TrimSpace(cfg.TLSClientAuth), TLSClientCAFile: strings.TrimSpace(cfg.TLSClientCAFile), TLSPKIClientCAID: strings.TrimSpace(cfg.TLSPKIClientCAID), TLSMinVersion: strings.TrimSpace(cfg.TLSMinVersion), } return settings } func defaultListenerPolicy() listenerAuthPolicy { var policy listenerAuthPolicy policy = listenerAuthPolicy{ Mode: "default", ApplyAPI: true, ApplyGit: true, ApplyRPM: true, ApplyV2: true, CertAllowlist: make(map[string]bool), } return policy } func listenerPolicyFromTLSListener(item models.TLSListener) listenerAuthPolicy { var policy listenerAuthPolicy var i int var fp string policy = defaultListenerPolicy() if strings.TrimSpace(item.AuthPolicy) != "" { policy.Mode = strings.TrimSpace(item.AuthPolicy) } policy.ApplyAPI = item.ApplyPolicyAPI policy.ApplyGit = item.ApplyPolicyGit policy.ApplyRPM = item.ApplyPolicyRPM policy.ApplyV2 = item.ApplyPolicyV2 for i = 0; i < len(item.ClientCertAllowlist); i++ { fp = strings.ToLower(strings.TrimSpace(item.ClientCertAllowlist[i])) if fp == "" { continue } policy.CertAllowlist[fp] = true } return policy } func normalizeAddrList(values []string) []string { var out []string var i int var v string for i = 0; i < len(values); i++ { v = strings.TrimSpace(values[i]) if v == "" { continue } out = append(out, v) } return out } func serveListeners(endpoints []listenerEndpoint, handler http.Handler, logger *util.Logger) error { var wg sync.WaitGroup var errs chan error var i int var ep listenerEndpoint var listenErr error if len(endpoints) == 0 { return http.ErrServerClosed } errs = make(chan error, len(endpoints)) for i = 0; i < len(endpoints); i++ { ep = endpoints[i] if logger != nil { if ep.IsHTTPS { logger.Write("", util.LOG_INFO, "codit server listener=%s https://%s", ep.Name, ep.Addr) } else { logger.Write("", util.LOG_INFO, "codit server listener=%s %s", ep.Name, ep.Addr) } } wg.Add(1) go func(endpoint listenerEndpoint) { var err error var server *http.Server var l *log.Logger defer wg.Done() l = log.New(&server_http_log_writer{l: logger, id: "", depth: +2}, "", 0) if endpoint.IsHTTPS { server = &http.Server{ Addr: endpoint.Addr, Handler: handler, TLSConfig: endpoint.TLSConfig, ErrorLog: l, ConnContext: connContextWithListenerPolicy(endpoint.Policy), } err = server.ListenAndServeTLS("", "") } else { server = &http.Server{ Addr: endpoint.Addr, Handler: handler, ErrorLog: l, ConnContext: connContextWithListenerPolicy(endpoint.Policy), } err = server.ListenAndServe() } errs <- err }(ep) } listenErr = <-errs return listenErr } func buildListenerEndpoints(name string, settings models.TLSSettings, policy listenerAuthPolicy, store *db.Store) ([]listenerEndpoint, error) { var out []listenerEndpoint var tlsConfig *tls.Config var i int var err error settings.HTTPAddrs = normalizeAddrList(settings.HTTPAddrs) settings.HTTPSAddrs = normalizeAddrList(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, Policy: policy}) } for i = 0; i < len(settings.HTTPSAddrs); i++ { out = append(out, listenerEndpoint{Name: name, Addr: settings.HTTPSAddrs[i], IsHTTPS: true, TLSConfig: tlsConfig, Policy: policy}) } 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 if strings.TrimSpace(settings.TLSPKIServerCertID) == "" { return cert, errors.New("tls_pki_server_cert_id is required for https listener") } 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 *config.Config, store *db.Store) error { var settings models.TLSSettings var envHTTPAddrs string var envHTTPSAddrs 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 err error settings, err = store.GetTLSSettings() if err != nil { return err } envHTTPAddrs = strings.TrimSpace(os.Getenv("CODIT_HTTP_ADDRS")) envHTTPSAddrs = strings.TrimSpace(os.Getenv("CODIT_HTTPS_ADDRS")) envServerSource = strings.TrimSpace(os.Getenv("CODIT_TLS_SERVER_CERT_SOURCE")) envCertFile = strings.TrimSpace(os.Getenv("CODIT_TLS_CERT_FILE")) envKeyFile = strings.TrimSpace(os.Getenv("CODIT_TLS_KEY_FILE")) envPKIServerCertID = strings.TrimSpace(os.Getenv("CODIT_TLS_PKI_SERVER_CERT_ID")) envClientAuth = strings.TrimSpace(os.Getenv("CODIT_TLS_CLIENT_AUTH")) envClientCAFile = strings.TrimSpace(os.Getenv("CODIT_TLS_CLIENT_CA_FILE")) envPKIClientCAID = strings.TrimSpace(os.Getenv("CODIT_TLS_PKI_CLIENT_CA_ID")) envTLSMinVersion = strings.TrimSpace(os.Getenv("CODIT_TLS_MIN_VERSION")) if len(settings.HTTPAddrs) > 0 && envHTTPAddrs == "" { cfg.HTTPAddrs = settings.HTTPAddrs } if len(settings.HTTPSAddrs) > 0 && envHTTPSAddrs == "" { cfg.HTTPSAddrs = settings.HTTPSAddrs } if strings.TrimSpace(settings.TLSServerCertSource) != "" && envServerSource == "" { cfg.TLSServerCertSource = settings.TLSServerCertSource } if strings.TrimSpace(settings.TLSCertFile) != "" && envCertFile == "" { cfg.TLSCertFile = settings.TLSCertFile } if strings.TrimSpace(settings.TLSKeyFile) != "" && envKeyFile == "" { cfg.TLSKeyFile = settings.TLSKeyFile } if strings.TrimSpace(settings.TLSPKIServerCertID) != "" && envPKIServerCertID == "" { cfg.TLSPKIServerCertID = settings.TLSPKIServerCertID } if strings.TrimSpace(settings.TLSClientAuth) != "" && envClientAuth == "" { cfg.TLSClientAuth = settings.TLSClientAuth } if strings.TrimSpace(settings.TLSClientCAFile) != "" && envClientCAFile == "" { cfg.TLSClientCAFile = settings.TLSClientCAFile } if strings.TrimSpace(settings.TLSPKIClientCAID) != "" && envPKIClientCAID == "" { cfg.TLSPKIClientCAID = settings.TLSPKIClientCAID } if strings.TrimSpace(settings.TLSMinVersion) != "" && envTLSMinVersion == "" { cfg.TLSMinVersion = settings.TLSMinVersion } return nil } func spaHandler(root string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var path string var reqPath string var cleaned string reqPath = strings.TrimPrefix(r.URL.Path, "/") cleaned = filepath.Clean(reqPath) if cleaned == "." { cleaned = "" } if strings.HasPrefix(cleaned, "..") { w.WriteHeader(http.StatusNotFound) return } path = filepath.Join(root, cleaned) var info os.FileInfo var err error info, err = os.Stat(path) if err == nil && !info.IsDir() { http.ServeFile(w, r, path) return } if filepath.Ext(cleaned) != "" { w.WriteHeader(http.StatusNotFound) return } var indexPath string indexPath = filepath.Join(root, "index.html") _, err = os.Stat(indexPath) if err == nil { http.ServeFile(w, r, indexPath) return } w.WriteHeader(http.StatusNotFound) } } func bootstrapAdmin(store *db.Store) error { var bootstrap string var parts []string var username string var password string var hash string var err error bootstrap = os.Getenv("CODIT_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 } _, _, err = store.GetUserByUsername(username) if err == nil { return nil } hash, err = auth.HashPassword(password) if err != nil { return err } _, err = store.CreateUser(models.User{ Username: username, DisplayName: username, Email: username + "@local", IsAdmin: true, AuthSource: "db", }, hash) return err }