1// Copyright 2014 The Gogs Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package cmd
5
6import (
7 "context"
8 "fmt"
9 "net"
10 "net/http"
11 "os"
12 "path/filepath"
13 "strconv"
14 "strings"
15 "time"
16
17 _ "net/http/pprof" // Used for debugging if enabled and a web server is running
18
19 "forgejo.org/modules/container"
20 "forgejo.org/modules/graceful"
21 "forgejo.org/modules/log"
22 "forgejo.org/modules/process"
23 "forgejo.org/modules/public"
24 "forgejo.org/modules/setting"
25 "forgejo.org/routers"
26 "forgejo.org/routers/install"
27
28 "github.com/felixge/fgprof"
29 "github.com/urfave/cli/v2"
30)
31
32// PIDFile could be set from build tag
33var PIDFile = "/run/gitea.pid"
34
35// CmdWeb represents the available web sub-command.
36var CmdWeb = &cli.Command{
37 Name: "web",
38 Usage: "Start the Forgejo web server",
39 Description: `The Forgejo web server is the only thing you need to run,
40and it takes care of all the other things for you`,
41 Before: PrepareConsoleLoggerLevel(log.INFO),
42 Action: runWeb,
43 Flags: []cli.Flag{
44 &cli.StringFlag{
45 Name: "port",
46 Aliases: []string{"p"},
47 Value: "3000",
48 Usage: "Temporary port number to prevent conflict",
49 },
50 &cli.StringFlag{
51 Name: "install-port",
52 Value: "3000",
53 Usage: "Temporary port number to run the install page on to prevent conflict",
54 },
55 &cli.StringFlag{
56 Name: "pid",
57 Aliases: []string{"P"},
58 Value: PIDFile,
59 Usage: "Custom pid file path",
60 },
61 &cli.BoolFlag{
62 Name: "quiet",
63 Aliases: []string{"q"},
64 Usage: "Only display Fatal logging errors until logging is set-up",
65 },
66 &cli.BoolFlag{
67 Name: "verbose",
68 Usage: "Set initial logging to TRACE level until logging is properly set-up",
69 },
70 },
71}
72
73func runHTTPRedirector() {
74 _, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), "Web: HTTP Redirector", process.SystemProcessType, true)
75 defer finished()
76
77 source := fmt.Sprintf("%s:%s", setting.HTTPAddr, setting.PortToRedirect)
78 dest := strings.TrimSuffix(setting.AppURL, "/")
79 log.Info("Redirecting: %s to %s", source, dest)
80
81 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82 target := dest + r.URL.Path
83 if len(r.URL.RawQuery) > 0 {
84 target += "?" + r.URL.RawQuery
85 }
86 http.Redirect(w, r, target, http.StatusTemporaryRedirect)
87 })
88
89 err := runHTTP("tcp", source, "HTTP Redirector", handler, setting.RedirectorUseProxyProtocol)
90 if err != nil {
91 log.Fatal("Failed to start port redirection: %v", err)
92 }
93}
94
95func createPIDFile(pidPath string) {
96 currentPid := os.Getpid()
97 if err := os.MkdirAll(filepath.Dir(pidPath), os.ModePerm); err != nil {
98 log.Fatal("Failed to create PID folder: %v", err)
99 }
100
101 file, err := os.Create(pidPath)
102 if err != nil {
103 log.Fatal("Failed to create PID file: %v", err)
104 }
105 defer file.Close()
106 if _, err := file.WriteString(strconv.FormatInt(int64(currentPid), 10)); err != nil {
107 log.Fatal("Failed to write PID information: %v", err)
108 }
109}
110
111func showWebStartupMessage(msg string) {
112 log.Info("Forgejo version: %s%s", setting.AppVer, setting.AppBuiltWith)
113 log.Info("* RunMode: %s", setting.RunMode)
114 log.Info("* AppPath: %s", setting.AppPath)
115 log.Info("* WorkPath: %s", setting.AppWorkPath)
116 log.Info("* CustomPath: %s", setting.CustomPath)
117 log.Info("* ConfigFile: %s", setting.CustomConf)
118 log.Info("%s", msg) // show startup message
119
120 if setting.CORSConfig.Enabled {
121 log.Info("CORS Service Enabled")
122 }
123 if setting.DefaultUILocation != time.Local {
124 log.Info("Default UI Location is %v", setting.DefaultUILocation.String())
125 }
126 if setting.MailService != nil {
127 log.Info("Mail Service Enabled: RegisterEmailConfirm=%v, Service.EnableNotifyMail=%v", setting.Service.RegisterEmailConfirm, setting.Service.EnableNotifyMail)
128 }
129}
130
131func serveInstall(ctx *cli.Context) error {
132 showWebStartupMessage("Prepare to run install page")
133
134 routers.InitWebInstallPage(graceful.GetManager().HammerContext())
135
136 // Flag for port number in case first time run conflict
137 if ctx.IsSet("port") {
138 if err := setPort(ctx.String("port")); err != nil {
139 return err
140 }
141 }
142 if ctx.IsSet("install-port") {
143 if err := setPort(ctx.String("install-port")); err != nil {
144 return err
145 }
146 }
147 c := install.Routes()
148 err := listen(c, false)
149 if err != nil {
150 log.Critical("Unable to open listener for installer. Is Forgejo already running?")
151 graceful.GetManager().DoGracefulShutdown()
152 }
153 select {
154 case <-graceful.GetManager().IsShutdown():
155 <-graceful.GetManager().Done()
156 log.Info("PID: %d Forgejo Web Finished", os.Getpid())
157 log.GetManager().Close()
158 return err
159 default:
160 }
161 return nil
162}
163
164func serveInstalled(ctx *cli.Context) error {
165 setting.InitCfgProvider(setting.CustomConf)
166 setting.LoadCommonSettings()
167 setting.MustInstalled()
168
169 showWebStartupMessage("Prepare to run web server")
170
171 if setting.AppWorkPathMismatch {
172 log.Error("WORK_PATH from config %q doesn't match other paths from environment variables or command arguments. "+
173 "Only WORK_PATH in config should be set and used. Please make sure the path in config file is correct, "+
174 "remove the other outdated work paths from environment variables and command arguments", setting.CustomConf)
175 }
176
177 rootCfg := setting.CfgProvider
178 if rootCfg.Section("").Key("WORK_PATH").String() == "" {
179 saveCfg, err := rootCfg.PrepareSaving()
180 if err != nil {
181 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)
182 } else {
183 rootCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
184 saveCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
185 if err = saveCfg.Save(); err != nil {
186 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)
187 }
188 }
189 }
190
191 // in old versions, user's custom web files are placed in "custom/public", and they were served as "http://domain.com/assets/xxx"
192 // 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"
193 publicFiles, _ := public.AssetFS().ListFiles(".")
194 publicFilesSet := container.SetOf(publicFiles...)
195 publicFilesSet.Remove(".well-known")
196 publicFilesSet.Remove("assets")
197 publicFilesSet.Remove("robots.txt")
198 for fn := range publicFilesSet.Seq() {
199 log.Error("Found legacy public asset %q in CustomPath. Please move it to %s/public/assets/%s", fn, setting.CustomPath, fn)
200 }
201
202 routers.InitWebInstalled(graceful.GetManager().HammerContext())
203
204 // We check that AppDataPath exists here (it should have been created during installation)
205 // We can't check it in `InitWebInstalled`, because some integration tests
206 // use cmd -> InitWebInstalled, but the AppDataPath doesn't exist during those tests.
207 if _, err := os.Stat(setting.AppDataPath); err != nil {
208 log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath)
209 }
210
211 // Override the provided port number within the configuration
212 if ctx.IsSet("port") {
213 if err := setPort(ctx.String("port")); err != nil {
214 return err
215 }
216 }
217
218 // Set up Chi routes
219 webRoutes := routers.NormalRoutes()
220 err := listen(webRoutes, true)
221 <-graceful.GetManager().Done()
222 log.Info("PID: %d Forgejo Web Finished", os.Getpid())
223 log.GetManager().Close()
224 return err
225}
226
227func servePprof() {
228 http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
229 _, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true)
230 // 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.
231 log.Info("Starting pprof server on localhost:6060")
232 log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
233 finished()
234}
235
236func runWeb(ctx *cli.Context) error {
237 defer func() {
238 if panicked := recover(); panicked != nil {
239 log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2))
240 }
241 }()
242
243 managerCtx, cancel := context.WithCancel(context.Background())
244 graceful.InitManager(managerCtx)
245 defer cancel()
246
247 if os.Getppid() > 1 && len(os.Getenv("LISTEN_FDS")) > 0 {
248 log.Info("Restarting Forgejo on PID: %d from parent PID: %d", os.Getpid(), os.Getppid())
249 } else {
250 log.Info("Starting Forgejo on PID: %d", os.Getpid())
251 }
252
253 // Set pid file setting
254 if ctx.IsSet("pid") {
255 createPIDFile(ctx.String("pid"))
256 }
257
258 if !setting.InstallLock {
259 if err := serveInstall(ctx); err != nil {
260 return err
261 }
262 } else {
263 NoInstallListener()
264 }
265
266 if setting.EnablePprof {
267 go servePprof()
268 }
269
270 return serveInstalled(ctx)
271}
272
273func setPort(port string) error {
274 setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, port, 1)
275 setting.HTTPPort = port
276
277 switch setting.Protocol {
278 case setting.HTTPUnix:
279 case setting.FCGI:
280 case setting.FCGIUnix:
281 default:
282 defaultLocalURL := string(setting.Protocol) + "://"
283 if setting.HTTPAddr == "0.0.0.0" {
284 defaultLocalURL += "localhost"
285 } else {
286 defaultLocalURL += setting.HTTPAddr
287 }
288 defaultLocalURL += ":" + setting.HTTPPort + "/"
289
290 // Save LOCAL_ROOT_URL if port changed
291 rootCfg := setting.CfgProvider
292 saveCfg, err := rootCfg.PrepareSaving()
293 if err != nil {
294 return fmt.Errorf("failed to save config file: %v", err)
295 }
296 rootCfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL)
297 saveCfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL)
298 if err = saveCfg.Save(); err != nil {
299 return fmt.Errorf("failed to save config file: %v", err)
300 }
301 }
302 return nil
303}
304
305func listen(m http.Handler, handleRedirector bool) error {
306 listenAddr := setting.HTTPAddr
307 if setting.Protocol != setting.HTTPUnix && setting.Protocol != setting.FCGIUnix {
308 listenAddr = net.JoinHostPort(listenAddr, setting.HTTPPort)
309 }
310 _, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), "Web: Forgejo Server", process.SystemProcessType, true)
311 defer finished()
312 log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubURL)
313 // This can be useful for users, many users do wrong to their config and get strange behaviors behind a reverse-proxy.
314 // A user may fix the configuration mistake when he sees this log.
315 // And this is also very helpful to maintainers to provide help to users to resolve their configuration problems.
316 log.Info("AppURL(ROOT_URL): %s", setting.AppURL)
317
318 if setting.LFS.StartServer {
319 log.Info("LFS server enabled")
320 }
321
322 var err error
323 switch setting.Protocol {
324 case setting.HTTP:
325 if handleRedirector {
326 NoHTTPRedirector()
327 }
328 err = runHTTP("tcp", listenAddr, "Web", m, setting.UseProxyProtocol)
329 case setting.HTTPS:
330 if setting.EnableAcme {
331 err = runACME(listenAddr, m)
332 break
333 }
334 if handleRedirector {
335 if setting.RedirectOtherPort {
336 go runHTTPRedirector()
337 } else {
338 NoHTTPRedirector()
339 }
340 }
341 err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m, setting.UseProxyProtocol, setting.ProxyProtocolTLSBridging)
342 case setting.FCGI:
343 if handleRedirector {
344 NoHTTPRedirector()
345 }
346 err = runFCGI("tcp", listenAddr, "FCGI Web", m, setting.UseProxyProtocol)
347 case setting.HTTPUnix:
348 if handleRedirector {
349 NoHTTPRedirector()
350 }
351 err = runHTTP("unix", listenAddr, "Web", m, setting.UseProxyProtocol)
352 case setting.FCGIUnix:
353 if handleRedirector {
354 NoHTTPRedirector()
355 }
356 err = runFCGI("unix", listenAddr, "Web", m, setting.UseProxyProtocol)
357 default:
358 log.Fatal("Invalid protocol: %s", setting.Protocol)
359 }
360 if err != nil {
361 log.Critical("Failed to start server: %v", err)
362 }
363 log.Info("HTTP Listener: %s Closed", listenAddr)
364 return err
365}