Skip to content

Commit 1edb57e

Browse files
GiteaBotwxiaoguangwolfogrelunnytechknowlogick
authored
Fix various bugs for "install" page (#23194) (#23286)
Backport #23194 ## TLDR * Fix the broken page / broken image problem when click "Install" * Fix the Password Hash Algorithm display problem for #22942 * Close #20089 * Close #23183 * Close #23184 ## Details ### The broken page / broken image problem when clicking on "Install" (Redirect failed after install - #23184) Before: when clicking on "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 fill 'Gitea Base URL' with 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: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Jason Song <i@wolfogre.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
1 parent a2a9b0f commit 1edb57e

File tree

7 files changed

+107
-30
lines changed

7 files changed

+107
-30
lines changed

modules/auth/password/hash/setting.go

+22-7
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ var RecommendedHashAlgorithms = []string{
4141
"pbkdf2_hi",
4242
}
4343

44-
// SetDefaultPasswordHashAlgorithm will take a provided algorithmName and dealias it to
45-
// a complete algorithm specification.
46-
func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) {
44+
// hashAlgorithmToSpec converts an algorithm name or a specification to a full algorithm specification
45+
func hashAlgorithmToSpec(algorithmName string) string {
4746
if algorithmName == "" {
4847
algorithmName = DefaultHashAlgorithmName
4948
}
@@ -52,10 +51,26 @@ func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHas
5251
algorithmName = alias
5352
alias, has = aliasAlgorithmNames[algorithmName]
5453
}
54+
return algorithmName
55+
}
5556

56-
// algorithmName should now be a full algorithm specification
57-
// e.g. pbkdf2$50000$50 rather than pbdkf2
58-
DefaultHashAlgorithm = Parse(algorithmName)
57+
// SetDefaultPasswordHashAlgorithm will take a provided algorithmName and de-alias it to
58+
// a complete algorithm specification.
59+
func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) {
60+
algoSpec := hashAlgorithmToSpec(algorithmName)
61+
// now we get a full specification, e.g. pbkdf2$50000$50 rather than pbdkf2
62+
DefaultHashAlgorithm = Parse(algoSpec)
63+
return algoSpec, DefaultHashAlgorithm
64+
}
5965

60-
return algorithmName, DefaultHashAlgorithm
66+
// ConfigHashAlgorithm will try to find a "recommended algorithm name" defined by RecommendedHashAlgorithms for config
67+
// This function is not fast and is only used for the installation page
68+
func ConfigHashAlgorithm(algorithm string) string {
69+
algorithm = hashAlgorithmToSpec(algorithm)
70+
for _, recommAlgo := range RecommendedHashAlgorithms {
71+
if algorithm == hashAlgorithmToSpec(recommAlgo) {
72+
return recommAlgo
73+
}
74+
}
75+
return algorithm
6176
}

options/locale/locale_en-US.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,6 @@ internal_token_failed = Failed to generate internal token: %v
237237
secret_key_failed = Failed to generate secret key: %v
238238
save_config_failed = Failed to save configuration: %v
239239
invalid_admin_setting = Administrator account setting is invalid: %v
240-
install_success = Welcome! Thank you for choosing Gitea. Have fun and take care!
241240
invalid_log_root_path = The log path is invalid: %v
242241
default_keep_email_private = Hide Email Addresses by Default
243242
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
248247
no_reply_address = Hidden Email Domain
249248
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'.
250249
password_algorithm = Password Hash Algorithm
250+
invalid_password_algorithm = Invalid password hash algorithm
251251
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.
252252
enable_update_checker = Enable Update Checker
253253
enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io.

routers/install/install.go

+33-15
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,6 @@ func Init(ctx goctx.Context) func(next http.Handler) http.Handler {
5959
dbTypeNames := getSupportedDbTypeNames()
6060
return func(next http.Handler) http.Handler {
6161
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
62-
if setting.InstallLock {
63-
resp.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
64-
_ = rnd.HTML(resp, http.StatusOK, string(tplPostInstall), nil)
65-
return
66-
}
6762
locale := middleware.Locale(resp, req)
6863
startTime := time.Now()
6964
ctx := context.Context{
@@ -93,6 +88,11 @@ func Init(ctx goctx.Context) func(next http.Handler) http.Handler {
9388

9489
// Install render installation page
9590
func Install(ctx *context.Context) {
91+
if setting.InstallLock {
92+
InstallDone(ctx)
93+
return
94+
}
95+
9696
form := forms.InstallForm{}
9797

9898
// Database settings
@@ -162,7 +162,7 @@ func Install(ctx *context.Context) {
162162
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
163163
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
164164
form.NoReplyAddress = setting.Service.NoReplyAddress
165-
form.PasswordAlgorithm = setting.PasswordHashAlgo
165+
form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo)
166166

167167
middleware.AssignForm(form, ctx.Data)
168168
ctx.HTML(http.StatusOK, tplInstall)
@@ -234,6 +234,11 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool {
234234

235235
// SubmitInstall response for submit install items
236236
func SubmitInstall(ctx *context.Context) {
237+
if setting.InstallLock {
238+
InstallDone(ctx)
239+
return
240+
}
241+
237242
var err error
238243

239244
form := *web.GetForm(ctx).(*forms.InstallForm)
@@ -277,7 +282,6 @@ func SubmitInstall(ctx *context.Context) {
277282
setting.Database.Charset = form.Charset
278283
setting.Database.Path = form.DbPath
279284
setting.Database.LogSQL = !setting.IsProd
280-
setting.PasswordHashAlgo = form.PasswordAlgorithm
281285

282286
if !checkDatabase(ctx, &form) {
283287
return
@@ -499,6 +503,12 @@ func SubmitInstall(ctx *context.Context) {
499503
}
500504

501505
if len(form.PasswordAlgorithm) > 0 {
506+
var algorithm *hash.PasswordHashAlgorithm
507+
setting.PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(form.PasswordAlgorithm)
508+
if algorithm == nil {
509+
ctx.RenderWithErr(ctx.Tr("install.invalid_password_algorithm"), tplInstall, &form)
510+
return
511+
}
502512
cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm)
503513
}
504514

@@ -571,18 +581,26 @@ func SubmitInstall(ctx *context.Context) {
571581
}
572582

573583
log.Info("First-time run install finished!")
584+
InstallDone(ctx)
574585

575-
ctx.Flash.Success(ctx.Tr("install.install_success"))
576-
577-
ctx.RespHeader().Add("Refresh", "1; url="+setting.AppURL+"user/login")
578-
ctx.HTML(http.StatusOK, tplPostInstall)
579-
580-
// Now get the http.Server from this request and shut it down
581-
// NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
582-
srv := ctx.Value(http.ServerContextKey).(*http.Server)
583586
go func() {
587+
// Sleep for a while to make sure the user's browser has loaded the post-install page and its assets (images, css, js)
588+
// 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 ....
589+
time.Sleep(3 * time.Second)
590+
591+
// Now get the http.Server from this request and shut it down
592+
// NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
593+
srv := ctx.Value(http.ServerContextKey).(*http.Server)
584594
if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil {
585595
log.Error("Unable to shutdown the install server! Error: %v", err)
586596
}
597+
598+
// After the HTTP server for "install" shuts down, the `runWeb()` will continue to run the "normal" server
587599
}()
588600
}
601+
602+
// InstallDone shows the "post-install" page, makes it easier to develop the page.
603+
// The name is not called as "PostInstall" to avoid misinterpretation as a handler for "POST /install"
604+
func InstallDone(ctx *context.Context) { //nolint
605+
ctx.HTML(http.StatusOK, tplPostInstall)
606+
}

routers/install/routes.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package install
66
import (
77
goctx "context"
88
"fmt"
9+
"html"
910
"net/http"
1011
"path"
1112

@@ -37,7 +38,7 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
3738
// Why we need this? The first recover will try to render a beautiful
3839
// error page for user, but the process can still panic again, then
3940
// we have to just recover twice and send a simple error page that
40-
// should not panic any more.
41+
// should not panic anymore.
4142
defer func() {
4243
if err := recover(); err != nil {
4344
combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, log.Stack(2))
@@ -107,14 +108,20 @@ func Routes(ctx goctx.Context) *web.Route {
107108

108109
r.Use(installRecovery(ctx))
109110
r.Use(Init(ctx))
110-
r.Get("/", Install)
111+
r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL
111112
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
113+
r.Get("/post-install", InstallDone)
112114
r.Get("/api/healthz", healthcheck.Check)
113115

114116
r.NotFound(web.Wrap(installNotFound))
115117
return r
116118
}
117119

118120
func installNotFound(w http.ResponseWriter, req *http.Request) {
119-
http.Redirect(w, req, setting.AppURL, http.StatusFound)
121+
w.Header().Add("Content-Type", "text/html; charset=utf-8")
122+
w.Header().Add("Refresh", fmt.Sprintf("1; url=%s", setting.AppSubURL+"/"))
123+
// do not use 30x status, because the "post-install" page needs to use 404/200 to detect if Gitea has been installed.
124+
// the fetch API could follow 30x requests to the page with 200 status.
125+
w.WriteHeader(http.StatusNotFound)
126+
_, _ = fmt.Fprintf(w, `Not Found. <a href="%s">Go to default page</a>.`, html.EscapeString(setting.AppSubURL+"/"))
120127
}

templates/base/head.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
7-
<link rel="manifest" href="data:{{.ManifestData}}">
7+
{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
88
<meta name="theme-color" content="{{ThemeColorMetaTag}}">
99
<meta name="default-theme" content="{{DefaultTheme}}">
1010
<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">

templates/post-install.tmpl

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{{template "base/head" .}}
2-
<div role="main" aria-label="{{.Title}}" class="page-content install">
2+
<div role="main" aria-label="{{.Title}}" class="page-content install post-install">
33
<div class="ui container">
44
<div class="ui grid">
55
<div class="sixteen wide column content">
@@ -13,7 +13,7 @@
1313
</div>
1414
<div class="ui stackable middle very relaxed page grid">
1515
<div class="sixteen wide center aligned centered column">
16-
<p><a href="{{AppSubUrl}}/user/login">{{AppSubUrl}}/user/login</a></p>
16+
<p><a id="goto-user-login" href="{{AppSubUrl}}/user/login">{{.locale.Tr "loading"}}</a></p>
1717
</div>
1818
</div>
1919
</div>

web_src/js/features/install.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@ import $ from 'jquery';
22
import {hideElem, showElem} from '../utils/dom.js';
33

44
export function initInstall() {
5-
if ($('.page-content.install').length === 0) {
5+
const $page = $('.page-content.install');
6+
if ($page.length === 0) {
67
return;
78
}
9+
if ($page.is('.post-install')) {
10+
initPostInstall();
11+
} else {
12+
initPreInstall();
13+
}
14+
}
815

16+
function initPreInstall() {
917
const defaultDbUser = 'gitea';
1018
const defaultDbName = 'gitea';
1119

@@ -40,6 +48,18 @@ export function initInstall() {
4048
} // else: for SQLite3, the default path is always prepared by backend code (setting)
4149
}).trigger('change');
4250

51+
const $appUrl = $('#app_url');
52+
const configAppUrl = $appUrl.val();
53+
if (configAppUrl.includes('://localhost')) {
54+
$appUrl.val(window.location.href);
55+
}
56+
57+
const $domain = $('#domain');
58+
const configDomain = $domain.val().trim();
59+
if (configDomain === 'localhost') {
60+
$domain.val(window.location.hostname);
61+
}
62+
4363
// TODO: better handling of exclusive relations.
4464
$('#offline-mode input').on('change', function () {
4565
if ($(this).is(':checked')) {
@@ -83,3 +103,20 @@ export function initInstall() {
83103
}
84104
});
85105
}
106+
107+
function initPostInstall() {
108+
const el = document.getElementById('goto-user-login');
109+
if (!el) return;
110+
111+
const targetUrl = el.getAttribute('href');
112+
let tid = setInterval(async () => {
113+
try {
114+
const resp = await fetch(targetUrl);
115+
if (tid && resp.status === 200) {
116+
clearInterval(tid);
117+
tid = null;
118+
window.location.href = targetUrl;
119+
}
120+
} catch {}
121+
}, 1000);
122+
}

0 commit comments

Comments
 (0)