mirror of
https://codeberg.org/forgejo/forgejo
synced 2025-10-18 22:50:36 +02:00
Fixes #9433. ``` $ ./gitea admin user create --username blah --must-change-password false Hint: boolean false must be specified as a single arg, eg. '--restricted=false', not '--restricted false' Command error: unexpected arguments: false ``` **Breaking**: CLI sub-commands that only have flags would previously ignore anything that might be considered an "extra" argument, and would proceed without any errors. I've manually tested this change on the single `admin user create` command with positive (ensuring cmd still works) and negative (ensuring errors are reported) test cases. I've attempted to ensure the change is applied only to commands which don't use the CLI `Args()` and avoided touching them, including: - `admin user must-change-password` takes a list of users - `doctor recreate-tables` takes a list of tables - `embedded [list/view/extract]` use a pattern of resources to operate upon - git repo hook subcommands, and the ssh serv command, use arguments and have been omitted from the change ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [x] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9458 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
367 lines
12 KiB
Go
367 lines
12 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
|
|
|
|
"forgejo.org/modules/container"
|
|
"forgejo.org/modules/graceful"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/process"
|
|
"forgejo.org/modules/public"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/routers"
|
|
"forgejo.org/routers/install"
|
|
|
|
"github.com/felixge/fgprof"
|
|
"github.com/urfave/cli/v3"
|
|
)
|
|
|
|
// PIDFile could be set from build tag
|
|
var PIDFile = "/run/gitea.pid"
|
|
|
|
// CmdWeb represents the available web sub-command.
|
|
func cmdWeb() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "web",
|
|
Usage: "Start the Forgejo web server",
|
|
Description: `The Forgejo web server is the only thing you need to run,
|
|
and it takes care of all the other things for you`,
|
|
Before: multipleBefore(noDanglingArgs, PrepareConsoleLoggerLevel(log.INFO)),
|
|
Action: runWeb,
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "port",
|
|
Aliases: []string{"p"},
|
|
Value: "3000",
|
|
Usage: "Temporary port number to prevent conflict",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "install-port",
|
|
Value: "3000",
|
|
Usage: "Temporary port number to run the install page on to prevent conflict",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "pid",
|
|
Aliases: []string{"P"},
|
|
Value: PIDFile,
|
|
Usage: "Custom pid file path",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "quiet",
|
|
Aliases: []string{"q"},
|
|
Usage: "Only display Fatal logging errors until logging is set-up",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "verbose",
|
|
Usage: "Set initial logging to TRACE level until logging is properly set-up",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func runHTTPRedirector() {
|
|
_, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), "Web: HTTP Redirector", process.SystemProcessType, true)
|
|
defer finished()
|
|
|
|
source := fmt.Sprintf("%s:%s", setting.HTTPAddr, setting.PortToRedirect)
|
|
dest := strings.TrimSuffix(setting.AppURL, "/")
|
|
log.Info("Redirecting: %s to %s", source, dest)
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
target := dest + r.URL.Path
|
|
if len(r.URL.RawQuery) > 0 {
|
|
target += "?" + r.URL.RawQuery
|
|
}
|
|
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
|
|
})
|
|
|
|
err := runHTTP("tcp", source, "HTTP Redirector", handler, setting.RedirectorUseProxyProtocol)
|
|
if err != nil {
|
|
log.Fatal("Failed to start port redirection: %v", err)
|
|
}
|
|
}
|
|
|
|
func createPIDFile(pidPath string) {
|
|
currentPid := os.Getpid()
|
|
if err := os.MkdirAll(filepath.Dir(pidPath), os.ModePerm); err != nil {
|
|
log.Fatal("Failed to create PID folder: %v", err)
|
|
}
|
|
|
|
file, err := os.Create(pidPath)
|
|
if err != nil {
|
|
log.Fatal("Failed to create PID file: %v", err)
|
|
}
|
|
defer file.Close()
|
|
if _, err := file.WriteString(strconv.FormatInt(int64(currentPid), 10)); err != nil {
|
|
log.Fatal("Failed to write PID information: %v", err)
|
|
}
|
|
}
|
|
|
|
func showWebStartupMessage(msg string) {
|
|
log.Info("Forgejo version: %s%s", setting.AppVer, setting.AppBuiltWith)
|
|
log.Info("* RunMode: %s", setting.RunMode)
|
|
log.Info("* AppPath: %s", setting.AppPath)
|
|
log.Info("* WorkPath: %s", setting.AppWorkPath)
|
|
log.Info("* CustomPath: %s", setting.CustomPath)
|
|
log.Info("* ConfigFile: %s", setting.CustomConf)
|
|
log.Info("%s", msg) // show startup message
|
|
|
|
if setting.CORSConfig.Enabled {
|
|
log.Info("CORS Service Enabled")
|
|
}
|
|
if setting.DefaultUILocation != time.Local {
|
|
log.Info("Default UI Location is %v", setting.DefaultUILocation.String())
|
|
}
|
|
if setting.MailService != nil {
|
|
log.Info("Mail Service Enabled: RegisterEmailConfirm=%v, Service.EnableNotifyMail=%v", setting.Service.RegisterEmailConfirm, setting.Service.EnableNotifyMail)
|
|
}
|
|
}
|
|
|
|
func serveInstall(_ context.Context, ctx *cli.Command) error {
|
|
showWebStartupMessage("Prepare to run install page")
|
|
|
|
routers.InitWebInstallPage(graceful.GetManager().HammerContext())
|
|
|
|
// Flag for port number in case first time run conflict
|
|
if ctx.IsSet("port") {
|
|
if err := setPort(ctx.String("port")); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if ctx.IsSet("install-port") {
|
|
if err := setPort(ctx.String("install-port")); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
c := install.Routes()
|
|
err := listen(c, false)
|
|
if err != nil {
|
|
log.Critical("Unable to open listener for installer. Is Forgejo already running?")
|
|
graceful.GetManager().DoGracefulShutdown()
|
|
}
|
|
select {
|
|
case <-graceful.GetManager().IsShutdown():
|
|
<-graceful.GetManager().Done()
|
|
log.Info("PID: %d Forgejo Web Finished", os.Getpid())
|
|
log.GetManager().Close()
|
|
return err
|
|
default:
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func serveInstalled(_ context.Context, ctx *cli.Command) error {
|
|
setting.InitCfgProvider(setting.CustomConf)
|
|
setting.LoadCommonSettings()
|
|
setting.MustInstalled()
|
|
|
|
showWebStartupMessage("Prepare to run web server")
|
|
|
|
if setting.AppWorkPathMismatch {
|
|
log.Error("WORK_PATH from config %q doesn't match other paths from environment variables or command arguments. "+
|
|
"Only WORK_PATH in config should be set and used. Please make sure the path in config file is correct, "+
|
|
"remove the other outdated work paths from environment variables and command arguments", setting.CustomConf)
|
|
}
|
|
|
|
rootCfg := setting.CfgProvider
|
|
if rootCfg.Section("").Key("WORK_PATH").String() == "" {
|
|
saveCfg, err := rootCfg.PrepareSaving()
|
|
if err != nil {
|
|
log.Error("Unable to prepare saving WORK_PATH=%s to config %q: %v\nYou should set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err)
|
|
} else {
|
|
rootCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
|
|
saveCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
|
|
if err = saveCfg.Save(); err != nil {
|
|
log.Error("Unable to update WORK_PATH=%s to config %q: %v\nYou should set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// in old versions, user's custom web files are placed in "custom/public", and they were served as "http://domain.com/assets/xxx"
|
|
// now, Gitea only serves pre-defined files in the "custom/public" folder basing on the web root, the user should move their custom files to "custom/public/assets"
|
|
publicFiles, _ := public.AssetFS().ListFiles(".")
|
|
publicFilesSet := container.SetOf(publicFiles...)
|
|
publicFilesSet.Remove(".well-known")
|
|
publicFilesSet.Remove("assets")
|
|
publicFilesSet.Remove("robots.txt")
|
|
for fn := range publicFilesSet.Seq() {
|
|
log.Error("Found legacy public asset %q in CustomPath. Please move it to %s/public/assets/%s", fn, setting.CustomPath, fn)
|
|
}
|
|
|
|
routers.InitWebInstalled(graceful.GetManager().HammerContext())
|
|
|
|
// We check that AppDataPath exists here (it should have been created during installation)
|
|
// We can't check it in `InitWebInstalled`, because some integration tests
|
|
// use cmd -> InitWebInstalled, but the AppDataPath doesn't exist during those tests.
|
|
if _, err := os.Stat(setting.AppDataPath); err != nil {
|
|
log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath)
|
|
}
|
|
|
|
// Override the provided port number within the configuration
|
|
if ctx.IsSet("port") {
|
|
if err := setPort(ctx.String("port")); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Set up Chi routes
|
|
webRoutes := routers.NormalRoutes()
|
|
err := listen(webRoutes, true)
|
|
<-graceful.GetManager().Done()
|
|
log.Info("PID: %d Forgejo Web Finished", os.Getpid())
|
|
log.GetManager().Close()
|
|
return err
|
|
}
|
|
|
|
func servePprof() {
|
|
http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
|
|
_, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true)
|
|
// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it.
|
|
log.Info("Starting pprof server on localhost:6060")
|
|
log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
|
|
finished()
|
|
}
|
|
|
|
func runWeb(ctx context.Context, cli *cli.Command) error {
|
|
defer func() {
|
|
if panicked := recover(); panicked != nil {
|
|
log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2))
|
|
}
|
|
}()
|
|
|
|
managerCtx, cancel := context.WithCancel(context.Background())
|
|
graceful.InitManager(managerCtx)
|
|
defer cancel()
|
|
|
|
if os.Getppid() > 1 && len(os.Getenv("LISTEN_FDS")) > 0 {
|
|
log.Info("Restarting Forgejo on PID: %d from parent PID: %d", os.Getpid(), os.Getppid())
|
|
} else {
|
|
log.Info("Starting Forgejo on PID: %d", os.Getpid())
|
|
}
|
|
|
|
// Set pid file setting
|
|
if cli.IsSet("pid") {
|
|
createPIDFile(cli.String("pid"))
|
|
}
|
|
|
|
if !setting.InstallLock {
|
|
if err := serveInstall(ctx, cli); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
NoInstallListener()
|
|
}
|
|
|
|
if setting.EnablePprof {
|
|
go servePprof()
|
|
}
|
|
|
|
return serveInstalled(ctx, cli)
|
|
}
|
|
|
|
func setPort(port string) error {
|
|
setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, port, 1)
|
|
setting.HTTPPort = port
|
|
|
|
switch setting.Protocol {
|
|
case setting.HTTPUnix:
|
|
case setting.FCGI:
|
|
case setting.FCGIUnix:
|
|
default:
|
|
defaultLocalURL := string(setting.Protocol) + "://"
|
|
if setting.HTTPAddr == "0.0.0.0" {
|
|
defaultLocalURL += "localhost"
|
|
} else {
|
|
defaultLocalURL += setting.HTTPAddr
|
|
}
|
|
defaultLocalURL += ":" + setting.HTTPPort + "/"
|
|
|
|
// Save LOCAL_ROOT_URL if port changed
|
|
rootCfg := setting.CfgProvider
|
|
saveCfg, err := rootCfg.PrepareSaving()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save config file: %v", err)
|
|
}
|
|
rootCfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL)
|
|
saveCfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL)
|
|
if err = saveCfg.Save(); err != nil {
|
|
return fmt.Errorf("failed to save config file: %v", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func listen(m http.Handler, handleRedirector bool) error {
|
|
listenAddr := setting.HTTPAddr
|
|
if setting.Protocol != setting.HTTPUnix && setting.Protocol != setting.FCGIUnix {
|
|
listenAddr = net.JoinHostPort(listenAddr, setting.HTTPPort)
|
|
}
|
|
_, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), "Web: Forgejo Server", process.SystemProcessType, true)
|
|
defer finished()
|
|
log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubURL)
|
|
// This can be useful for users, many users do wrong to their config and get strange behaviors behind a reverse-proxy.
|
|
// A user may fix the configuration mistake when he sees this log.
|
|
// And this is also very helpful to maintainers to provide help to users to resolve their configuration problems.
|
|
log.Info("AppURL(ROOT_URL): %s", setting.AppURL)
|
|
|
|
if setting.LFS.StartServer {
|
|
log.Info("LFS server enabled")
|
|
}
|
|
|
|
var err error
|
|
switch setting.Protocol {
|
|
case setting.HTTP:
|
|
if handleRedirector {
|
|
NoHTTPRedirector()
|
|
}
|
|
err = runHTTP("tcp", listenAddr, "Web", m, setting.UseProxyProtocol)
|
|
case setting.HTTPS:
|
|
if setting.EnableAcme {
|
|
err = runACME(listenAddr, m)
|
|
break
|
|
}
|
|
if handleRedirector {
|
|
if setting.RedirectOtherPort {
|
|
go runHTTPRedirector()
|
|
} else {
|
|
NoHTTPRedirector()
|
|
}
|
|
}
|
|
err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m, setting.UseProxyProtocol, setting.ProxyProtocolTLSBridging)
|
|
case setting.FCGI:
|
|
if handleRedirector {
|
|
NoHTTPRedirector()
|
|
}
|
|
err = runFCGI("tcp", listenAddr, "FCGI Web", m, setting.UseProxyProtocol)
|
|
case setting.HTTPUnix:
|
|
if handleRedirector {
|
|
NoHTTPRedirector()
|
|
}
|
|
err = runHTTP("unix", listenAddr, "Web", m, setting.UseProxyProtocol)
|
|
case setting.FCGIUnix:
|
|
if handleRedirector {
|
|
NoHTTPRedirector()
|
|
}
|
|
err = runFCGI("unix", listenAddr, "Web", m, setting.UseProxyProtocol)
|
|
default:
|
|
log.Fatal("Invalid protocol: %s", setting.Protocol)
|
|
}
|
|
if err != nil {
|
|
log.Critical("Failed to start server: %v", err)
|
|
}
|
|
log.Info("HTTP Listener: %s Closed", listenAddr)
|
|
return err
|
|
}
|