Skip to content
Draft
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
4 changes: 2 additions & 2 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Keycloak
^^^^^^^^^^^^

.. note::
This guide has been written for version 24.0.3
This guide has been written for version 26.5.5

.. warning::
In a previous version of this guide, the client mapping was for the predefined mapper "Group memberships", which in some cases always returned the value "admin". Please make sure that you are using a custom mapper, as described in :ref:`oidcconfig_keycloak_opt`
Expand Down Expand Up @@ -233,7 +233,7 @@ Gokapi Configuration
+---------------------------+-----------------------------------------------------------------------+--------------------------------------------+
| Client Secret | Client secret provided | AhXeV7_EXAMPLE_KEY |
+---------------------------+-----------------------------------------------------------------------+--------------------------------------------+
| Recheck identity | If open ``Consent required`` is disabled, use a low interval | 12 hours |
| Recheck identity | If ``Consent required`` is disabled, use a low interval | 12 hours |
+---------------------------+-----------------------------------------------------------------------+--------------------------------------------+
| Admin email address | The email address for the super-admin | gokapi@example.com |
+---------------------------+-----------------------------------------------------------------------+--------------------------------------------+
Expand Down
156 changes: 65 additions & 91 deletions internal/webserver/Webserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/forceu/gokapi/internal/webserver/authentication/oauth"
"github.com/forceu/gokapi/internal/webserver/authentication/sessionmanager"
"github.com/forceu/gokapi/internal/webserver/authentication/tokengeneration"
"github.com/forceu/gokapi/internal/webserver/errorHandling"
"github.com/forceu/gokapi/internal/webserver/favicon"
"github.com/forceu/gokapi/internal/webserver/fileupload"
"github.com/forceu/gokapi/internal/webserver/ratelimiter"
Expand Down Expand Up @@ -111,9 +112,6 @@ func Start() {
mux.HandleFunc("/downloadPresigned", requireLogin(downloadPresigned, false, false))
mux.HandleFunc("/e2eSetup", requireLogin(showE2ESetup, true, false))
mux.HandleFunc("/error", showError)
mux.HandleFunc("/error-auth", showErrorAuth)
mux.HandleFunc("/error-header", showErrorHeader)
mux.HandleFunc("/error-oauth", showErrorIntOAuth)
mux.HandleFunc("/filerequests", requireLogin(showUploadRequest, true, false))
mux.HandleFunc("/forgotpw", forgotPassword)
mux.HandleFunc("/h/", showHotlink)
Expand Down Expand Up @@ -226,13 +224,13 @@ func initTemplates(templateFolderEmbedded embed.FS) {
}

// Sends a redirect HTTP output to the client. Variable url is used to redirect to ./url
func redirect(w http.ResponseWriter, url string) {
_, _ = io.WriteString(w, "<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=./"+url+"\"></head></html>")
func redirect(w http.ResponseWriter, r *http.Request, url string) {
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

func redirectOnIncorrectId(w http.ResponseWriter, r *http.Request, url string) {
ratelimiter.WaitOnFailedId(r)
redirect(w, url)
redirect(w, r, url)
}

type redirectValues struct {
Expand All @@ -251,7 +249,7 @@ func redirectFromFilename(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
file, ok := storage.GetFile(id)
if !ok {
redirect(w, "../../error")
redirect(w, r, "../../error")
return
}

Expand Down Expand Up @@ -325,7 +323,7 @@ func changePassword(w http.ResponseWriter, r *http.Request) {
panic(err)
}
if !user.ResetPassword {
redirect(w, "admin")
redirect(w, r, "admin")
return
}
err = r.ParseForm()
Expand All @@ -344,7 +342,7 @@ func changePassword(w http.ResponseWriter, r *http.Request) {
user.Password = pwHash
user.ResetPassword = false
database.SaveUser(user, false)
redirect(w, "admin")
redirect(w, r, "admin")
return
}
}
Expand Down Expand Up @@ -377,60 +375,24 @@ func validateNewPassword(newPassword string, user models.User, userCsrfToken str

// Handling of /error
func showError(w http.ResponseWriter, r *http.Request) {
const (
invalidFile = iota
noCipherSupplied
wrongCipher
invalidFileRequest
)

errorReason := invalidFile
cardWidth := 18
if r.URL.Query().Has("e2e") {
errorReason = noCipherSupplied
cardWidth = 25
}
if r.URL.Query().Has("key") {
errorReason = wrongCipher
cardWidth = 25
}
if r.URL.Query().Has("fr") {
errorReason = invalidFileRequest
cardWidth = 30
}
err := templateFolder.ExecuteTemplate(w, "error", genericView{
ErrorId: errorReason,
ErrorCardWidth: cardWidth,
PublicName: configuration.Get().PublicName,
CustomContent: customStaticInfo})
helper.CheckIgnoreTimeout(err)
}

// Handling of /error-auth
func showErrorAuth(w http.ResponseWriter, r *http.Request) {
err := templateFolder.ExecuteTemplate(w, "error_auth", genericView{
PublicName: configuration.Get().PublicName,
CustomContent: customStaticInfo})
helper.CheckIgnoreTimeout(err)
}
displayedError := errorHandling.Get(r)

// Handling of /error-header
func showErrorHeader(w http.ResponseWriter, r *http.Request) {
err := templateFolder.ExecuteTemplate(w, "error_auth_header", genericView{
PublicName: configuration.Get().PublicName,
CustomContent: customStaticInfo})
helper.CheckIgnoreTimeout(err)
}
if r.URL.Query().Has("e2e") {
displayedError.ErrorId = errorHandling.TypeE2ECipher
displayedError.IsGeneric = true
displayedError.CardWidth = "25rem"
}

// Handling of /error-oauth
func showErrorIntOAuth(w http.ResponseWriter, r *http.Request) {
view := oauthErrorView{PublicName: configuration.Get().PublicName,
CustomContent: customStaticInfo}
view.IsAuthDenied = r.URL.Query().Get("isDenied") == "true"
view.ErrorProvidedName = r.URL.Query().Get("error")
view.ErrorProvidedMessage = r.URL.Query().Get("error_description")
view.ErrorGenericMessage = r.URL.Query().Get("error_generic")
err := templateFolder.ExecuteTemplate(w, "error_int_oauth", view)
err := templateFolder.ExecuteTemplate(w, "error", genericView{
ErrorId: displayedError.ErrorId,
ErrorCardWidth: displayedError.CardWidth,
IsGenericError: displayedError.IsGeneric,
ErrorTitle: displayedError.Title,
ErrorMessage: displayedError.Message,
ErrorOauthMessage: displayedError.OAuthProviderMessage,
PublicName: configuration.Get().PublicName,
CustomContent: customStaticInfo})
helper.CheckIgnoreTimeout(err)
}

Expand All @@ -451,7 +413,7 @@ func showUploadRequest(w http.ResponseWriter, r *http.Request) {
view := (&AdminView{}).convertGlobalConfig(ViewFileRequests, userId)

if !view.ActiveUser.HasPermissionCreateFileRequests() {
redirect(w, "admin")
redirect(w, r, "admin")
return
}
err = templateFolder.ExecuteTemplate(w, "uploadreq", view)
Expand All @@ -468,7 +430,7 @@ func showApiAdmin(w http.ResponseWriter, r *http.Request) {
view := (&AdminView{}).convertGlobalConfig(ViewAPI, userId)

if configuration.GetEnvironment().DisableApiMenu && !view.ActiveUser.IsAdmin() {
redirect(w, "admin")
redirect(w, r, "admin")
return
}

Expand All @@ -485,7 +447,7 @@ func showUserAdmin(w http.ResponseWriter, r *http.Request) {
}
view := (&AdminView{}).convertGlobalConfig(ViewUsers, userId)
if !view.ActiveUser.HasPermissionManageUsers() || configuration.Get().Authentication.Method == models.AuthenticationDisabled {
redirect(w, "admin")
redirect(w, r, "admin")
return
}
err = templateFolder.ExecuteTemplate(w, "users", view)
Expand All @@ -501,25 +463,30 @@ func processApi(w http.ResponseWriter, r *http.Request) {
// Shows a login form. If not authenticated, client needs to wait for three seconds.
// If correct, a new session is created and the user is redirected to the admin menu
func showLogin(w http.ResponseWriter, r *http.Request) {
_, ok := authentication.IsAuthenticated(w, r)
_, ok, err := authentication.IsAuthenticated(w, r)
if err != nil {
errorHandling.RedirectToErrorPage(w, r, "Unable to log in", "The following error was raised: "+err.Error(), errorHandling.WidthDefault)
return
}
if ok {
redirect(w, "admin")
redirect(w, r, "admin")
return
}
if configuration.Get().Authentication.Method == models.AuthenticationHeader {
redirect(w, "error-header")
errorHandling.RedirectToErrorPage(w, r, "Unauthorised",
"No login information was sent from the authentication provider.", errorHandling.WidthDefault)
return
}
if configuration.Get().Authentication.Method == models.AuthenticationOAuth2 {
// If user clicked logout, force consent
if r.URL.Query().Has("consent") {
redirect(w, "oauth-login?consent=true")
redirect(w, r, "oauth-login?consent=true")
} else {
redirect(w, "oauth-login")
redirect(w, r, "oauth-login")
}
return
}
err := r.ParseForm()
err = r.ParseForm()
if err != nil {
fmt.Println("Invalid form data sent to server for /login")
fmt.Println(err)
Expand All @@ -536,7 +503,7 @@ func showLogin(w http.ResponseWriter, r *http.Request) {
if validCredentials {
logging.LogValidLogin(user)
sessionmanager.CreateSession(w, false, 0, retrievedUser.Id)
redirect(w, "admin")
redirect(w, r, "admin")
return
}
logging.LogInvalidLogin(user, ip)
Expand Down Expand Up @@ -569,7 +536,7 @@ type LoginView struct {
// If it exists, a download form is shown, or a password needs to be entered.
func showDownload(w http.ResponseWriter, r *http.Request) {
addNoCacheHeader(w)
keyId := queryUrl(w, r, "id", "error")
keyId := queryUrl(w, r, "id", errorHandling.TypeFileNotFound)
file, ok := storage.GetFile(keyId)
if !ok || file.IsFileRequest() {
redirectOnIncorrectId(w, r, "error")
Expand Down Expand Up @@ -616,7 +583,7 @@ func showDownload(w http.ResponseWriter, r *http.Request) {
if configuration.HashPassword(enteredPassword, true) == file.PasswordHash {
writeFilePwCookie(w, file)
// redirect so that there is no post data to be resent if user refreshes page
redirect(w, "d?id="+file.Id)
redirect(w, r, "d?id="+file.Id)
return
}
view.IsFailedLogin = true
Expand Down Expand Up @@ -646,10 +613,10 @@ func showHotlink(w http.ResponseWriter, r *http.Request) {
}

// Checks if a file is associated with the GET parameter from the current URL
func queryUrl(w http.ResponseWriter, r *http.Request, keyword string, redirectUrl string) string {
func queryUrl(w http.ResponseWriter, r *http.Request, keyword string, errorType int) string {
keys, ok := r.URL.Query()[keyword]
if !ok || len(keys[0]) < environment.MinLengthId {
redirect(w, redirectUrl)
errorHandling.RedirectGenericErrorPage(w, r, errorType)
return ""
}
return keys[0]
Expand All @@ -667,7 +634,7 @@ func showAdminMenu(w http.ResponseWriter, r *http.Request) {
if config.Encryption.Level == encryption.EndToEndEncryption {
e2einfo := database.GetEnd2EndInfo(user.Id)
if !e2einfo.HasBeenSetUp() {
redirect(w, "e2eSetup")
redirect(w, r, "e2eSetup")
return
}
}
Expand All @@ -692,7 +659,7 @@ func showLogs(w http.ResponseWriter, r *http.Request) {
}
view := (&AdminView{}).convertGlobalConfig(ViewLogs, user)
if !view.ActiveUser.HasPermissionManageLogs() {
redirect(w, "admin")
redirect(w, r, "admin")
return
}
err = templateFolder.ExecuteTemplate(w, "logs", view)
Expand All @@ -701,7 +668,7 @@ func showLogs(w http.ResponseWriter, r *http.Request) {

func showE2ESetup(w http.ResponseWriter, r *http.Request) {
if configuration.Get().Encryption.Level != encryption.EndToEndEncryption {
redirect(w, "admin")
redirect(w, r, "admin")
return
}

Expand Down Expand Up @@ -961,23 +928,23 @@ type userInfo struct {
// Handling of /publicUpload
func showPublicUpload(w http.ResponseWriter, r *http.Request) {
addNoCacheHeader(w)
fileRequestId := queryUrl(w, r, "id", "error?fr")
fileRequestId := queryUrl(w, r, "id", errorHandling.TypeInvalidFileRequest)
request, ok := filerequest.Get(fileRequestId)
if !ok {
redirect(w, "error?fr")
errorHandling.RedirectGenericErrorPage(w, r, errorHandling.TypeInvalidFileRequest)
return
}
if !request.IsUnlimitedTime() && request.Expiry < time.Now().Unix() {
redirect(w, "error?fr")
errorHandling.RedirectGenericErrorPage(w, r, errorHandling.TypeInvalidFileRequest)
return
}
if !request.IsUnlimitedFiles() && request.UploadedFiles >= request.MaxFiles {
redirect(w, "error?fr")
errorHandling.RedirectGenericErrorPage(w, r, errorHandling.TypeInvalidFileRequest)
return
}
apiKey := queryUrl(w, r, "key", "error?fr")
apiKey := queryUrl(w, r, "key", errorHandling.TypeInvalidFileRequest)
if subtle.ConstantTimeCompare([]byte(request.ApiKey), []byte(apiKey)) != 1 {
redirect(w, "error?fr")
errorHandling.RedirectGenericErrorPage(w, r, errorHandling.TypeInvalidFileRequest)
return
}

Expand Down Expand Up @@ -1030,7 +997,7 @@ func downloadFileWithNameInUrl(w http.ResponseWriter, r *http.Request) {
// Handling of /downloadFile
// Outputs the file to the user and reduces the download remaining count for the file
func downloadFile(w http.ResponseWriter, r *http.Request) {
id := queryUrl(w, r, "id", "error")
id := queryUrl(w, r, "id", errorHandling.TypeFileNotFound)
serveFile(id, true, w, r)
}

Expand Down Expand Up @@ -1082,9 +1049,9 @@ func serveFile(id string, isRootUrl bool, w http.ResponseWriter, r *http.Request
if savedFile.PasswordHash != "" {
if !(isValidPwCookie(r, savedFile)) {
if isRootUrl {
redirect(w, "d?id="+savedFile.Id)
redirect(w, r, "d?id="+savedFile.Id)
} else {
redirect(w, "../../d?id="+savedFile.Id)
redirect(w, r, "../../d?id="+savedFile.Id)
}
return
}
Expand All @@ -1095,11 +1062,15 @@ func serveFile(id string, isRootUrl bool, w http.ResponseWriter, r *http.Request
func requireLogin(next http.HandlerFunc, isUiCall, isPwChangeView bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
addNoCacheHeader(w)
user, isLoggedIn := authentication.IsAuthenticated(w, r)
user, isLoggedIn, err := authentication.IsAuthenticated(w, r)
if err != nil {
errorHandling.RedirectToErrorPage(w, r, "Unable to log in", "The following error was raised: "+err.Error(), errorHandling.WidthDefault)
return
}
if isLoggedIn {
if user.ResetPassword && isUiCall && configuration.Get().Authentication.Method == models.AuthenticationInternal {
if !isPwChangeView {
redirect(w, "changePassword")
redirect(w, r, "changePassword")
return
}
}
Expand All @@ -1112,7 +1083,7 @@ func requireLogin(next http.HandlerFunc, isUiCall, isPwChangeView bool) http.Han
_, _ = io.WriteString(w, "{\"Result\":\"error\",\"ErrorMessage\":\"Not authenticated\"}")
return
}
redirect(w, "login")
redirect(w, r, "login")
}
}

Expand All @@ -1121,7 +1092,7 @@ type adminButtonContext struct {
ActiveUser *models.User
}

// Used internally in templates, to create buttons with user context
// Used internally in templates to create buttons with user context
func newAdminButtonContext(file models.FileApiOutput, user models.User) adminButtonContext {
return adminButtonContext{CurrentFile: file, ActiveUser: &user}
}
Expand Down Expand Up @@ -1164,12 +1135,15 @@ func addCacheHeader(w http.ResponseWriter) {
type genericView struct {
IsAdminView bool
IsDownloadView bool
IsGenericError bool
PublicName string
RedirectUrl string
ErrorTitle string
ErrorMessage string
ErrorOauthMessage string
CsrfToken string
ErrorCardWidth string
ErrorId int
ErrorCardWidth int
MinPasswordLength int
CustomContent customStatic
}
Expand Down
Loading
Loading