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>

![image](https://user-images.githubusercontent.com/2114189/221917143-e1e54798-1698-4fee-a18d-00c48081fc39.png)

</details>

After: the UI shows `pbkdf2`

<details>

![image](https://user-images.githubusercontent.com/2114189/221916999-97a15be8-2ebb-4a01-bf93-dac18e354fcc.png)

</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:
wxiaoguang 2023-03-04 10:12:02 +08:00 committed by GitHub
parent 5c4075e16d
commit b2359f3df6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 109 additions and 32 deletions

View file

@ -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
} }

View file

@ -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.

View file

@ -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")) 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)
ctx.RespHeader().Add("Refresh", "1; url="+setting.AppURL+"user/login") // 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 ....
ctx.HTML(http.StatusOK, tplPostInstall) time.Sleep(3 * time.Second)
// Now get the http.Server from this request and shut it down // 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 // NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
srv := ctx.Value(http.ServerContextKey).(*http.Server) srv := ctx.Value(http.ServerContextKey).(*http.Server)
go func() {
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)
}

View file

@ -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+"/"))
} }

View file

@ -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}}">

View file

@ -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>

View file

@ -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);
}