From 979b302e4cb279bd2153406725f2dddcae3067b9 Mon Sep 17 00:00:00 2001 From: Thomas Miceli <27960254+thomiceli@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:34:52 +0200 Subject: [PATCH] Add listen to Unix websocket (#484) --- config.yml | 4 ++ docs/configuration/cheat-sheet.md | 91 ++++++++++++------------ internal/cli/main.go | 9 ++- internal/config/config.go | 4 ++ internal/web/server/server.go | 112 ++++++++++++++++++++++++++++++ scripts/watch.sh | 21 +++++- 6 files changed, 190 insertions(+), 51 deletions(-) diff --git a/config.yml b/config.yml index cde43f6..48bf1a5 100644 --- a/config.yml +++ b/config.yml @@ -43,6 +43,7 @@ sqlite.journal-mode: WAL # HTTP server configuration # Host to bind to. Default: 0.0.0.0 +# Use an IP address for network binding. Use a path for Unix socket binding (e.g. /run/opengist.sock) http.host: 0.0.0.0 # Port to bind to. Default: 6157 @@ -51,6 +52,9 @@ http.port: 6157 # Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true http.git-enabled: true +# File permissions for Unix socket (octal format). Default: 0666 +unix-socket-permissions: 0666 + # Enable or disable the metrics endpoint (either `true` or `false`). Default: false metrics.enabled: false diff --git a/docs/configuration/cheat-sheet.md b/docs/configuration/cheat-sheet.md index ea319a2..a012d67 100644 --- a/docs/configuration/cheat-sheet.md +++ b/docs/configuration/cheat-sheet.md @@ -4,48 +4,49 @@ aside: false # Configuration Cheat Sheet -| YAML Config Key | Environment Variable | Default value | Description | -|-----------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `debug`, `info`, `warn`, `error`, `fatal`. | -| log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. | -| external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. | -| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. | -| secret-key | OG_SECRET_KEY | randomized 32 bytes | Secret key used for session store & encrypt MFA data on database. | -| db-uri | OG_DB_URI | `opengist.db` | URI of the database. | -| index | OG_INDEX | `bleve` | Define the code indexer (either `bleve`, `meilisearch`, or empty for no index). | -| index.meili.host | OG_MEILI_HOST | none | Set the host for the Meiliseach server. | -| index.meili.api-key | OG_MEILI_API_KEY | none | Set the API key for the Meiliseach server. | -| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) | -| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) | -| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. | -| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. | -| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) | -| metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable Prometheus metrics endpoint at `/metrics` (`true` or `false`) | -| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) | -| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. | -| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. | -| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. | -| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. | -| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. | -| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. | -| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. | -| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. | -| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. | -| gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. | -| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. | -| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. | -| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. | -| gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. | -| oidc.provider-name | OG_OIDC_PROVIDER_NAME | none | The name of the OIDC provider | -| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. | -| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. | -| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. | -| ldap.url | OG_LDAP_URL | none | URL of the LDAP instance; if not set, LDAP authentication is disabled | -| ldap.bind-dn | OG_LDAP_BIND_DN | none | Bind DN to authenticate against the LDAP. e.g: cn=read-only-admin,dc=example,dc=com | -| ldap.bind-credentials | OG_LDAP_BIND_CREDENTIALS | none | The password for the Bind DN. | -| ldap.search-base | OG_LDAP_SEARCH_BASE | none | The Base DN to start search from. e.g: ou=People,dc=example,dc=com | -| ldap.search-filter | OG_LDAP_SEARCH_FILTER | none | The filter to search against (the format string %s will be replaced with the username). e.g: (uid=%s) | -| custom.name | OG_CUSTOM_NAME | none | The name of your instance, to be displayed in the tab title | -| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. | -| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. | -| custom.static-links | OG_CUSTOM_STATIC_LINK_#_(PATH,NAME) | none | Path and name to custom links, more info [here](custom-links.md). | +| YAML Config Key | Environment Variable | Default value | Description | +|-------------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `debug`, `info`, `warn`, `error`, `fatal`. | +| log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. | +| external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. | +| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. | +| secret-key | OG_SECRET_KEY | randomized 32 bytes | Secret key used for session store & encrypt MFA data on database. | +| db-uri | OG_DB_URI | `opengist.db` | URI of the database. | +| index | OG_INDEX | `bleve` | Define the code indexer (either `bleve`, `meilisearch`, or empty for no index). | +| index.meili.host | OG_MEILI_HOST | none | Set the host for the Meiliseach server. | +| index.meili.api-key | OG_MEILI_API_KEY | none | Set the API key for the Meiliseach server. | +| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) | +| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) | +| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. Use an IP address for network binding. Use a path for Unix socket binding (e.g. /run/opengist.sock) | +| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. | +| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) | +| unix-socket-permissions | OG_UNIX_SOCKET_PERMISSIONS | `0666` | File permissions for Unix socket (octal format). | +| metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable Prometheus metrics endpoint at `/metrics` (`true` or `false`) | +| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) | +| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. | +| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. | +| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. | +| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. | +| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. | +| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. | +| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. | +| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. | +| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. | +| gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. | +| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. | +| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. | +| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. | +| gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. | +| oidc.provider-name | OG_OIDC_PROVIDER_NAME | none | The name of the OIDC provider | +| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. | +| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. | +| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. | +| ldap.url | OG_LDAP_URL | none | URL of the LDAP instance; if not set, LDAP authentication is disabled | +| ldap.bind-dn | OG_LDAP_BIND_DN | none | Bind DN to authenticate against the LDAP. e.g: cn=read-only-admin,dc=example,dc=com | +| ldap.bind-credentials | OG_LDAP_BIND_CREDENTIALS | none | The password for the Bind DN. | +| ldap.search-base | OG_LDAP_SEARCH_BASE | none | The Base DN to start search from. e.g: ou=People,dc=example,dc=com | +| ldap.search-filter | OG_LDAP_SEARCH_FILTER | none | The filter to search against (the format string %s will be replaced with the username). e.g: (uid=%s) | +| custom.name | OG_CUSTOM_NAME | none | The name of your instance, to be displayed in the tab title | +| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. | +| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. | +| custom.static-links | OG_CUSTOM_STATIC_LINK_#_(PATH,NAME) | none | Path and name to custom links, more info [here](custom-links.md). | diff --git a/internal/cli/main.go b/internal/cli/main.go index 9a832b2..3521297 100644 --- a/internal/cli/main.go +++ b/internal/cli/main.go @@ -36,11 +36,12 @@ var CmdStart = cli.Command{ Initialize(ctx) - go server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false).Start() + server := server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false) + go server.Start() go ssh.Start() <-stopCtx.Done() - shutdown() + shutdown(server) return nil }, } @@ -130,7 +131,7 @@ func Initialize(ctx *cli.Context) { } } -func shutdown() { +func shutdown(server *server.Server) { log.Info().Msg("Shutting down database...") if err := db.Close(); err != nil { log.Error().Err(err).Msg("Failed to close database") @@ -141,6 +142,8 @@ func shutdown() { index.Close() } + server.Stop() + log.Info().Msg("Shutdown complete") } diff --git a/internal/config/config.go b/internal/config/config.go index 1925fdb..45dfb8b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,6 +51,8 @@ type config struct { HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"` HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"` + UnixSocketPermissions string `yaml:"unix-socket-permissions" env:"OG_UNIX_SOCKET_PERMISSIONS"` + SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"` SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"` SshPort string `yaml:"ssh.port" env:"OG_SSH_PORT"` @@ -113,6 +115,8 @@ func configWithDefaults() (*config, error) { c.HttpPort = "6157" c.HttpGit = true + c.UnixSocketPermissions = "0666" + c.SshGit = true c.SshHost = "0.0.0.0" c.SshPort = "2222" diff --git a/internal/web/server/server.go b/internal/web/server/server.go index 1c4092a..96e6b52 100644 --- a/internal/web/server/server.go +++ b/internal/web/server/server.go @@ -1,8 +1,14 @@ package server import ( + "fmt" "github.com/thomiceli/opengist/internal/validator" + "net" "net/http" + "os" + "path/filepath" + "strconv" + "strings" "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" @@ -45,7 +51,19 @@ func NewServer(isDev bool, sessionsPath string, ignoreCsrf bool) *Server { return s } +func isSocketPath(host string) bool { + return strings.Contains(host, "/") || strings.Contains(host, "\\") +} + func (s *Server) Start() { + if isSocketPath(config.C.HttpHost) { + s.startUnixSocket() + } else { + s.startHTTP() + } +} + +func (s *Server) startHTTP() { addr := config.C.HttpHost + ":" + config.C.HttpPort log.Info().Msg("Starting HTTP server on http://" + addr) @@ -54,12 +72,106 @@ func (s *Server) Start() { } } +func (s *Server) startUnixSocket() { + socketPath := config.C.HttpHost + if socketPath == "" { + socketPath = "/tmp/opengist.sock" + } + + if dir := filepath.Dir(socketPath); dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + log.Warn().Err(err).Str("dir", dir).Msg("Failed to create socket directory") + } + } + if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { + log.Warn().Err(err).Str("socket", socketPath).Msg("Failed to remove existing socket file") + } + + pidPath := strings.TrimSuffix(socketPath, filepath.Ext(socketPath)) + ".pid" + if err := s.createPidFile(pidPath); err != nil { + log.Warn().Err(err).Str("pid-file", pidPath).Msg("Failed to create PID file") + } + + listener, err := net.Listen("unix", socketPath) + if err != nil { + log.Fatal().Err(err).Msg("Failed to start Unix socket server") + } + s.echo.Listener = listener + + if config.C.UnixSocketPermissions != "" { + if perm, err := strconv.ParseUint(config.C.UnixSocketPermissions, 8, 32); err == nil { + if err := os.Chmod(socketPath, os.FileMode(perm)); err != nil { + log.Warn().Err(err).Str("socket", socketPath).Str("permissions", config.C.UnixSocketPermissions).Msg("Failed to set socket permissions") + } + } else { + log.Warn().Err(err).Str("permissions", config.C.UnixSocketPermissions).Msg("Invalid socket permissions format") + } + } + + log.Info().Str("socket", socketPath).Msg("Starting Unix socket server") + log.Info().Str("pid-file", pidPath).Msg("PID file created") + server := new(http.Server) + if err := s.echo.StartServer(server); err != nil && err != http.ErrServerClosed { + log.Fatal().Err(err).Msg("Failed to start Unix socket server") + } +} + func (s *Server) Stop() { + if isSocketPath(config.C.HttpHost) { + s.stopUnixSocket() + } else { + s.stopHTTP() + } +} + +func (s *Server) stopHTTP() { + log.Info().Msg("Stopping HTTP server...") if err := s.echo.Close(); err != nil { log.Fatal().Err(err).Msg("Failed to stop HTTP server") } } +func (s *Server) stopUnixSocket() { + log.Info().Msg("Stopping Unix socket server...") + + var socketPath string + if s.echo.Listener != nil { + if unixListener, ok := s.echo.Listener.(*net.UnixListener); ok { + socketPath = unixListener.Addr().String() + } + } + + if err := s.echo.Close(); err != nil { + log.Error().Err(err).Msg("Failed to stop Unix socket server") + } + + if socketPath != "" { + if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { + log.Error().Err(err).Str("socket", socketPath).Msg("Failed to remove socket file") + } else { + log.Info().Str("socket", socketPath).Msg("Socket file removed") + } + + pidPath := strings.TrimSuffix(socketPath, filepath.Ext(socketPath)) + ".pid" + if err := os.Remove(pidPath); err != nil && !os.IsNotExist(err) { + log.Error().Err(err).Str("pid-file", pidPath).Msg("Failed to remove PID file") + } else { + log.Info().Str("pid-file", pidPath).Msg("PID file removed") + } + } +} + +func (s *Server) createPidFile(pidPath string) error { + pid := os.Getpid() + pidContent := fmt.Sprintf("%d\n", pid) + + if err := os.WriteFile(pidPath, []byte(pidContent), 0644); err != nil { + return err + } + + return nil +} + func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.echo.ServeHTTP(w, r) } diff --git a/scripts/watch.sh b/scripts/watch.sh index ddf85c9..7fb54f6 100755 --- a/scripts/watch.sh +++ b/scripts/watch.sh @@ -1,8 +1,23 @@ #!/bin/sh set -euo pipefail +# Start background processes make watch_frontend & -make watch_backend & +FRONTEND_PID=$! -trap 'kill $(jobs -p)' EXIT -wait +make watch_backend & +BACKEND_PID=$! + +# Function for graceful shutdown +cleanup() { + echo "Shutting down gracefully..." + kill -TERM $FRONTEND_PID $BACKEND_PID 2>/dev/null || true + wait $FRONTEND_PID $BACKEND_PID 2>/dev/null || true + echo "Shutdown complete" +} + +# Set up trap for graceful shutdown +trap cleanup EXIT INT TERM + +# Wait for background processes +wait \ No newline at end of file