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", | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| } | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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. <a href="%s">Go to default page</a>.`, html.EscapeString(setting.AppSubURL+"/")) | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| 	<meta charset="utf-8"> | ||||
| 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| 	<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="default-theme" content="{{DefaultTheme}}"> | ||||
| 	<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}"> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| {{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 grid"> | ||||
| 			<div class="sixteen wide column content"> | ||||
| @@ -13,7 +13,7 @@ | ||||
| 					</div> | ||||
| 					<div class="ui stackable middle very relaxed page grid"> | ||||
| 						<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> | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user