mirror of
https://github.com/Forceu/Gokapi.git
synced 2026-01-06 00:49:33 -06:00
Refactoring, adding of documentation
This commit is contained in:
2
.github/workflows/test-code.yml
vendored
2
.github/workflows/test-code.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Docker Publish Test Compile
|
||||
name: Test Compilation
|
||||
on: [push, pull_request]
|
||||
|
||||
|
||||
|
||||
10
BuildVars.go
10
BuildVars.go
@@ -1,6 +1,14 @@
|
||||
package main
|
||||
|
||||
// compiler sets this to true if compiled with the Docker image
|
||||
/**
|
||||
Variables that are set during build
|
||||
*/
|
||||
|
||||
// Has to be true if compiled for the Docker image
|
||||
var IS_DOCKER = "false"
|
||||
|
||||
// Time of the build
|
||||
var BUILD_TIME = "Dev Build"
|
||||
|
||||
// Name of builder
|
||||
var BUILDER = "Manual Build"
|
||||
|
||||
@@ -9,12 +9,26 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
/**
|
||||
Loading and saving of the persistent configuration
|
||||
*/
|
||||
|
||||
// Name of the config dir that will be created
|
||||
const configDir = "config"
|
||||
|
||||
// Name of the config file where the configuration is written to
|
||||
const configFile = "config.json"
|
||||
|
||||
// Full path of configDir and configFile
|
||||
const configPath = configDir + "/" + configFile
|
||||
|
||||
// Name of the data dir that will be created
|
||||
const dataDir = "data"
|
||||
|
||||
// Global object containing the configuration
|
||||
var globalConfig Configuration
|
||||
|
||||
// Struct that contains the global configuration
|
||||
type Configuration struct {
|
||||
Port string `json:"Port"`
|
||||
AdminName string `json:"AdminName"`
|
||||
@@ -28,20 +42,7 @@ type Configuration struct {
|
||||
Files map[string]FileList `json:"Files"`
|
||||
}
|
||||
|
||||
func (f *FileList) toJsonResult() string {
|
||||
result := Result{
|
||||
Result: "OK",
|
||||
Url: globalConfig.ServerUrl + "d?id=",
|
||||
FileInfo: f,
|
||||
}
|
||||
bytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "{\"Result\":\"error\",\"ErrorMessage\":\"" + err.Error() + "\"}"
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
// Loads the configuration or creates the folder structure and a default configuration
|
||||
func loadConfig() {
|
||||
createConfigDir()
|
||||
if !fileExists(configPath) {
|
||||
@@ -60,6 +61,7 @@ func loadConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a default configuration and asks for items like username/password etc.
|
||||
func generateDefaultConfig() {
|
||||
fmt.Println("First start, creating new admin account")
|
||||
username := askForUsername()
|
||||
@@ -86,6 +88,7 @@ func generateDefaultConfig() {
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
// Saves the configuration as a json file
|
||||
func saveConfig() {
|
||||
file, err := os.OpenFile(configPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
@@ -101,6 +104,7 @@ func saveConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Asks for username and returns input as string if valid
|
||||
func askForUsername() string {
|
||||
fmt.Print("Username: ")
|
||||
username := readLine()
|
||||
@@ -111,15 +115,7 @@ func askForUsername() string {
|
||||
return askForUsername()
|
||||
}
|
||||
|
||||
func askForLocalOnly() bool {
|
||||
if IS_DOCKER != "false" {
|
||||
return false
|
||||
}
|
||||
fmt.Print("Bind port to localhost only? [Y/n]: ")
|
||||
input := strings.ToLower(readLine())
|
||||
return input != "n"
|
||||
}
|
||||
|
||||
// Asks for password and returns input as string if valid
|
||||
func askForPassword() string {
|
||||
fmt.Print("Password: ")
|
||||
password1, err := terminal.ReadPassword(0)
|
||||
@@ -139,6 +135,17 @@ func askForPassword() string {
|
||||
return string(password1)
|
||||
}
|
||||
|
||||
// Asks if the server shall be bound to 127.0.0.1 and returns result as bool
|
||||
func askForLocalOnly() bool {
|
||||
if IS_DOCKER != "false" {
|
||||
return false
|
||||
}
|
||||
fmt.Print("Bind port to localhost only? [Y/n]: ")
|
||||
input := strings.ToLower(readLine())
|
||||
return input != "n"
|
||||
}
|
||||
|
||||
// Asks for server URL and returns input as string if valid
|
||||
func askForUrl() string {
|
||||
fmt.Print("Server URL [eg. https://gokapi.url/]: ")
|
||||
url := readLine()
|
||||
@@ -151,6 +158,7 @@ func askForUrl() string {
|
||||
return url
|
||||
}
|
||||
|
||||
// Asks for redirect URL and returns input as string if valid
|
||||
func askForRedirect() string {
|
||||
fmt.Print("URL that the index gets redirected to [eg. https://yourcompany.com/]: ")
|
||||
url := readLine()
|
||||
@@ -166,16 +174,11 @@ func askForRedirect() string {
|
||||
return url
|
||||
}
|
||||
|
||||
// Returns true if URL starts with http:// or https://
|
||||
func isValidUrl(url string) bool {
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
fmt.Println("URL needs to start with http:// or https://")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Result string `json:"Result"`
|
||||
FileInfo *FileList `json:"FileInfo"`
|
||||
Url string `json:"Url"`
|
||||
}
|
||||
}
|
||||
24
Dockerfile.testing
Normal file
24
Dockerfile.testing
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM golang:1.16 AS build_base
|
||||
|
||||
## Usage:
|
||||
## docker build . -t gokapi
|
||||
## docker run -it -v gokapi-data:/app/data -v gokapi-config:/app/config -p 127.0.0.1:53842:53842 gokapi
|
||||
|
||||
RUN mkdir /compile
|
||||
COPY go.mod /compile
|
||||
RUN cd /compile && go mod download
|
||||
|
||||
COPY . /compile
|
||||
|
||||
RUN cd /compile && CGO_ENABLED=0 go build -ldflags="-s -w -X 'main.IS_DOCKER=true' -X 'main.BUILDER=Docker Testing' -X 'main.BUILD_TIME=$(date)'" -o /compile/gokapi
|
||||
|
||||
FROM alpine:3.13
|
||||
|
||||
|
||||
RUN apk add ca-certificates && mkdir /app && touch /app/.isdocker
|
||||
COPY --from=build_base /compile/gokapi /app/gokapi
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["/app/gokapi"]
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
@@ -10,6 +11,14 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
/**
|
||||
Serving and processing uploaded files
|
||||
*/
|
||||
|
||||
// The length for IDs used in URLs. Can be increased to improve security and decreased to increase readability
|
||||
const lengthId = 15
|
||||
|
||||
// Struct used for saving information about an uploaded file
|
||||
type FileList struct {
|
||||
Id string `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
@@ -21,8 +30,31 @@ type FileList struct {
|
||||
PasswordHash string `json:"PasswordHash"`
|
||||
}
|
||||
|
||||
const lengthId = 15
|
||||
// Converts the file info to a json String used for returning a result for an upload
|
||||
func (f *FileList) toJsonResult() string {
|
||||
result := Result{
|
||||
Result: "OK",
|
||||
Url: globalConfig.ServerUrl + "d?id=",
|
||||
FileInfo: f,
|
||||
}
|
||||
bytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "{\"Result\":\"error\",\"ErrorMessage\":\"" + err.Error() + "\"}"
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
// The struct used for the result after an upload
|
||||
type Result struct {
|
||||
Result string `json:"Result"`
|
||||
FileInfo *FileList `json:"FileInfo"`
|
||||
Url string `json:"Url"`
|
||||
}
|
||||
|
||||
// Creates a new file in the system. Called after an upload has been completed. If a file with the same sha256 hash
|
||||
// already exists, it is deduplicated. This function gathers information about the file, creates and ID and saves
|
||||
// it into the global configuration.
|
||||
func createNewFile(fileContent *multipart.File, fileHeader *multipart.FileHeader, expireAt int64, downloads int, password string) (FileList, error) {
|
||||
id, err := generateRandomString(lengthId)
|
||||
if err != nil {
|
||||
@@ -46,8 +78,8 @@ func createNewFile(fileContent *multipart.File, fileHeader *multipart.FileHeader
|
||||
PasswordHash: hashPassword(password, SALT_PW_FILES),
|
||||
}
|
||||
globalConfig.Files[id] = file
|
||||
filename := "data/" + file.SHA256
|
||||
if !fileExists("data/" + file.SHA256) {
|
||||
filename := dataDir + "/" + file.SHA256
|
||||
if !fileExists(dataDir + "/" + file.SHA256) {
|
||||
destinationFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return FileList{}, err
|
||||
@@ -59,7 +91,10 @@ func createNewFile(fileContent *multipart.File, fileHeader *multipart.FileHeader
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func cleanUpOldFiles(sleep bool) {
|
||||
// Removes expired files from the config and from the filesystem if they are not referenced by other files anymore
|
||||
// Will be called periodically or after a file has been manually deleted in the admin view.
|
||||
// If parameter periodic is true, this function is recursive and calls itself every hour.
|
||||
func cleanUpOldFiles(periodic bool) {
|
||||
timeNow := time.Now().Unix()
|
||||
wasItemDeleted := false
|
||||
for key, element := range globalConfig.Files {
|
||||
@@ -71,7 +106,7 @@ func cleanUpOldFiles(sleep bool) {
|
||||
}
|
||||
}
|
||||
if deleteFile {
|
||||
err := os.Remove("data/" + element.SHA256)
|
||||
err := os.Remove(dataDir + "/" + element.SHA256)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
@@ -84,8 +119,8 @@ func cleanUpOldFiles(sleep bool) {
|
||||
saveConfig()
|
||||
cleanUpOldFiles(false)
|
||||
}
|
||||
if sleep {
|
||||
if periodic {
|
||||
time.Sleep(time.Hour)
|
||||
go cleanUpOldFiles(true)
|
||||
go cleanUpOldFiles(periodic)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,11 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func check(e error) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
|
||||
const SALT_PW_ADMIN = "eefwkjqweduiotbrkl##$2342brerlk2321"
|
||||
const SALT_PW_FILES = "P1UI5sRNDwuBgOvOYhNsmucZ2pqo4KEvOoqqbpdu"
|
||||
/**
|
||||
Various functions, mostly for OS access
|
||||
*/
|
||||
|
||||
// Hashes a password with SHA256 and a salt
|
||||
func hashPassword(password, salt string) string {
|
||||
if password == "" {
|
||||
return ""
|
||||
@@ -32,6 +28,7 @@ func hashPassword(password, salt string) string {
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
// Returns true if a folder exists
|
||||
func folderExists(folder string) bool {
|
||||
_, err := os.Stat(folder)
|
||||
if err == nil {
|
||||
@@ -40,6 +37,7 @@ func folderExists(folder string) bool {
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// Returns true if a file exists
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -48,6 +46,7 @@ func fileExists(filename string) bool {
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
// Converts bytes to a human readable format
|
||||
func byteCountSI(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
@@ -62,6 +61,7 @@ func byteCountSI(b int64) string {
|
||||
float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
// A rune array to be used for pseudo-random string generation
|
||||
var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
//Used if unable to generate secure random string. A warning will be output
|
||||
@@ -75,7 +75,7 @@ func unsafeId(length int) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// generateRandomBytes returns securely generated random bytes.
|
||||
// Returns securely generated random bytes.
|
||||
// It will return an error if the system's secure random
|
||||
// number generator fails to function correctly
|
||||
func generateRandomBytes(n int) ([]byte, error) {
|
||||
@@ -87,28 +87,38 @@ func generateRandomBytes(n int) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// generateRandomString returns a URL-safe, base64 encoded
|
||||
// securely generated random string.
|
||||
// Returns a URL-safe, base64 encoded securely generated random string.
|
||||
func generateRandomString(length int) (string, error) {
|
||||
b, err := generateRandomBytes(length)
|
||||
return base64.URLEncoding.EncodeToString(b), err
|
||||
}
|
||||
|
||||
// Creates the data folder if it does not exist
|
||||
func createDataDir() {
|
||||
if !folderExists("data") {
|
||||
err := os.Mkdir("data", 0770)
|
||||
if !folderExists(dataDir) {
|
||||
err := os.Mkdir(dataDir, 0770)
|
||||
check(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Creates the config folder if it does not exist
|
||||
func createConfigDir() {
|
||||
if !folderExists(configDir) {
|
||||
err := os.Mkdir(configDir, 0770)
|
||||
check(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reads a line from the terminal and returns it as a string
|
||||
func readLine() string {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
text, _ := reader.ReadString('\n')
|
||||
return strings.Replace(text, "\n", "", -1)
|
||||
}
|
||||
}
|
||||
|
||||
// Panics if error is not nil
|
||||
func check(e error) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
|
||||
15
Main.go
15
Main.go
@@ -7,9 +7,20 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
//needs to be changed in ./templates/string_constants.tmpl as well
|
||||
/**
|
||||
Main routine
|
||||
*/
|
||||
|
||||
// needs to be changed in ./templates/string_constants.tmpl as well
|
||||
const VERSION = "1.1.0"
|
||||
|
||||
// Salt for the admin password hash
|
||||
const SALT_PW_ADMIN = "eefwkjqweduiotbrkl##$2342brerlk2321"
|
||||
|
||||
// Salt for the file password hashes
|
||||
const SALT_PW_FILES = "P1UI5sRNDwuBgOvOYhNsmucZ2pqo4KEvOoqqbpdu"
|
||||
|
||||
// Main routine that is called on startup
|
||||
func main() {
|
||||
checkPrimaryArguments()
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
@@ -22,6 +33,7 @@ func main() {
|
||||
startWebserver()
|
||||
}
|
||||
|
||||
// Checks for command line arguments that have to be parsed before loading the configuration
|
||||
func checkPrimaryArguments() {
|
||||
if len(os.Args) > 1 {
|
||||
if os.Args[1] == "--version" || os.Args[1] == "-v" {
|
||||
@@ -34,6 +46,7 @@ func checkPrimaryArguments() {
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for command line arguments that have to be parsed after loading the configuration
|
||||
func checkArguments() {
|
||||
if len(os.Args) > 1 {
|
||||
if os.Args[1] == "--reset-pw" {
|
||||
|
||||
@@ -5,28 +5,22 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
//If no login occurred during this time, the session will be deleted. Default 30 days
|
||||
const COOKIE_LIFE = 30 * 24 * time.Hour
|
||||
/**
|
||||
Manages the sessions for the admin user or to access password protected files
|
||||
*/
|
||||
|
||||
//If no login occurred during this time, the admin session will be deleted. Default 30 days
|
||||
const COOKIE_LIFE_ADMIN = 30 * 24 * time.Hour
|
||||
|
||||
// Structure for cookies
|
||||
type Session struct {
|
||||
RenewAt int64
|
||||
ValidUntil int64
|
||||
}
|
||||
|
||||
func useSession(w http.ResponseWriter, sessionString string) bool {
|
||||
session := globalConfig.Sessions[sessionString]
|
||||
if session.ValidUntil < time.Now().Unix() {
|
||||
delete(globalConfig.Sessions, sessionString)
|
||||
return false
|
||||
}
|
||||
if session.RenewAt < time.Now().Unix() {
|
||||
createSession(w)
|
||||
delete(globalConfig.Sessions, sessionString)
|
||||
saveConfig()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Checks if the user is submitting a valid session token
|
||||
// If valid session is found, useSession will be called
|
||||
// Returns true if authenticated, otherwise false
|
||||
func isValidSession(w http.ResponseWriter, r *http.Request) bool {
|
||||
cookie, err := r.Cookie("session_token")
|
||||
if err == nil {
|
||||
@@ -41,6 +35,25 @@ func isValidSession(w http.ResponseWriter, r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Checks if a session is still valid. Changes the session string if it has
|
||||
// been used for more than an hour to limit session hijacking
|
||||
// Returns true if session is still valid
|
||||
// Returns false if session is invalid (and deletes it)
|
||||
func useSession(w http.ResponseWriter, sessionString string) bool {
|
||||
session := globalConfig.Sessions[sessionString]
|
||||
if session.ValidUntil < time.Now().Unix() {
|
||||
delete(globalConfig.Sessions, sessionString)
|
||||
return false
|
||||
}
|
||||
if session.RenewAt < time.Now().Unix() {
|
||||
createSession(w)
|
||||
delete(globalConfig.Sessions, sessionString)
|
||||
saveConfig()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
//Creates a new session - called after login with correct username / password
|
||||
func createSession(w http.ResponseWriter) {
|
||||
sessionString, err := generateRandomString(60)
|
||||
if err != nil {
|
||||
@@ -48,12 +61,13 @@ func createSession(w http.ResponseWriter) {
|
||||
}
|
||||
globalConfig.Sessions[sessionString] = Session{
|
||||
RenewAt: time.Now().Add(time.Hour).Unix(),
|
||||
ValidUntil: time.Now().Add(COOKIE_LIFE).Unix(),
|
||||
ValidUntil: time.Now().Add(COOKIE_LIFE_ADMIN).Unix(),
|
||||
}
|
||||
writeSessionCookie(w, sessionString, time.Now().Add(COOKIE_LIFE))
|
||||
writeSessionCookie(w, sessionString, time.Now().Add(COOKIE_LIFE_ADMIN))
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
// Logs out user and deletes session
|
||||
func logoutSession(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("session_token")
|
||||
if err == nil {
|
||||
@@ -63,6 +77,7 @@ func logoutSession(w http.ResponseWriter, r *http.Request) {
|
||||
writeSessionCookie(w, "", time.Now())
|
||||
}
|
||||
|
||||
// Writes session cookie to browser
|
||||
func writeSessionCookie(w http.ResponseWriter, sessionString string, expiry time.Time) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session_token",
|
||||
@@ -70,4 +85,3 @@ func writeSessionCookie(w http.ResponseWriter, sessionString string, expiry time
|
||||
Expires: expiry,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
47
Webserver.go
47
Webserver.go
@@ -15,14 +15,24 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
/**
|
||||
Handling of webserver and requests / uploads
|
||||
*/
|
||||
|
||||
// Embedded version of the "static" folder
|
||||
// This contains JS files, CSS, images etc
|
||||
//go:embed static
|
||||
var staticFolderEmbedded embed.FS
|
||||
|
||||
// Embedded version of the "templates" folder
|
||||
// This contains templates that Gokapi uses for creating the HTML output
|
||||
//go:embed templates
|
||||
var templateFolderEmbedded embed.FS
|
||||
|
||||
// Variable containing all parsed templates
|
||||
var templateFolder *template.Template
|
||||
|
||||
// Starts the webserver on the port set in the config
|
||||
func startWebserver() {
|
||||
webserverDir, _ := fs.Sub(staticFolderEmbedded, "static")
|
||||
if folderExists("static") {
|
||||
@@ -45,6 +55,9 @@ func startWebserver() {
|
||||
log.Fatal(http.ListenAndServe(globalConfig.Port, nil))
|
||||
}
|
||||
|
||||
// Initialises the templateFolder variable by scanning through all the templates.
|
||||
// If a folder "templates" exists in the main directory, it is used.
|
||||
// Otherwise templateFolderEmbedded will be used.
|
||||
func initTemplates() {
|
||||
var err error
|
||||
if folderExists("templates") {
|
||||
@@ -56,30 +69,38 @@ func initTemplates() {
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a redirect HTTP output to the client. Variable url is used to redirect to ./url
|
||||
func redirect(w http.ResponseWriter, url string) {
|
||||
_, _ = fmt.Fprint(w, "<head><meta http-equiv=\"Refresh\" content=\"0; URL=./"+url+"\"></head>")
|
||||
}
|
||||
|
||||
// Handling of /logout
|
||||
func doLogout(w http.ResponseWriter, r *http.Request) {
|
||||
logoutSession(w, r)
|
||||
redirect(w, "login")
|
||||
}
|
||||
|
||||
// Handling of /index and redirecting to globalConfig.RedirectUrl
|
||||
func showIndex(w http.ResponseWriter, r *http.Request) {
|
||||
err := templateFolder.ExecuteTemplate(w, "index", globalConfig.RedirectUrl)
|
||||
check(err)
|
||||
}
|
||||
|
||||
// Handling of /error
|
||||
func showError(w http.ResponseWriter, r *http.Request) {
|
||||
err := templateFolder.ExecuteTemplate(w, "error", nil)
|
||||
check(err)
|
||||
}
|
||||
|
||||
// Handling of /forgotpw
|
||||
func forgotPassword(w http.ResponseWriter, r *http.Request) {
|
||||
err := templateFolder.ExecuteTemplate(w, "forgotpw", nil)
|
||||
check(err)
|
||||
}
|
||||
|
||||
// Handling of /login
|
||||
// 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) {
|
||||
err := r.ParseForm()
|
||||
check(err)
|
||||
@@ -103,11 +124,15 @@ func showLogin(w http.ResponseWriter, r *http.Request) {
|
||||
check(err)
|
||||
}
|
||||
|
||||
// Variables for the login template
|
||||
type LoginView struct {
|
||||
IsFailedLogin bool
|
||||
User string
|
||||
}
|
||||
|
||||
// Handling of /d
|
||||
// Checks if a file exists for the submitted ID
|
||||
// If it exists, a download form is shown or a password needs to be entered.
|
||||
func showDownload(w http.ResponseWriter, r *http.Request) {
|
||||
keyId := queryUrl(w, r, "error")
|
||||
if keyId == "" {
|
||||
@@ -154,6 +179,8 @@ func showDownload(w http.ResponseWriter, r *http.Request) {
|
||||
check(err)
|
||||
}
|
||||
|
||||
// Handling of /delete
|
||||
// User needs to be admin. Deleted the requested file
|
||||
func deleteFile(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAuthenticated(w, r, false) {
|
||||
return
|
||||
@@ -169,9 +196,11 @@ func deleteFile(w http.ResponseWriter, r *http.Request) {
|
||||
redirect(w, "admin")
|
||||
}
|
||||
|
||||
// Checks if a file is associated with the GET parameter from the current URL
|
||||
// Stops for 500ms to limit brute forcing if invalid key and redirects to redirectUrl
|
||||
func queryUrl(w http.ResponseWriter, r *http.Request, redirectUrl string) string {
|
||||
keys, ok := r.URL.Query()["id"]
|
||||
if !ok || len(keys[0]) < 15 {
|
||||
if !ok || len(keys[0]) < lengthId {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
redirect(w, redirectUrl)
|
||||
return ""
|
||||
@@ -179,6 +208,8 @@ func queryUrl(w http.ResponseWriter, r *http.Request, redirectUrl string) string
|
||||
return keys[0]
|
||||
}
|
||||
|
||||
// Handling of /admin
|
||||
// If user is authenticated, this menu lists all uploads and enables uploading new files
|
||||
func showAdminMenu(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAuthenticated(w, r, false) {
|
||||
return
|
||||
@@ -187,6 +218,7 @@ func showAdminMenu(w http.ResponseWriter, r *http.Request) {
|
||||
check(err)
|
||||
}
|
||||
|
||||
// Parameters for the download template
|
||||
type DownloadView struct {
|
||||
Name string
|
||||
Size string
|
||||
@@ -194,6 +226,7 @@ type DownloadView struct {
|
||||
IsFailedLogin bool
|
||||
}
|
||||
|
||||
// Parameters for the admin menu template
|
||||
type UploadView struct {
|
||||
Items []FileList
|
||||
Url string
|
||||
@@ -203,6 +236,8 @@ type UploadView struct {
|
||||
DefaultPassword string
|
||||
}
|
||||
|
||||
// Converts the globalConfig variable to an UploadView struct to pass the infos to
|
||||
// the admin template
|
||||
func (u *UploadView) convertGlobalConfig() *UploadView {
|
||||
var result []FileList
|
||||
for _, element := range globalConfig.Files {
|
||||
@@ -220,6 +255,9 @@ func (u *UploadView) convertGlobalConfig() *UploadView {
|
||||
return u
|
||||
}
|
||||
|
||||
// Handling of /upload
|
||||
// If the user is authenticated, this parses the uploaded file from the Multipart Form and
|
||||
// adds it to the system.
|
||||
func uploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAuthenticated(w, r, true) {
|
||||
return
|
||||
@@ -248,6 +286,8 @@ func uploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
_, err = fmt.Fprint(w, result.toJsonResult())
|
||||
check(err)
|
||||
}
|
||||
|
||||
// Outputs an error in json format
|
||||
func responseError(w http.ResponseWriter, err error) {
|
||||
if err != nil {
|
||||
fmt.Fprint(w, "{\"Result\":\"error\",\"ErrorMessage\":\""+err.Error()+"\"}")
|
||||
@@ -255,6 +295,7 @@ func responseError(w http.ResponseWriter, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Outputs the file to the user and reduces the download remaining count for the file
|
||||
func downloadFile(w http.ResponseWriter, r *http.Request) {
|
||||
keyId := queryUrl(w, r, "error")
|
||||
if keyId == "" {
|
||||
@@ -284,6 +325,7 @@ func downloadFile(w http.ResponseWriter, r *http.Request) {
|
||||
check(err)
|
||||
}
|
||||
|
||||
// Checks if the user is logged in as an admin
|
||||
func isAuthenticated(w http.ResponseWriter, r *http.Request, isUpload bool) bool {
|
||||
if isValidSession(w, r) {
|
||||
return true
|
||||
@@ -297,6 +339,7 @@ func isAuthenticated(w http.ResponseWriter, r *http.Request, isUpload bool) bool
|
||||
return false
|
||||
}
|
||||
|
||||
// Write a cookie if the user has entered a correct password for a password-protected file
|
||||
func writeFilePwCookie(w http.ResponseWriter, file FileList) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "p" + file.Id,
|
||||
@@ -305,6 +348,8 @@ func writeFilePwCookie(w http.ResponseWriter, file FileList) {
|
||||
})
|
||||
}
|
||||
|
||||
// Checks if a cookie contains the correct password hash for a password-protected file
|
||||
// If incorrect, a 3 second delay is introduced unless the cookie was empty.
|
||||
func isValidPwCookie(r *http.Request, file FileList) bool {
|
||||
cookie, err := r.Cookie("p" + file.Id)
|
||||
if err == nil {
|
||||
|
||||
Reference in New Issue
Block a user