Skip to content

Commit

Permalink
Added deployment-password flag, generate salts if invalid, added sect…
Browse files Browse the repository at this point in the history
…ion for automatic deployment to docs
  • Loading branch information
Forceu committed May 22, 2024
1 parent 486c1d5 commit 5a31cd5
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 80 deletions.
38 changes: 24 additions & 14 deletions cmd/gokapi/Main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func main() {
fmt.Println("Gokapi v" + versionGokapi + " starting")
setup.RunIfFirstStart()
configuration.Load()
setDeploymentPassword(passedFlags)
reconfigureServer(passedFlags)
encryption.Init(*configuration.Get())
authentication.Init(configuration.Get().Authentication)
Expand All @@ -75,21 +76,22 @@ func shutdown() {

// Checks for command line arguments that have to be parsed before loading the configuration
func showVersion(passedFlags flagparser.MainFlags) {
if passedFlags.ShowVersion {
fmt.Println("Gokapi v" + versionGokapi)
fmt.Println()
fmt.Println("Builder: " + environment.Builder)
fmt.Println("Build Date: " + environment.BuildTime)
fmt.Println("Is Docker Version: " + environment.IsDocker)
info, ok := debug.ReadBuildInfo()
if ok {
fmt.Println("Go Version: " + info.GoVersion)
} else {
fmt.Println("Go Version: unknown")
}
parseBuildSettings(info.Settings)
osExit(0)
if !passedFlags.ShowVersion {
return
}
fmt.Println("Gokapi v" + versionGokapi)
fmt.Println()
fmt.Println("Builder: " + environment.Builder)
fmt.Println("Build Date: " + environment.BuildTime)
fmt.Println("Is Docker Version: " + environment.IsDocker)
info, ok := debug.ReadBuildInfo()
if ok {
fmt.Println("Go Version: " + info.GoVersion)
} else {
fmt.Println("Go Version: unknown")
}
parseBuildSettings(info.Settings)
osExit(0)
}

func parseBuildSettings(infos []debug.BuildSetting) {
Expand Down Expand Up @@ -173,6 +175,14 @@ func handleServiceInstall(passedFlags flagparser.MainFlags) {
}
}

func setDeploymentPassword(passedFlags flagparser.MainFlags) {
if passedFlags.DeploymentPassword == "" {
return
}
logging.AddString("Password has been changed for deployment")
configuration.SetDeploymentPassword(passedFlags.DeploymentPassword)
}

var osExit = os.Exit

// ASCII art logo
Expand Down
91 changes: 73 additions & 18 deletions docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ Advanced usage
Environment variables
********************************

Environment variables can be passed to Gokapi - that way you can set it up without any interaction and pass cloud storage credentials without saving them to the filesystem.
Several environment variables can be passed to Gokapi. They can be used to modify settings that are not present during setup or to pass cloud storage credentials without saving them to the filesystem.


.. _passingenv:

Passing environment variables to Gokapi
===============================================
=========================================


Docker
------

Pass the variable with the ``-e`` argument. Example for setting the username to *admin* and the password to *123456*:
Pass the variable with the ``-e`` argument. Example for setting the port in use to *12345* and the database filename to *database.sqlite*:
::

docker run -it -e GOKAPI_USERNAME=admin -e GOKAPI_PASSWORD=123456 f0rc3/gokapi:latest
docker run -it -e GOKAPI_PORT=12345 -e GOKAPI_DB_NAME=database.sqlite f0rc3/gokapi:latest


Bare Metal
Expand Down Expand Up @@ -90,21 +90,21 @@ Available environment variables
All values that are described in :ref:`cloudstorage` can be passed as environment variables as well. No values are persistent, therefore need to be set on every start.
All values that are described in :ref:`cloudstorage` can be passed as environment variables as well. No values are persistent; therefore, they need to be set on every start.

+-----------------------+-------------------------+
| Name | Action |
+=======================+=========================+
| GOKAPI_AWS_BUCKET | Sets the bucket name |
+-----------------------+-------------------------+
| GOKAPI_AWS_REGION | Sets the region name |
+-----------------------+-------------------------+
| GOKAPI_AWS_KEY | Sets the API key |
+-----------------------+-------------------------+
| GOKAPI_AWS_KEY_SECRET | Sets the API key secret |
+-----------------------+-------------------------+
| GOKAPI_AWS_ENDPOINT | Sets the endpoint |
+-----------------------+-------------------------+
+-----------------------+-------------------------+-----------------------------+
| Name | Action | Example |
+=======================+=========================+=============================+
| GOKAPI_AWS_BUCKET | Sets the bucket name | gokapi |
+-----------------------+-------------------------+-----------------------------+
| GOKAPI_AWS_REGION | Sets the region name | eu-central-000 |
+-----------------------+-------------------------+-----------------------------+
| GOKAPI_AWS_KEY | Sets the API key | 123456789 |
+-----------------------+-------------------------+-----------------------------+
| GOKAPI_AWS_KEY_SECRET | Sets the API key secret | abcdefg123 |
+-----------------------+-------------------------+-----------------------------+
| GOKAPI_AWS_ENDPOINT | Sets the endpoint | eu-central-000.provider.com |
+-----------------------+-------------------------+-----------------------------+


.. _api:
Expand Down Expand Up @@ -142,6 +142,61 @@ Example: Deleting a file




********************************
Automatic Deployment
********************************

It is possible to deploy Gokapi without having to run the setup. You will need to complete the setup on a temporary instance first. This is to create the configuration files, which can then be used for deployment.


Configuration Files
============================


The configuration consists of up to two files in the configuration directory (default: ``config``). All files can be read-only, however ``config.json`` might need write access in some situations.

cloudconfig.yml
------------------------

Stores the access data for cloud storage. This can be reused without modification, however all fields can also be set with environment variables. The file does not exist if no cloud storage is used and can always be read-only.


config.json
------------------------

Contains the server configuration. If you want to deploy Gokapi in multiple instances for redundancy (e.g. all instances share the same data), then the configuration file can be reused without modification. Otherwise you need to modify it before deploying (see below). Can be read-only, but might need write access when upgrading Gokapi to a newer version. Needs write access when re-running setup or changing the admin password.


Modifying config.json to deploy without setup
====================================================

If you want to deploy Gokapi to multiple instances that contain different data, you have to modify the config.json. Open it and change the following fields:

+-----------+------------------------------------------------------------+----------------------+
| Field | Operation | Example |
+===========+============================================================+======================+
| SaltAdmin | Change to empty value | "SaltAdmin": "", |
+-----------+------------------------------------------------------------+----------------------+
| SaltFiles | Change to empty value | "SaltFiles": "", |
+-----------+------------------------------------------------------------+----------------------+
| Password | Change to empty value | "Password": "", |
+-----------+------------------------------------------------------------+----------------------+
| Username | Change to the username of your preference, | "Username": "admin", |
| | | |
| | if you are using internal username/password authentication | |
+-----------+------------------------------------------------------------+----------------------+

Setting an admin password
====================================================

If you are using internal username/password authentication, run the binary with the parameter ``--deployment-password [YOUR_PASSWORD]``. This sets the password and also generates a new salt for the password. This has to be done before Gokapi is run for the first time on the new instance. Alternatively you can do this on the orchestrating machine and then copy the configuration file to the new instance.

If you are using a Docker image, this has to be done by starting a container with the entrypoint ``/app/run.sh``, for example: ::

docker run --rm -v gokapi-data:/app/data -v gokapi-config:/app/config f0rc3/gokapi:latest /app/run.sh --deployment-password newPassword


********************************
Customising
********************************
Expand Down
71 changes: 59 additions & 12 deletions internal/configuration/Configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import (
"strings"
)

// Min length of admin password in characters
const minLengthPassword = 6
// MinLengthPassword is the required length of admin password in characters
const MinLengthPassword = 8

// Environment is an object containing the environment variables
var Environment environment.Environment
Expand All @@ -41,17 +41,42 @@ func Exists() bool {
return helper.FileExists(configPath)
}

// loadFromFile parses the given file and adds salts, if they are invalid
func loadFromFile(path string) (models.Configuration, error) {
file, err := os.Open(path)
if err != nil {
return models.Configuration{}, err
}
decoder := json.NewDecoder(file)
settings := models.Configuration{}
err = decoder.Decode(&settings)
if err != nil {
return models.Configuration{}, err
}
err = file.Close()
if err != nil {
return models.Configuration{}, err
}
if len(settings.Authentication.SaltFiles) < 20 {
settings.Authentication.SaltFiles = helper.GenerateRandomString(30)
fmt.Println("Warning: Salt for file hash invalid, generating new salt")
}
if len(settings.Authentication.SaltAdmin) < 20 {
settings.Authentication.SaltAdmin = helper.GenerateRandomString(30)
if settings.Authentication.Method == 0 { // == authentication.Internal, but would create import cycle
fmt.Println("Warning: Salt for admin password invalid, generating new salt. You will need to reset the admin password.")
}
}
return settings, nil
}

// Load loads the configuration or creates the folder structure and a default configuration
func Load() {
Environment = environment.New()
// No check if file exists, as this was checked earlier
file, err := os.Open(Environment.ConfigPath)
settings, err := loadFromFile(Environment.ConfigPath)
helper.Check(err)
decoder := json.NewDecoder(file)
serverSettings = models.Configuration{}
err = decoder.Decode(&serverSettings)
helper.Check(err)
file.Close()
serverSettings = settings
database.Init(serverSettings.DataDir, Environment.DatabaseName)
if configupgrade.DoUpgrade(&serverSettings, &Environment) {
save()
Expand Down Expand Up @@ -83,7 +108,7 @@ func Get() *models.Configuration {
func save() {
file, err := os.OpenFile(Environment.ConfigPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
fmt.Println("Error reading configuration:", err)
fmt.Println("Error writing configuration:", err)
os.Exit(1)
}
defer file.Close()
Expand Down Expand Up @@ -123,11 +148,33 @@ func LoadFromSetup(config models.Configuration, cloudConfig *cloudconfig.CloudCo
Load()
}

// SetDeploymentPassword sets a new password. This should only be used for non-interactive deployment, but is not enforced
func SetDeploymentPassword(newPassword string) {
if len(newPassword) < MinLengthPassword {
fmt.Printf("Password needs to be at least %d characters long\n", MinLengthPassword)
os.Exit(1)
}
serverSettings.Authentication.SaltAdmin = helper.GenerateRandomString(30)
serverSettings.Authentication.Password = hashUserPassword(newPassword)
database.DeleteAllSessions()
save()
fmt.Println("New password has been set successfully")
os.Exit(0)
}

// HashPassword hashes a string with SHA1 the file salt or admin user salt
func HashPassword(password string, useFileSalt bool) string {
if useFileSalt {
return HashPasswordCustomSalt(password, serverSettings.Authentication.SaltFiles)
return hashFilePassword(password)
}
return hashUserPassword(password)
}

func hashFilePassword(password string) string {
return HashPasswordCustomSalt(password, serverSettings.Authentication.SaltFiles)
}

func hashUserPassword(password string) string {
return HashPasswordCustomSalt(password, serverSettings.Authentication.SaltAdmin)
}

Expand All @@ -139,8 +186,8 @@ func HashPasswordCustomSalt(password, salt string) string {
if salt == "" {
panic(errors.New("no salt provided"))
}
bytes := []byte(password + salt)
pwBytes := []byte(password + salt)
hash := sha1.New()
hash.Write(bytes)
hash.Write(pwBytes)
return hex.EncodeToString(hash.Sum(nil))
}
23 changes: 13 additions & 10 deletions internal/configuration/setup/Setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func startSetupWebserver() {
log.Fatalf("Setup Webserver: %v", err)
}
err = srv.Serve(listener)
if err != nil && err != http.ErrServerClosed {
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Setup Webserver: %v", err)
}
}
Expand All @@ -145,10 +145,11 @@ func isErrorAddressAlreadyInUse(err error) bool {
if !errors.As(eOsSyscall, &errErrno) {
return false
}
if errErrno == syscall.EADDRINUSE {
if errors.Is(errErrno, syscall.EADDRINUSE) {
return true
}
const WSAEADDRINUSE = 10048
//noinspection GoBoolExpressions
if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE {
return true
}
Expand Down Expand Up @@ -530,9 +531,8 @@ func parseEncryptionAndDelete(result *models.Configuration, formObjects *[]jsonF
if encLevel == encryption.LocalEncryptionInput || encLevel == encryption.FullEncryptionInput {
result.Encryption.Salt = helper.GenerateRandomString(30)
result.Encryption.ChecksumSalt = helper.GenerateRandomString(30)
const minLength = 8
if len(masterPw) < minLength {
return errors.New("password is less than " + strconv.Itoa(minLength) + " characters long")
if len(masterPw) < configuration.MinLengthPassword {
return errors.New("password is less than " + strconv.Itoa(configuration.MinLengthPassword) + " characters long")
}
result.Encryption.Checksum = encryption.PasswordChecksum(masterPw, result.Encryption.ChecksumSalt)
}
Expand Down Expand Up @@ -635,7 +635,7 @@ func handleShowSetup(w http.ResponseWriter, r *http.Request) {
}

func handleShowMaintenance(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Server is in maintenance mode, please try again in a few minutes."))
_, _ = w.Write([]byte("Server is in maintenance mode, please try again in a few minutes."))
}

// Handling of /setupResult
Expand All @@ -653,16 +653,19 @@ func handleResult(w http.ResponseWriter, r *http.Request) {
}
configuration.LoadFromSetup(newConfig, cloudSettings, isInitialSetup)
w.WriteHeader(200)
w.Write([]byte("{ \"result\": \"OK\"}"))
_, _ = w.Write([]byte("{ \"result\": \"OK\"}"))
go func() {
time.Sleep(1500 * time.Millisecond)
srv.Shutdown(context.Background())
err = srv.Shutdown(context.Background())
if err != nil {
fmt.Println(err)
}
}()
}

func outputError(w http.ResponseWriter, err error) {
w.WriteHeader(500)
w.Write([]byte("{ \"result\": \"Error\", \"error\": \"" + err.Error() + "\"}"))
_, _ = w.Write([]byte("{ \"result\": \"Error\", \"error\": \"" + err.Error() + "\"}"))
}

// Adds a / character to the end of a URL if it does not exist
Expand Down Expand Up @@ -696,7 +699,7 @@ func handleTestAws(w http.ResponseWriter, r *http.Request) {
var t testAwsRequest
err := decoder.Decode(&t)
if err != nil {
w.Write([]byte("Error: " + err.Error()))
_, _ = w.Write([]byte("Error: " + err.Error()))
return
}
var awsConfig models.AwsConfig
Expand Down
Loading

0 comments on commit 5a31cd5

Please sign in to comment.