Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix various bugs for "install" page #23194

Merged
merged 7 commits into from Mar 4, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 22 additions & 7 deletions modules/auth/password/hash/setting.go
Expand Up @@ -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
}
Expand All @@ -52,10 +51,26 @@ func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHas
algorithmName = alias
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.
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
}

return algorithmName, 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
}
2 changes: 1 addition & 1 deletion options/locale/locale_en-US.ini
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
48 changes: 33 additions & 15 deletions routers/install/install.go
Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
13 changes: 10 additions & 3 deletions routers/install/routes.go
Expand Up @@ -6,6 +6,7 @@ package install
import (
goctx "context"
"fmt"
"html"
"net/http"
"path"

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -107,14 +108,20 @@ 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))
return r
}

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+"/"))
}
2 changes: 1 addition & 1 deletion templates/base/head.tmpl
Expand Up @@ -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}}">
Expand Down
4 changes: 2 additions & 2 deletions templates/post-install.tmpl
@@ -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">
Expand All @@ -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>
Expand Down
39 changes: 38 additions & 1 deletion web_src/js/features/install.js
Expand Up @@ -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';

Expand Down Expand Up @@ -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') || configAppUrl.includes('://127.0.0.1')) {
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
$appUrl.val(window.location.href);
}

const $domain = $('#domain');
const configDomain = $domain.val().trim();
if (configDomain === 'localhost' || configDomain === '127.0.0.1') {
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
$domain.val(window.location.hostname);
}

// TODO: better handling of exclusive relations.
$('#offline-mode input').on('change', function () {
if ($(this).is(':checked')) {
Expand Down Expand Up @@ -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);
}