Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 73 additions & 22 deletions internal/configuration/setup/Setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package setup
import (
"bufio"
"context"
"crypto/subtle"
"embed"
"encoding/json"
"errors"
Expand Down Expand Up @@ -33,6 +34,7 @@ import (
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/storage/filesystem/s3filesystem/aws"
"github.com/forceu/gokapi/internal/webserver/ratelimiter"
)

// webserverDir is the embedded version of the "static" folder
Expand All @@ -51,6 +53,7 @@ var srv http.Server
var isInitialSetup = true
var credentialUsername string
var credentialPassword string
var credentialAWSTest string

// debugDisableAuth can be set to true for testing purposes. It will disable the
// password requirement for accessing the setup page
Expand All @@ -60,6 +63,13 @@ const debugDisableAuth = false
func RunIfFirstStart() {
if !configuration.Exists() {
isInitialSetup = true
credentialAWSTest = helper.GenerateRandomString(10)
fmt.Println()
fmt.Println("###################################################################")
fmt.Println("Use the following password for testing the AWS configuration:")
fmt.Println("Password: " + credentialAWSTest)
fmt.Println("###################################################################")
fmt.Println()
startSetupWebserver()
}
}
Expand Down Expand Up @@ -90,15 +100,15 @@ func basicAuth(next http.HandlerFunc) http.HandlerFunc {

enteredUser, enteredPw, ok := r.BasicAuth()
if ok {
ratelimiter.WaitOnSetupLogin()
usernameMatch := helper.IsEqualStringConstantTime(strings.ToLower(enteredUser), strings.ToLower(credentialUsername))
passwordMatch := helper.IsEqualStringConstantTime(enteredPw, credentialPassword)
if usernameMatch && passwordMatch {
next.ServeHTTP(w, r)
return
}
}
time.Sleep(time.Second)
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
w.Header().Set("WWW-Authenticate", `Basic realm="Please enter the credentials shown in the console output", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
}
Expand Down Expand Up @@ -826,24 +836,46 @@ func verifyPortNumber(port int) int {
}

type testAwsRequest struct {
Bucket string `json:"bucket"`
Region string `json:"region"`
ApiKey string `json:"apikey"`
ApiSecret string `json:"apisecret"`
Endpoint string `json:"endpoint"`
GokapiUrl string `json:"exturl"`
EnvProvided bool `json:"isEnvProvided"`
Bucket string `json:"bucket"`
Region string `json:"region"`
ApiKey string `json:"apikey"`
ApiSecret string `json:"apisecret"`
Endpoint string `json:"endpoint"`
GokapiUrl string `json:"exturl"`
SetupPassword string `json:"setupPassword"`
EnvProvided bool `json:"isEnvProvided"`
}

type awsTestResponse struct {
Code int `json:"code"`
Result string `json:"result"`
}

// Handling of /testaws
func handleTestAws(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
decoder := json.NewDecoder(r.Body)
var t testAwsRequest
err := decoder.Decode(&t)
if err != nil {
_, _ = w.Write([]byte("Error: " + err.Error()))
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(awsTestResponse{
Code: http.StatusBadRequest,
Result: "Invalid JSON provided",
})
return
}
if isInitialSetup {
ratelimiter.WaitOnSetupLogin()
if t.SetupPassword == "" || subtle.ConstantTimeCompare([]byte(t.SetupPassword), []byte(credentialAWSTest)) != 1 {
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(awsTestResponse{
Code: http.StatusUnauthorized,
Result: "Invalid AWS test password provided",
})
return
}
}
var awsConfig models.AwsConfig

if !t.EnvProvided {
Expand All @@ -870,7 +902,11 @@ func handleTestAws(w http.ResponseWriter, r *http.Request) {
return
}
if !ok {
_, _ = w.Write([]byte("Error: Invalid or incomplete credentials provided"))
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(awsTestResponse{
Code: http.StatusBadRequest,
Result: "Invalid or incomplete credentials provided",
})
return
}
aws.Init(awsConfig)
Expand All @@ -881,10 +917,18 @@ func handleTestAws(w http.ResponseWriter, r *http.Request) {
return
}
if !ok {
_, _ = w.Write([]byte("Test OK. WARNING: CORS settings do not allow encrypted downloads."))
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(awsTestResponse{
Code: http.StatusOK,
Result: "Test OK. WARNING: CORS settings do not allow encrypted downloads.",
})
return
}
_, _ = w.Write([]byte("All tests OK."))
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(awsTestResponse{
Code: http.StatusOK,
Result: "All tests OK.",
})
}

const (
Expand All @@ -902,28 +946,35 @@ func handleAwsError(w http.ResponseWriter, err error, operation int) {
case awsOperationCors:
prefix = "Could not get CORS settings. "
}
var response awsTestResponse

if isAwsErr {
response.Code = http.StatusBadRequest
code := awsErr.Code()
switch code {
case s3.ErrCodeNoSuchBucket:
_, _ = w.Write([]byte("Invalid bucket or regions provided, bucket does not exist."))
response.Result = "Invalid bucket or regions provided, bucket does not exist."
case "Forbidden":
_, _ = w.Write([]byte("Unable to log in, invalid credentials."))
response.Result = "Invalid credentials provided, check bucket and region."
case "RequestError":
_, _ = w.Write([]byte("Unable to connect to server, check endpoint."))
response.Result = "Unable to connect to server, check endpoint."
case "SerializationError":
_, _ = w.Write([]byte("Invalid response received by server, check endpoint."))
response.Result = "Invalid response received by server, check endpoint."
case "NotFound":
if operation == awsOperationCors {
_, _ = w.Write([]byte("Login OK, but could not check bucket CORS settings."))

response.Code = http.StatusOK
response.Result = "Login OK, but could not check bucket CORS settings."
} else {
_, _ = w.Write([]byte("The requested resource could not be found, check endpoint"))
response.Result = "The requested resource could not be found, check endpoint."
}
default:
_, _ = w.Write([]byte(prefix + "Error " + awsErr.Code() + ": " + err.Error()))
response.Result = prefix + "Error " + awsErr.Code() + ": " + err.Error()
}
} else {
_, _ = w.Write([]byte(prefix + "Error: " + err.Error()))
response.Code = http.StatusInternalServerError
response.Result = prefix + "Error: " + err.Error()
}

w.WriteHeader(response.Code)
_ = json.NewEncoder(w).Encode(response)
}
99 changes: 67 additions & 32 deletions internal/configuration/setup/templates/setup.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -495,41 +495,76 @@
</div>

<script>

function TestAWS(button, isManual) {
button.disabled=true;
button.innerHTML="Connecting...";

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE) {
if (this.status == 200) {
button.innerHTML="Test configuration";
button.disabled=false;
alert(xhr.responseText);
} else {
alert("Unable to connect to Gokapi server");
}
}
};

xhr.open("POST", "./testaws", true);
xhr.setRequestHeader('Content-Type', 'application/json');
if (isManual) {
xhr.send(JSON.stringify({
bucket: document.getElementById("s3_bucket").value,
region: document.getElementById("s3_region").value,
apikey: document.getElementById("s3_api").value,
apisecret: document.getElementById("s3_secret").value,
endpoint: document.getElementById("s3_endpoint").value,
isEnvProvided: false
}));
} else {
xhr.send(JSON.stringify({
isEnvProvided: true
}));
}

button.disabled = true;
{{ if .IsInitialSetup }}
if (sessionStorage.getItem("awstestpw") == null || sessionStorage.getItem("awstestpw") == "") {
let pw = window.prompt("To test the AWS connection, a password is required.\nPlease enter the password that is displayed in the console output.", "");
if (pw == "" || pw == null) {
button.disabled = false;
return;
}
sessionStorage.setItem("awstestpw", pw);

}
{{ end }}

button.innerHTML = "Connecting...";

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE) {
button.innerHTML = "Test configuration";
button.disabled = false;

let response = {};
try {
response = JSON.parse(xhr.responseText);
} catch (e) {
response = {
result: "Error: Could not parse server response."
};
}


switch (this.status) {
case 200:
alert(response.result || "Test OK.");
break;
case 401:
sessionStorage.setItem("awstestpw", "");
alert("Incorrect AWS Test Password.\nPlease enter the password that is displayed in the console output.");
break;
default:
const errorMessage = response.result || "An unexpected error occurred";
alert(`Error: ${errorMessage}`);
}
}
};

xhr.open("POST", "./testaws", true);
xhr.setRequestHeader('Content-Type', 'application/json');
if (isManual) {
xhr.send(JSON.stringify({
bucket: document.getElementById("s3_bucket").value,
region: document.getElementById("s3_region").value,
apikey: document.getElementById("s3_api").value,
apisecret: document.getElementById("s3_secret").value,
endpoint: document.getElementById("s3_endpoint").value,
setupPassword: sessionStorage.getItem("awstestpw"),
isEnvProvided: false
}));
} else {
xhr.send(JSON.stringify({
setupPassword: sessionStorage.getItem("awstestpw"),
isEnvProvided: true
}));
}

}

</script>


Expand Down
7 changes: 7 additions & 0 deletions internal/webserver/ratelimiter/RateLimiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var failedLoginLimiter = newLimiter()
var failedIdLimiter = newLimiter()
var failedDownloadPasswordLimiter = newLimiter()
var failedApiKeyLimiter = newLimiter()
var failedSetupLogin = newLimiter()

// isUnitTest must be false and is only set to true for running test units
// If true, rate limiting is disabled
Expand Down Expand Up @@ -51,6 +52,12 @@ func WaitOnLogin(ip string) {
_ = failedLoginLimiter.Get(ip, 1, 9).WaitN(context.Background(), 3)
}

// WaitOnSetupLogin blocks the current goroutine until the rate limiter allows a request
// Three attempts without limiting, thereafter one attempt every 1 second
func WaitOnSetupLogin() {
_ = failedSetupLogin.Get("setup", 1, 15).WaitN(context.Background(), 1)
}

// WaitOnApiAuthentication blocks the current goroutine until the rate limiter allows a request
// 200 attempts without limiting, thereafter one attempt every second
func WaitOnApiAuthentication(ip string) {
Expand Down
Loading