diff --git a/modules/auth/password/hash/setting.go b/modules/auth/password/hash/setting.go index 7016974304..f0715f31e1 100644 --- a/modules/auth/password/hash/setting.go +++ b/modules/auth/password/hash/setting.go @@ -41,9 +41,8 @@ var RecommendedHashAlgorithms = []string{ "pbkdf2_hi", } -// SetDefaultPasswordHashAlgorithm will take a provided algorithmName and dealias it to -// a complete algorithm specification. -func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) { +// hashAlgorithmToSpec converts an algorithm name or a specification to a full algorithm specification +func hashAlgorithmToSpec(algorithmName string) string { if algorithmName == "" { algorithmName = DefaultHashAlgorithmName } @@ -52,10 +51,26 @@ func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHas algorithmName = alias alias, has = aliasAlgorithmNames[algorithmName] } - - // algorithmName should now be a full algorithm specification - // e.g. pbkdf2$50000$50 rather than pbdkf2 - DefaultHashAlgorithm = Parse(algorithmName) - - return algorithmName, DefaultHashAlgorithm + return algorithmName +} + +// SetDefaultPasswordHashAlgorithm will take a provided algorithmName and de-alias it to +// a complete algorithm specification. +func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) { + algoSpec := hashAlgorithmToSpec(algorithmName) + // now we get a full specification, e.g. pbkdf2$50000$50 rather than pbdkf2 + DefaultHashAlgorithm = Parse(algoSpec) + return algoSpec, DefaultHashAlgorithm +} + +// ConfigHashAlgorithm will try to find a "recommended algorithm name" defined by RecommendedHashAlgorithms for config +// This function is not fast and is only used for the installation page +func ConfigHashAlgorithm(algorithm string) string { + algorithm = hashAlgorithmToSpec(algorithm) + for _, recommAlgo := range RecommendedHashAlgorithms { + if algorithm == hashAlgorithmToSpec(recommAlgo) { + return recommAlgo + } + } + return algorithm } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index df66ce2339..b2fb837bf0 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -237,7 +237,6 @@ internal_token_failed = Failed to generate internal token: %v secret_key_failed = Failed to generate secret key: %v save_config_failed = Failed to save configuration: %v invalid_admin_setting = Administrator account setting is invalid: %v -install_success = Welcome! Thank you for choosing Gitea. Have fun and take care! invalid_log_root_path = The log path is invalid: %v default_keep_email_private = Hide Email Addresses by Default default_keep_email_private_popup = Hide email addresses of new user accounts by default. @@ -248,6 +247,7 @@ default_enable_timetracking_popup = Enable time tracking for new repositories by no_reply_address = Hidden Email Domain no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'. password_algorithm = Password Hash Algorithm +invalid_password_algorithm = Invalid password hash algorithm password_algorithm_helper = Set the password hashing algorithm. Algorithms have differing requirements and strength. `argon2` whilst having good characteristics uses a lot of memory and may be inappropriate for small systems. enable_update_checker = Enable Update Checker enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io. diff --git a/routers/install/install.go b/routers/install/install.go index a3d64e5f73..a377c2950b 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -59,11 +59,6 @@ func Init(ctx goctx.Context) func(next http.Handler) http.Handler { dbTypeNames := getSupportedDbTypeNames() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - if setting.InstallLock { - resp.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login") - _ = rnd.HTML(resp, http.StatusOK, string(tplPostInstall), nil) - return - } locale := middleware.Locale(resp, req) startTime := time.Now() ctx := context.Context{ @@ -93,6 +88,11 @@ func Init(ctx goctx.Context) func(next http.Handler) http.Handler { // Install render installation page func Install(ctx *context.Context) { + if setting.InstallLock { + InstallDone(ctx) + return + } + form := forms.InstallForm{} // Database settings @@ -162,7 +162,7 @@ func Install(ctx *context.Context) { form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking form.NoReplyAddress = setting.Service.NoReplyAddress - form.PasswordAlgorithm = setting.PasswordHashAlgo + form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo) middleware.AssignForm(form, ctx.Data) ctx.HTML(http.StatusOK, tplInstall) @@ -234,6 +234,11 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { // SubmitInstall response for submit install items func SubmitInstall(ctx *context.Context) { + if setting.InstallLock { + InstallDone(ctx) + return + } + var err error form := *web.GetForm(ctx).(*forms.InstallForm) @@ -277,7 +282,6 @@ func SubmitInstall(ctx *context.Context) { setting.Database.Charset = form.Charset setting.Database.Path = form.DbPath setting.Database.LogSQL = !setting.IsProd - setting.PasswordHashAlgo = form.PasswordAlgorithm if !checkDatabase(ctx, &form) { return @@ -499,6 +503,12 @@ func SubmitInstall(ctx *context.Context) { } if len(form.PasswordAlgorithm) > 0 { + var algorithm *hash.PasswordHashAlgorithm + setting.PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(form.PasswordAlgorithm) + if algorithm == nil { + ctx.RenderWithErr(ctx.Tr("install.invalid_password_algorithm"), tplInstall, &form) + return + } cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm) } @@ -571,18 +581,26 @@ func SubmitInstall(ctx *context.Context) { } log.Info("First-time run install finished!") + InstallDone(ctx) - ctx.Flash.Success(ctx.Tr("install.install_success")) - - ctx.RespHeader().Add("Refresh", "1; url="+setting.AppURL+"user/login") - ctx.HTML(http.StatusOK, tplPostInstall) - - // Now get the http.Server from this request and shut it down - // NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown - srv := ctx.Value(http.ServerContextKey).(*http.Server) go func() { + // Sleep for a while to make sure the user's browser has loaded the post-install page and its assets (images, css, js) + // What if this duration is not long enough? That's impossible -- if the user can't load the simple page in time, how could they install or use Gitea in the future .... + time.Sleep(3 * time.Second) + + // Now get the http.Server from this request and shut it down + // NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown + srv := ctx.Value(http.ServerContextKey).(*http.Server) if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil { log.Error("Unable to shutdown the install server! Error: %v", err) } + + // After the HTTP server for "install" shuts down, the `runWeb()` will continue to run the "normal" server }() } + +// InstallDone shows the "post-install" page, makes it easier to develop the page. +// The name is not called as "PostInstall" to avoid misinterpretation as a handler for "POST /install" +func InstallDone(ctx *context.Context) { //nolint + ctx.HTML(http.StatusOK, tplPostInstall) +} diff --git a/routers/install/routes.go b/routers/install/routes.go index 9aa5a88d24..a8efc92fe1 100644 --- a/routers/install/routes.go +++ b/routers/install/routes.go @@ -6,6 +6,7 @@ package install import ( goctx "context" "fmt" + "html" "net/http" "path" @@ -37,7 +38,7 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler { // Why we need this? The first recover will try to render a beautiful // error page for user, but the process can still panic again, then // we have to just recover twice and send a simple error page that - // should not panic any more. + // should not panic anymore. defer func() { if err := recover(); err != nil { combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, log.Stack(2)) @@ -107,8 +108,9 @@ func Routes(ctx goctx.Context) *web.Route { r.Use(installRecovery(ctx)) r.Use(Init(ctx)) - r.Get("/", Install) + r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall) + r.Get("/post-install", InstallDone) r.Get("/api/healthz", healthcheck.Check) r.NotFound(web.Wrap(installNotFound)) @@ -116,5 +118,10 @@ func Routes(ctx goctx.Context) *web.Route { } func installNotFound(w http.ResponseWriter, req *http.Request) { - http.Redirect(w, req, setting.AppURL, http.StatusFound) + w.Header().Add("Content-Type", "text/html; charset=utf-8") + w.Header().Add("Refresh", fmt.Sprintf("1; url=%s", setting.AppSubURL+"/")) + // do not use 30x status, because the "post-install" page needs to use 404/200 to detect if Gitea has been installed. + // the fetch API could follow 30x requests to the page with 200 status. + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprintf(w, `Not Found. Go to default page.`, html.EscapeString(setting.AppSubURL+"/")) } diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index c552dcfd2d..d179140b23 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -4,7 +4,7 @@ {{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}} - + {{if .ManifestData}}{{end}} diff --git a/templates/post-install.tmpl b/templates/post-install.tmpl index 0d6cd3082c..f237a6e01b 100644 --- a/templates/post-install.tmpl +++ b/templates/post-install.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
diff --git a/web_src/js/features/install.js b/web_src/js/features/install.js index 2ba6fe1279..23122ca4c3 100644 --- a/web_src/js/features/install.js +++ b/web_src/js/features/install.js @@ -2,10 +2,18 @@ import $ from 'jquery'; import {hideElem, showElem} from '../utils/dom.js'; export function initInstall() { - if ($('.page-content.install').length === 0) { + const $page = $('.page-content.install'); + if ($page.length === 0) { return; } + if ($page.is('.post-install')) { + initPostInstall(); + } else { + initPreInstall(); + } +} +function initPreInstall() { const defaultDbUser = 'gitea'; const defaultDbName = 'gitea'; @@ -40,6 +48,18 @@ export function initInstall() { } // else: for SQLite3, the default path is always prepared by backend code (setting) }).trigger('change'); + const $appUrl = $('#app_url'); + const configAppUrl = $appUrl.val(); + if (configAppUrl.includes('://localhost')) { + $appUrl.val(window.location.href); + } + + const $domain = $('#domain'); + const configDomain = $domain.val().trim(); + if (configDomain === 'localhost') { + $domain.val(window.location.hostname); + } + // TODO: better handling of exclusive relations. $('#offline-mode input').on('change', function () { if ($(this).is(':checked')) { @@ -83,3 +103,20 @@ export function initInstall() { } }); } + +function initPostInstall() { + const el = document.getElementById('goto-user-login'); + if (!el) return; + + const targetUrl = el.getAttribute('href'); + let tid = setInterval(async () => { + try { + const resp = await fetch(targetUrl); + if (tid && resp.status === 200) { + clearInterval(tid); + tid = null; + window.location.href = targetUrl; + } + } catch {} + }, 1000); +}