Files
codit/backend/server.go

2470 lines
84 KiB
Go

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
}