mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00:00 
			
		
		
		
	Fix various bugs for "install" page (#23194)
## TLDR * Fix the broken page / broken image problem when click "Install" * Close #20089 * Fix the Password Hash Algorithm display problem for #22942 * Close #23183 * Close #23184 ## Details ### The broken page / broken image problem when click "Install" (Redirect failed after install gitea #23184) Before: when click "install", all new requests will fail, because the server has been restarted. Users just see a broken page with broken images, sometimes the server is not ready but the user would have been redirect to "/user/login" page, then the users see a new broken page (connection refused or something wrong ...) After: only check InstallLock=true for necessary handlers, and sleep for a while before restarting the server, then the browser has enough time to load the "post-install" page. And there is a script to check whether "/user/login" is ready, the user will only be redirected to the login page when the server is ready. ### During new instance setup make 'Gitea Base URL' filled from window.location.origin #20089 If the "app_url" input contains `localhost` (the default value from config), use current window's location href as the `app_url` (aka ROOT_URL) ### Fix the Password Hash Algorithm display problem for "Provide the ability to set password hash algorithm parameters #22942" Before: the UI shows `pbkdf2$50000$50` <details>  </details> After: the UI shows `pbkdf2` <details>  </details> ### GET data: net::ERR_INVALID_URL #23183 Cause by empty `data:` in `<link rel="manifest" href="data:{{.ManifestData}}">` --------- Co-authored-by: Jason Song <i@wolfogre.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
		| @@ -41,9 +41,8 @@ var RecommendedHashAlgorithms = []string{ | |||||||
| 	"pbkdf2_hi", | 	"pbkdf2_hi", | ||||||
| } | } | ||||||
|  |  | ||||||
| // SetDefaultPasswordHashAlgorithm will take a provided algorithmName and dealias it to | // hashAlgorithmToSpec converts an algorithm name or a specification to a full algorithm specification | ||||||
| // a complete algorithm specification. | func hashAlgorithmToSpec(algorithmName string) string { | ||||||
| func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) { |  | ||||||
| 	if algorithmName == "" { | 	if algorithmName == "" { | ||||||
| 		algorithmName = DefaultHashAlgorithmName | 		algorithmName = DefaultHashAlgorithmName | ||||||
| 	} | 	} | ||||||
| @@ -52,10 +51,26 @@ func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHas | |||||||
| 		algorithmName = alias | 		algorithmName = alias | ||||||
| 		alias, has = aliasAlgorithmNames[algorithmName] | 		alias, has = aliasAlgorithmNames[algorithmName] | ||||||
| 	} | 	} | ||||||
|  | 	return algorithmName | ||||||
| 	// algorithmName should now be a full algorithm specification | } | ||||||
| 	// e.g. pbkdf2$50000$50 rather than pbdkf2 |  | ||||||
| 	DefaultHashAlgorithm = Parse(algorithmName) | // SetDefaultPasswordHashAlgorithm will take a provided algorithmName and de-alias it to | ||||||
|  | // a complete algorithm specification. | ||||||
| 	return algorithmName, DefaultHashAlgorithm | 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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -237,7 +237,6 @@ internal_token_failed = Failed to generate internal token: %v | |||||||
| secret_key_failed = Failed to generate secret key: %v | secret_key_failed = Failed to generate secret key: %v | ||||||
| save_config_failed = Failed to save configuration: %v | save_config_failed = Failed to save configuration: %v | ||||||
| invalid_admin_setting = Administrator account setting is invalid: %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 | invalid_log_root_path = The log path is invalid: %v | ||||||
| default_keep_email_private = Hide Email Addresses by Default | default_keep_email_private = Hide Email Addresses by Default | ||||||
| default_keep_email_private_popup = Hide email addresses of new user accounts 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 = 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'. | 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 | 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. | 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 = Enable Update Checker | ||||||
| enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io. | enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io. | ||||||
|   | |||||||
| @@ -59,11 +59,6 @@ func Init(ctx goctx.Context) func(next http.Handler) http.Handler { | |||||||
| 	dbTypeNames := getSupportedDbTypeNames() | 	dbTypeNames := getSupportedDbTypeNames() | ||||||
| 	return func(next http.Handler) http.Handler { | 	return func(next http.Handler) http.Handler { | ||||||
| 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { | 		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) | 			locale := middleware.Locale(resp, req) | ||||||
| 			startTime := time.Now() | 			startTime := time.Now() | ||||||
| 			ctx := context.Context{ | 			ctx := context.Context{ | ||||||
| @@ -93,6 +88,11 @@ func Init(ctx goctx.Context) func(next http.Handler) http.Handler { | |||||||
|  |  | ||||||
| // Install render installation page | // Install render installation page | ||||||
| func Install(ctx *context.Context) { | func Install(ctx *context.Context) { | ||||||
|  | 	if setting.InstallLock { | ||||||
|  | 		InstallDone(ctx) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	form := forms.InstallForm{} | 	form := forms.InstallForm{} | ||||||
|  |  | ||||||
| 	// Database settings | 	// Database settings | ||||||
| @@ -162,7 +162,7 @@ func Install(ctx *context.Context) { | |||||||
| 	form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization | 	form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization | ||||||
| 	form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking | 	form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking | ||||||
| 	form.NoReplyAddress = setting.Service.NoReplyAddress | 	form.NoReplyAddress = setting.Service.NoReplyAddress | ||||||
| 	form.PasswordAlgorithm = setting.PasswordHashAlgo | 	form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo) | ||||||
|  |  | ||||||
| 	middleware.AssignForm(form, ctx.Data) | 	middleware.AssignForm(form, ctx.Data) | ||||||
| 	ctx.HTML(http.StatusOK, tplInstall) | 	ctx.HTML(http.StatusOK, tplInstall) | ||||||
| @@ -234,6 +234,11 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { | |||||||
|  |  | ||||||
| // SubmitInstall response for submit install items | // SubmitInstall response for submit install items | ||||||
| func SubmitInstall(ctx *context.Context) { | func SubmitInstall(ctx *context.Context) { | ||||||
|  | 	if setting.InstallLock { | ||||||
|  | 		InstallDone(ctx) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	var err error | 	var err error | ||||||
|  |  | ||||||
| 	form := *web.GetForm(ctx).(*forms.InstallForm) | 	form := *web.GetForm(ctx).(*forms.InstallForm) | ||||||
| @@ -277,7 +282,6 @@ func SubmitInstall(ctx *context.Context) { | |||||||
| 	setting.Database.Charset = form.Charset | 	setting.Database.Charset = form.Charset | ||||||
| 	setting.Database.Path = form.DbPath | 	setting.Database.Path = form.DbPath | ||||||
| 	setting.Database.LogSQL = !setting.IsProd | 	setting.Database.LogSQL = !setting.IsProd | ||||||
| 	setting.PasswordHashAlgo = form.PasswordAlgorithm |  | ||||||
|  |  | ||||||
| 	if !checkDatabase(ctx, &form) { | 	if !checkDatabase(ctx, &form) { | ||||||
| 		return | 		return | ||||||
| @@ -499,6 +503,12 @@ func SubmitInstall(ctx *context.Context) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(form.PasswordAlgorithm) > 0 { | 	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) | 		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!") | 	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() { | 	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 { | 		if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil { | ||||||
| 			log.Error("Unable to shutdown the install server! Error: %v", err) | 			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) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ package install | |||||||
| import ( | import ( | ||||||
| 	goctx "context" | 	goctx "context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"html" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"path" | 	"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 | 				// 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 | 				// 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 | 				// we have to just recover twice and send a simple error page that | ||||||
| 				// should not panic any more. | 				// should not panic anymore. | ||||||
| 				defer func() { | 				defer func() { | ||||||
| 					if err := recover(); err != nil { | 					if err := recover(); err != nil { | ||||||
| 						combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, log.Stack(2)) | 						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(installRecovery(ctx)) | ||||||
| 	r.Use(Init(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.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall) | ||||||
|  | 	r.Get("/post-install", InstallDone) | ||||||
| 	r.Get("/api/healthz", healthcheck.Check) | 	r.Get("/api/healthz", healthcheck.Check) | ||||||
|  |  | ||||||
| 	r.NotFound(web.Wrap(installNotFound)) | 	r.NotFound(web.Wrap(installNotFound)) | ||||||
| @@ -116,5 +118,10 @@ func Routes(ctx goctx.Context) *web.Route { | |||||||
| } | } | ||||||
|  |  | ||||||
| func installNotFound(w http.ResponseWriter, req *http.Request) { | 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. <a href="%s">Go to default page</a>.`, html.EscapeString(setting.AppSubURL+"/")) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
| 	<meta charset="utf-8"> | 	<meta charset="utf-8"> | ||||||
| 	<meta name="viewport" content="width=device-width, initial-scale=1"> | 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
| 	<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title> | 	<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title> | ||||||
| 	<link rel="manifest" href="data:{{.ManifestData}}"> | 	{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}} | ||||||
| 	<meta name="theme-color" content="{{ThemeColorMetaTag}}"> | 	<meta name="theme-color" content="{{ThemeColorMetaTag}}"> | ||||||
| 	<meta name="default-theme" content="{{DefaultTheme}}"> | 	<meta name="default-theme" content="{{DefaultTheme}}"> | ||||||
| 	<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}"> | 	<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}"> | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| {{template "base/head" .}} | {{template "base/head" .}} | ||||||
| <div role="main" aria-label="{{.Title}}" class="page-content install"> | <div role="main" aria-label="{{.Title}}" class="page-content install post-install"> | ||||||
| 	<div class="ui container"> | 	<div class="ui container"> | ||||||
| 		<div class="ui grid"> | 		<div class="ui grid"> | ||||||
| 			<div class="sixteen wide column content"> | 			<div class="sixteen wide column content"> | ||||||
| @@ -13,7 +13,7 @@ | |||||||
| 					</div> | 					</div> | ||||||
| 					<div class="ui stackable middle very relaxed page grid"> | 					<div class="ui stackable middle very relaxed page grid"> | ||||||
| 						<div class="sixteen wide center aligned centered column"> | 						<div class="sixteen wide center aligned centered column"> | ||||||
| 							<p><a href="{{AppSubUrl}}/user/login">{{AppSubUrl}}/user/login</a></p> | 							<p><a id="goto-user-login" href="{{AppSubUrl}}/user/login">{{.locale.Tr "loading"}}</a></p> | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
|   | |||||||
| @@ -2,10 +2,18 @@ import $ from 'jquery'; | |||||||
| import {hideElem, showElem} from '../utils/dom.js'; | import {hideElem, showElem} from '../utils/dom.js'; | ||||||
|  |  | ||||||
| export function initInstall() { | export function initInstall() { | ||||||
|   if ($('.page-content.install').length === 0) { |   const $page = $('.page-content.install'); | ||||||
|  |   if ($page.length === 0) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |   if ($page.is('.post-install')) { | ||||||
|  |     initPostInstall(); | ||||||
|  |   } else { | ||||||
|  |     initPreInstall(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function initPreInstall() { | ||||||
|   const defaultDbUser = 'gitea'; |   const defaultDbUser = 'gitea'; | ||||||
|   const defaultDbName = 'gitea'; |   const defaultDbName = 'gitea'; | ||||||
|  |  | ||||||
| @@ -40,6 +48,18 @@ export function initInstall() { | |||||||
|     } // else: for SQLite3, the default path is always prepared by backend code (setting) |     } // else: for SQLite3, the default path is always prepared by backend code (setting) | ||||||
|   }).trigger('change'); |   }).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. |   // TODO: better handling of exclusive relations. | ||||||
|   $('#offline-mode input').on('change', function () { |   $('#offline-mode input').on('change', function () { | ||||||
|     if ($(this).is(':checked')) { |     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); | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user