mirror of
https://github.com/Forceu/Gokapi.git
synced 2026-02-21 16:18:55 -06:00
Added header-based authentication #30
This commit is contained in:
@@ -43,28 +43,30 @@ var mutex sync.RWMutex
|
||||
|
||||
// Configuration is a struct that contains the global configuration
|
||||
type Configuration struct {
|
||||
Port string `json:"Port"`
|
||||
AdminName string `json:"AdminName"`
|
||||
AdminPassword string `json:"AdminPassword"`
|
||||
ServerUrl string `json:"ServerUrl"`
|
||||
DefaultDownloads int `json:"DefaultDownloads"`
|
||||
DefaultExpiry int `json:"DefaultExpiry"`
|
||||
DefaultPassword string `json:"DefaultPassword"`
|
||||
RedirectUrl string `json:"RedirectUrl"`
|
||||
Sessions map[string]models.Session `json:"Sessions"`
|
||||
Files map[string]models.File `json:"Files"`
|
||||
Hotlinks map[string]models.Hotlink `json:"Hotlinks"`
|
||||
DownloadStatus map[string]models.DownloadStatus `json:"DownloadStatus"`
|
||||
ApiKeys map[string]models.ApiKey `json:"ApiKeys"`
|
||||
ConfigVersion int `json:"ConfigVersion"`
|
||||
SaltAdmin string `json:"SaltAdmin"`
|
||||
SaltFiles string `json:"SaltFiles"`
|
||||
LengthId int `json:"LengthId"`
|
||||
DataDir string `json:"DataDir"`
|
||||
MaxMemory int `json:"MaxMemory"`
|
||||
UseSsl bool `json:"UseSsl"`
|
||||
MaxFileSizeMB int `json:"MaxFileSizeMB"`
|
||||
DisableLogin bool `json:"DisableLogin"`
|
||||
Port string `json:"Port"`
|
||||
AdminName string `json:"AdminName"`
|
||||
AdminPassword string `json:"AdminPassword"`
|
||||
ServerUrl string `json:"ServerUrl"`
|
||||
DefaultDownloads int `json:"DefaultDownloads"`
|
||||
DefaultExpiry int `json:"DefaultExpiry"`
|
||||
DefaultPassword string `json:"DefaultPassword"`
|
||||
RedirectUrl string `json:"RedirectUrl"`
|
||||
Sessions map[string]models.Session `json:"Sessions"`
|
||||
Files map[string]models.File `json:"Files"`
|
||||
Hotlinks map[string]models.Hotlink `json:"Hotlinks"`
|
||||
DownloadStatus map[string]models.DownloadStatus `json:"DownloadStatus"`
|
||||
ApiKeys map[string]models.ApiKey `json:"ApiKeys"`
|
||||
ConfigVersion int `json:"ConfigVersion"`
|
||||
SaltAdmin string `json:"SaltAdmin"`
|
||||
SaltFiles string `json:"SaltFiles"`
|
||||
LengthId int `json:"LengthId"`
|
||||
DataDir string `json:"DataDir"`
|
||||
MaxMemory int `json:"MaxMemory"`
|
||||
UseSsl bool `json:"UseSsl"`
|
||||
MaxFileSizeMB int `json:"MaxFileSizeMB"`
|
||||
DisableLogin bool `json:"DisableLogin"`
|
||||
LoginHeaderKey string `json:"LoginHeaderKey"`
|
||||
LoginHeaderForceUsername bool `json:"LoginHeaderForceUsername"`
|
||||
}
|
||||
|
||||
// Load loads the configuration or creates the folder structure and a default configuration
|
||||
@@ -161,6 +163,8 @@ func updateConfig() {
|
||||
// < v1.3.2
|
||||
if serverSettings.ConfigVersion < 9 {
|
||||
serverSettings.DisableLogin = environment.ToBool(Environment.DisableLogin)
|
||||
serverSettings.LoginHeaderForceUsername = environment.ToBool(Environment.LoginHeaderForceUser)
|
||||
serverSettings.LoginHeaderKey = Environment.LoginHeaderKey
|
||||
}
|
||||
|
||||
if serverSettings.ConfigVersion < currentConfigVersion {
|
||||
@@ -444,4 +448,8 @@ func IsLoginDisabled() bool {
|
||||
return serverSettings.DisableLogin
|
||||
}
|
||||
|
||||
func IsLogoutAvailable() bool {
|
||||
return !serverSettings.DisableLogin && serverSettings.LoginHeaderKey == ""
|
||||
}
|
||||
|
||||
var osExit = os.Exit
|
||||
|
||||
@@ -15,28 +15,30 @@ const IsFalse = "no"
|
||||
|
||||
// Environment is a struct containing available env variables
|
||||
type Environment struct {
|
||||
ConfigDir string
|
||||
ConfigFile string
|
||||
ConfigPath string
|
||||
DataDir string
|
||||
AdminName string
|
||||
AdminPassword string
|
||||
WebserverPort string
|
||||
WebserverLocalhost string
|
||||
ExternalUrl string
|
||||
RedirectUrl string
|
||||
SaltAdmin string
|
||||
SaltFiles string
|
||||
UseSsl string
|
||||
AwsBucket string
|
||||
AwsRegion string
|
||||
AwsKeyId string
|
||||
AwsKeySecret string
|
||||
AwsEndpoint string
|
||||
DisableLogin string
|
||||
LengthId int
|
||||
MaxMemory int
|
||||
MaxFileSize int
|
||||
ConfigDir string
|
||||
ConfigFile string
|
||||
ConfigPath string
|
||||
DataDir string
|
||||
AdminName string
|
||||
AdminPassword string
|
||||
WebserverPort string
|
||||
WebserverLocalhost string
|
||||
ExternalUrl string
|
||||
RedirectUrl string
|
||||
SaltAdmin string
|
||||
SaltFiles string
|
||||
UseSsl string
|
||||
AwsBucket string
|
||||
AwsRegion string
|
||||
AwsKeyId string
|
||||
AwsKeySecret string
|
||||
AwsEndpoint string
|
||||
DisableLogin string
|
||||
LoginHeaderKey string
|
||||
LoginHeaderForceUser string
|
||||
LengthId int
|
||||
MaxMemory int
|
||||
MaxFileSize int
|
||||
}
|
||||
|
||||
// ToBool checks if a string output by the environment package is equal true or false
|
||||
@@ -68,28 +70,30 @@ func New() Environment {
|
||||
configPath := configDir + "/" + configFile
|
||||
|
||||
return Environment{
|
||||
ConfigDir: configDir,
|
||||
ConfigFile: configFile,
|
||||
ConfigPath: configPath,
|
||||
DataDir: envString("DATA_DIR"),
|
||||
AdminName: envString("USERNAME"),
|
||||
AdminPassword: envString("PASSWORD"),
|
||||
WebserverPort: envString("PORT"),
|
||||
ExternalUrl: envString("EXTERNAL_URL"),
|
||||
RedirectUrl: envString("REDIRECT_URL"),
|
||||
SaltAdmin: envString("SALT_ADMIN"),
|
||||
SaltFiles: envString("SALT_FILES"),
|
||||
WebserverLocalhost: envBool("LOCALHOST"),
|
||||
LengthId: envInt("LENGTH_ID", 5),
|
||||
MaxMemory: envInt("MAX_MEMORY_UPLOAD_MB", 5),
|
||||
UseSsl: envBool("USE_SSL"),
|
||||
AwsBucket: envString("AWS_BUCKET"),
|
||||
AwsRegion: envString("AWS_REGION"),
|
||||
AwsKeyId: envString("AWS_KEY"),
|
||||
AwsKeySecret: envString("AWS_KEY_SECRET"),
|
||||
AwsEndpoint: envString("AWS_ENDPOINT"),
|
||||
MaxFileSize: envInt("MAX_FILESIZE", 1),
|
||||
DisableLogin: envBool("DISABLE_LOGIN"),
|
||||
ConfigDir: configDir,
|
||||
ConfigFile: configFile,
|
||||
ConfigPath: configPath,
|
||||
DataDir: envString("DATA_DIR"),
|
||||
AdminName: envString("USERNAME"),
|
||||
AdminPassword: envString("PASSWORD"),
|
||||
WebserverPort: envString("PORT"),
|
||||
ExternalUrl: envString("EXTERNAL_URL"),
|
||||
RedirectUrl: envString("REDIRECT_URL"),
|
||||
SaltAdmin: envString("SALT_ADMIN"),
|
||||
SaltFiles: envString("SALT_FILES"),
|
||||
WebserverLocalhost: envBool("LOCALHOST"),
|
||||
LengthId: envInt("LENGTH_ID", 5),
|
||||
MaxMemory: envInt("MAX_MEMORY_UPLOAD_MB", 5),
|
||||
UseSsl: envBool("USE_SSL"),
|
||||
AwsBucket: envString("AWS_BUCKET"),
|
||||
AwsRegion: envString("AWS_REGION"),
|
||||
AwsKeyId: envString("AWS_KEY"),
|
||||
AwsKeySecret: envString("AWS_KEY_SECRET"),
|
||||
AwsEndpoint: envString("AWS_ENDPOINT"),
|
||||
MaxFileSize: envInt("MAX_FILESIZE", 1),
|
||||
DisableLogin: envBool("DISABLE_LOGIN"),
|
||||
LoginHeaderKey: envString("LOGIN_HEADER_KEY"),
|
||||
LoginHeaderForceUser: envBool("LOGIN_HEADER_FORCE_USER"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"Gokapi/internal/models"
|
||||
"Gokapi/internal/storage"
|
||||
"Gokapi/internal/webserver/api"
|
||||
"Gokapi/internal/webserver/authentication"
|
||||
"Gokapi/internal/webserver/fileupload"
|
||||
"Gokapi/internal/webserver/sessionmanager"
|
||||
"Gokapi/internal/webserver/ssl"
|
||||
@@ -48,13 +49,11 @@ var imageExpiredPicture []byte
|
||||
const expiredFile = "static/expired.png"
|
||||
|
||||
var (
|
||||
webserverPort string
|
||||
webserverExtUrl string
|
||||
webserverRedirectUrl string
|
||||
webserverAdminName string
|
||||
webserverAdminPassword string
|
||||
webserverMaxMemory int
|
||||
webserverUseSsl bool
|
||||
webserverPort string
|
||||
webserverExtUrl string
|
||||
webserverRedirectUrl string
|
||||
webserverMaxMemory int
|
||||
webserverUseSsl bool
|
||||
)
|
||||
|
||||
// Start the webserver on the port set in the config
|
||||
@@ -117,8 +116,6 @@ func initLocalVariables() {
|
||||
webserverPort = settings.Port
|
||||
webserverExtUrl = settings.ServerUrl
|
||||
webserverRedirectUrl = settings.RedirectUrl
|
||||
webserverAdminName = settings.AdminName
|
||||
webserverAdminPassword = settings.AdminPassword
|
||||
webserverMaxMemory = settings.MaxMemory
|
||||
webserverUseSsl = settings.UseSsl
|
||||
configuration.ReleaseReadOnly()
|
||||
@@ -211,7 +208,7 @@ func processApi(w http.ResponseWriter, r *http.Request) {
|
||||
// Shows a login form. If username / pw combo is incorrect, 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) {
|
||||
if configuration.IsLoginDisabled() {
|
||||
if authentication.IsAuthenticated(w, r) {
|
||||
redirect(w, "admin")
|
||||
return
|
||||
}
|
||||
@@ -221,7 +218,7 @@ func showLogin(w http.ResponseWriter, r *http.Request) {
|
||||
pw := r.Form.Get("password")
|
||||
failedLogin := false
|
||||
if pw != "" && user != "" {
|
||||
if strings.ToLower(user) == strings.ToLower(webserverAdminName) && configuration.HashPassword(pw, false) == webserverAdminPassword {
|
||||
if authentication.IsCorrectUsernameAndPassword(user, pw) {
|
||||
sessionmanager.CreateSession(w, nil)
|
||||
redirect(w, "admin")
|
||||
return
|
||||
@@ -350,19 +347,19 @@ type DownloadView struct {
|
||||
|
||||
// UploadView contains parameters for the admin menu template
|
||||
type UploadView struct {
|
||||
Items []models.File
|
||||
ApiKeys []models.ApiKey
|
||||
Url string
|
||||
HotlinkUrl string
|
||||
TimeNow int64
|
||||
DefaultDownloads int
|
||||
DefaultExpiry int
|
||||
DefaultPassword string
|
||||
IsAdminView bool
|
||||
IsMainView bool
|
||||
IsApiView bool
|
||||
MaxFileSize int
|
||||
IsLoginDisabled bool
|
||||
Items []models.File
|
||||
ApiKeys []models.ApiKey
|
||||
Url string
|
||||
HotlinkUrl string
|
||||
TimeNow int64
|
||||
DefaultDownloads int
|
||||
DefaultExpiry int
|
||||
DefaultPassword string
|
||||
IsAdminView bool
|
||||
IsMainView bool
|
||||
IsApiView bool
|
||||
MaxFileSize int
|
||||
IsLogoutAvailable bool
|
||||
}
|
||||
|
||||
// Converts the globalConfig variable to an UploadView struct to pass the infos to
|
||||
@@ -408,7 +405,7 @@ func (u *UploadView) convertGlobalConfig(isMainView bool) *UploadView {
|
||||
u.IsAdminView = true
|
||||
u.IsMainView = isMainView
|
||||
u.MaxFileSize = settings.MaxFileSizeMB
|
||||
u.IsLoginDisabled = settings.DisableLogin
|
||||
u.IsLogoutAvailable = configuration.IsLogoutAvailable()
|
||||
configuration.ReleaseReadOnly()
|
||||
return u
|
||||
}
|
||||
@@ -454,10 +451,7 @@ func downloadFile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Checks if the user is logged in as an admin. Redirects to login page if not authenticated
|
||||
func isAuthenticatedOrRedirect(w http.ResponseWriter, r *http.Request, isUpload bool) bool {
|
||||
if configuration.IsLoginDisabled() {
|
||||
return true
|
||||
}
|
||||
if sessionmanager.IsValidSession(w, r) {
|
||||
if authentication.IsAuthenticated(w, r) {
|
||||
return true
|
||||
}
|
||||
if isUpload {
|
||||
|
||||
@@ -40,7 +40,7 @@ func Process(w http.ResponseWriter, r *http.Request, maxMemory int) {
|
||||
|
||||
// DeleteKey deletes the selected API key
|
||||
func DeleteKey(id string) bool {
|
||||
if !isValidKey(id, false) {
|
||||
if !IsValidApiKey(id, false) {
|
||||
return false
|
||||
}
|
||||
settings := configuration.GetServerSettings()
|
||||
@@ -63,7 +63,7 @@ func NewKey() string {
|
||||
}
|
||||
|
||||
func changeFriendlyName(w http.ResponseWriter, request apiRequest) {
|
||||
if !isValidKey(request.apiKeyToModify, false) {
|
||||
if !IsValidApiKey(request.apiKeyToModify, false) {
|
||||
sendError(w, http.StatusBadRequest, "Invalid api key provided.")
|
||||
return
|
||||
}
|
||||
@@ -115,25 +115,8 @@ func upload(w http.ResponseWriter, request apiRequest, maxMemory int) {
|
||||
sendOk(w)
|
||||
}
|
||||
|
||||
func isValidKey(key string, modifyTime bool) bool {
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
settings := configuration.GetServerSettings()
|
||||
defer configuration.Release()
|
||||
savedKey, ok := settings.ApiKeys[key]
|
||||
if ok && savedKey.Id != "" {
|
||||
if modifyTime {
|
||||
savedKey.LastUsed = time.Now().Unix()
|
||||
settings.ApiKeys[key] = savedKey
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isAuthorisedForApi(w http.ResponseWriter, request apiRequest) bool {
|
||||
if isValidKey(request.apiKey, true) || sessionmanager.IsValidSession(w, request.request) {
|
||||
if IsValidApiKey(request.apiKey, true) || sessionmanager.IsValidSession(w, request.request) {
|
||||
return true
|
||||
}
|
||||
sendError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
@@ -168,3 +151,20 @@ func parseRequest(r *http.Request) apiRequest {
|
||||
request: r,
|
||||
}
|
||||
}
|
||||
|
||||
func IsValidApiKey(key string, modifyTime bool) bool {
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
settings := configuration.GetServerSettings()
|
||||
defer configuration.Release()
|
||||
savedKey, ok := settings.ApiKeys[key]
|
||||
if ok && savedKey.Id != "" {
|
||||
if modifyTime {
|
||||
savedKey.LastUsed = time.Now().Unix()
|
||||
settings.ApiKeys[key] = savedKey
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ func TestDeleteKey(t *testing.T) {
|
||||
func TestIsValidApiKey(t *testing.T) {
|
||||
settings := configuration.GetServerSettings()
|
||||
configuration.Release()
|
||||
test.IsEqualBool(t, isValidKey("", false), false)
|
||||
test.IsEqualBool(t, isValidKey("invalid", false), false)
|
||||
test.IsEqualBool(t, isValidKey("validkey", false), true)
|
||||
test.IsEqualBool(t, IsValidApiKey("", false), false)
|
||||
test.IsEqualBool(t, IsValidApiKey("invalid", false), false)
|
||||
test.IsEqualBool(t, IsValidApiKey("validkey", false), true)
|
||||
test.IsEqualBool(t, settings.ApiKeys["validkey"].LastUsed == 0, true)
|
||||
test.IsEqualBool(t, isValidKey("validkey", true), true)
|
||||
test.IsEqualBool(t, IsValidApiKey("validkey", true), true)
|
||||
test.IsEqualBool(t, settings.ApiKeys["validkey"].LastUsed == 0, false)
|
||||
}
|
||||
|
||||
|
||||
58
internal/webserver/authentication/Authentication.go
Normal file
58
internal/webserver/authentication/Authentication.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"Gokapi/internal/configuration"
|
||||
"Gokapi/internal/webserver/sessionmanager"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func IsAuthenticated(w http.ResponseWriter, r *http.Request) bool {
|
||||
if byDisabledLogin() {
|
||||
return true
|
||||
}
|
||||
if byHeader(r) {
|
||||
return true
|
||||
}
|
||||
if byInternalSession(w, r) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// byHeader returns true if the user was authenticated by a proxy header if enabled
|
||||
func byHeader(r *http.Request) bool {
|
||||
settings := configuration.GetServerSettingsReadOnly()
|
||||
defer configuration.ReleaseReadOnly()
|
||||
|
||||
if settings.LoginHeaderKey == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
value := r.Header.Get(settings.LoginHeaderKey)
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
if settings.LoginHeaderForceUsername {
|
||||
return strings.ToLower(value) == strings.ToLower(settings.AdminName)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// byDisabledLogin returns true if login has been disabled
|
||||
func byDisabledLogin() bool {
|
||||
return configuration.IsLoginDisabled()
|
||||
}
|
||||
|
||||
// byInternalSession returns true if the user holds a valid internal session cookie
|
||||
func byInternalSession(w http.ResponseWriter, r *http.Request) bool {
|
||||
return sessionmanager.IsValidSession(w, r)
|
||||
}
|
||||
|
||||
// IsCorrectUsernameAndPassword checks if a provided username and password is correct
|
||||
func IsCorrectUsernameAndPassword(username, password string) bool {
|
||||
settings := configuration.GetServerSettingsReadOnly()
|
||||
configuration.ReleaseReadOnly()
|
||||
return strings.ToLower(username) == strings.ToLower(settings.AdminName) && configuration.HashPassword(password, false) == settings.AdminPassword
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
<nav class="nav nav-masthead justify-content-center">
|
||||
<a class="nav-link {{ if .IsMainView }}active{{ end }}" href="./admin">Upload</a>
|
||||
<a class="nav-link {{ if not .IsMainView }}active{{ end }}" href="./apiKeys">API</a>
|
||||
{{ if not .IsLoginDisabled }}<a class="nav-link" href="./logout">Logout</a>{{ end }}
|
||||
{{ if .IsLogoutAvailable }}<a class="nav-link" href="./logout">Logout</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user