From 132187dd082ab30e1ca2bd7e8c96f413e9c8265f Mon Sep 17 00:00:00 2001 From: Marc Ole Bulling Date: Tue, 6 Apr 2021 15:16:48 +0200 Subject: [PATCH] Added hotlinking for image files --- Main.go | 2 +- src/configuration/Configuration.go | 8 ++- src/helper/OS.go | 9 ++++ src/helper/StringGeneration.go | 13 ++++- src/storage/FileServing.go | 70 ++++++++++++++++++++++++++ src/storage/filestructure/FileList.go | 20 +++++--- src/webserver/Webserver.go | 63 ++++++++++++----------- static/expired.png | Bin 0 -> 5774 bytes static/js/admin.js | 11 +++- templates/html_admin.tmpl | 7 ++- templates/string_constants.tmpl | 2 +- 11 files changed, 164 insertions(+), 41 deletions(-) create mode 100644 static/expired.png diff --git a/Main.go b/Main.go index d961cdb..83da949 100644 --- a/Main.go +++ b/Main.go @@ -31,7 +31,7 @@ func main() { configuration.Load() checkArguments() go storage.CleanUp(true) - webserver.Start(staticFolderEmbedded, templateFolderEmbedded) + webserver.Start(&staticFolderEmbedded, &templateFolderEmbedded) } // Checks for command line arguments that have to be parsed before loading the configuration diff --git a/src/configuration/Configuration.go b/src/configuration/Configuration.go index ee5af78..121ddbb 100644 --- a/src/configuration/Configuration.go +++ b/src/configuration/Configuration.go @@ -34,7 +34,7 @@ var Environment environment.Environment var ServerSettings Configuration // Version of the configuration structure. Used for upgrading -const currentConfigVersion = 3 +const currentConfigVersion = 4 // Struct that contains the global configuration type Configuration struct { @@ -48,6 +48,7 @@ type Configuration struct { RedirectUrl string `json:"RedirectUrl"` Sessions map[string]sessionstructure.Session `json:"Sessions"` Files map[string]filestructure.File `json:"Files"` + Hotlinks map[string]filestructure.Hotlink `json:"Hotlinks"` ConfigVersion int `json:"ConfigVersion"` SaltAdmin string `json:"SaltAdmin"` SaltFiles string `json:"SaltFiles"` @@ -86,6 +87,10 @@ func updateConfig() { ServerSettings.LengthId = 15 ServerSettings.DataDir = Environment.DataDir } + // < v1.1.3 + if ServerSettings.ConfigVersion < 4 { + ServerSettings.Hotlinks = make(map[string]filestructure.Hotlink) + } if ServerSettings.ConfigVersion < currentConfigVersion { fmt.Println("Successfully upgraded database") @@ -126,6 +131,7 @@ func generateDefaultConfig() { RedirectUrl: redirect, Files: make(map[string]filestructure.File), Sessions: make(map[string]sessionstructure.Session), + Hotlinks: make(map[string]filestructure.Hotlink), ConfigVersion: currentConfigVersion, SaltAdmin: saltAdmin, SaltFiles: saltFiles, diff --git a/src/helper/OS.go b/src/helper/OS.go index 2ae0f77..ae7274a 100644 --- a/src/helper/OS.go +++ b/src/helper/OS.go @@ -57,3 +57,12 @@ func Check(e error) { panic(e) } } + +func IsInArray(haystack []string, needle string) bool { + for _, item := range haystack { + if needle == item { + return true + } + } + return false +} diff --git a/src/helper/StringGeneration.go b/src/helper/StringGeneration.go index 0415c8c..4449cb7 100644 --- a/src/helper/StringGeneration.go +++ b/src/helper/StringGeneration.go @@ -10,6 +10,7 @@ import ( "fmt" "log" "math/rand" + "regexp" ) // A rune array to be used for pseudo-random string generation @@ -44,7 +45,7 @@ func GenerateRandomString(length int) string { if err != nil { return generateUnsafeId(length) } - return base64.URLEncoding.EncodeToString(b) + return cleanRandomString(base64.URLEncoding.EncodeToString(b)) } // Converts bytes to a human readable format @@ -61,3 +62,13 @@ func ByteCountSI(b int64) string { return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) } + +// Removes special characters from string +func cleanRandomString(input string) string { + reg, err := regexp.Compile("[^a-zA-Z0-9]+") + if err != nil { + log.Fatal(err) + } + return reg.ReplaceAllString(input, "") + +} diff --git a/src/storage/FileServing.go b/src/storage/FileServing.go index b0a86f6..ee75700 100644 --- a/src/storage/FileServing.go +++ b/src/storage/FileServing.go @@ -11,9 +11,13 @@ import ( "crypto/sha1" "encoding/hex" "fmt" + "io" "io/ioutil" "mime/multipart" + "net/http" "os" + "path/filepath" + "strings" "time" ) @@ -41,6 +45,7 @@ func NewFile(fileContent *multipart.File, fileHeader *multipart.FileHeader, expi DownloadsRemaining: downloads, PasswordHash: configuration.HashPassword(password, true), } + addHotlink(&file) configuration.ServerSettings.Files[id] = file filename := configuration.ServerSettings.DataDir + "/" + file.SHA256 if !helper.FileExists(configuration.ServerSettings.DataDir + "/" + file.SHA256) { @@ -55,6 +60,68 @@ func NewFile(fileContent *multipart.File, fileHeader *multipart.FileHeader, expi return file, nil } +var imageFileExtensions = []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"} + +// If file is an image, create link for hotlinking +func addHotlink(file *filestructure.File) { + extension := strings.ToLower(filepath.Ext(file.Name)) + if !helper.IsInArray(imageFileExtensions, extension) { + return + } + link := helper.GenerateRandomString(40) + extension + file.HotlinkId = link + configuration.ServerSettings.Hotlinks[link] = filestructure.Hotlink{ + Id: link, + FileId: file.Id, + } +} + +// Gets the file by id. Returns (empty File, false) if invalid / expired file +// or (file, true) if valid file +func GetFile(id string) (filestructure.File, bool) { + var emptyResult = filestructure.File{} + if id == "" { + return emptyResult, false + } + file := configuration.ServerSettings.Files[id] + if file.ExpireAt < time.Now().Unix() || file.DownloadsRemaining < 1 { + return emptyResult, false + } + if !helper.FileExists(configuration.ServerSettings.DataDir + "/" + file.SHA256) { + return emptyResult, false + } + return file, true +} + +// Gets the file by hotlink id. Returns (empty File, false) if invalid / expired file +// or (file, true) if valid file +func GetFileByHotlink(id string) (filestructure.File, bool) { + var emptyResult = filestructure.File{} + if id == "" { + return emptyResult, false + } + hotlink := configuration.ServerSettings.Hotlinks[id] + return GetFile(hotlink.FileId) +} + +// Subtracts a download allowance and serves the file to the browser +func ServeFile(file filestructure.File, w http.ResponseWriter, r *http.Request, forceDownload bool) { + file.DownloadsRemaining = file.DownloadsRemaining - 1 + configuration.ServerSettings.Files[file.Id] = file + // Investigate: Possible race condition with clean-up routine? + configuration.Save() + + if forceDownload { + w.Header().Set("Content-Disposition", "attachment; filename=\""+file.Name+"\"") + } + w.Header().Set("Content-Type", r.Header.Get("Content-Type")) + storageData, err := os.OpenFile(configuration.ServerSettings.DataDir+"/"+file.SHA256, os.O_RDONLY, 0644) + defer storageData.Close() + helper.Check(err) + _, err = io.Copy(w, storageData) + helper.Check(err) +} + // 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. @@ -75,6 +142,9 @@ func CleanUp(periodic bool) { fmt.Println(err) } } + if element.HotlinkId != "" { + delete(configuration.ServerSettings.Hotlinks, element.HotlinkId) + } delete(configuration.ServerSettings.Files, key) wasItemDeleted = true } diff --git a/src/storage/filestructure/FileList.go b/src/storage/filestructure/FileList.go index 8b24f91..625d9a3 100644 --- a/src/storage/filestructure/FileList.go +++ b/src/storage/filestructure/FileList.go @@ -15,14 +15,21 @@ type File struct { ExpireAtString string `json:"ExpireAtString"` DownloadsRemaining int `json:"DownloadsRemaining"` PasswordHash string `json:"PasswordHash"` + HotlinkId string `json:"HotlinkId"` +} + +type Hotlink struct { + Id string `json:"Id"` + FileId string `json:"FileId"` } // Converts the file info to a json String used for returning a result for an upload func (f *File) ToJsonResult(serverUrl string) string { result := Result{ - Result: "OK", - Url: serverUrl + "d?id=", - FileInfo: f, + Result: "OK", + Url: serverUrl + "d?id=", + HotlinkUrl: serverUrl + "hotlink/", + FileInfo: f, } bytes, err := json.Marshal(result) if err != nil { @@ -34,7 +41,8 @@ func (f *File) ToJsonResult(serverUrl string) string { // The struct used for the result after an upload type Result struct { - Result string `json:"Result"` - FileInfo *File `json:"FileInfo"` - Url string `json:"Url"` + Result string `json:"Result"` + FileInfo *File `json:"FileInfo"` + Url string `json:"Url"` + HotlinkUrl string `json:"HotlinkUrl"` } diff --git a/src/webserver/Webserver.go b/src/webserver/Webserver.go index a8e4ba2..3b7ed81 100644 --- a/src/webserver/Webserver.go +++ b/src/webserver/Webserver.go @@ -12,7 +12,6 @@ import ( "embed" "fmt" "html/template" - "io" "io/fs" "log" "net/http" @@ -26,18 +25,28 @@ import ( // Variable containing all parsed templates var templateFolder *template.Template +var imageExpiredPicture []byte + +const expiredFile = "static/expired.png" + // Starts the webserver on the port set in the config -func Start(staticFolderEmbedded, templateFolderEmbedded embed.FS) { - initTemplates(templateFolderEmbedded) - webserverDir, _ := fs.Sub(staticFolderEmbedded, "static") +func Start(staticFolderEmbedded, templateFolderEmbedded *embed.FS) { + initTemplates(*templateFolderEmbedded) + webserverDir, _ := fs.Sub(*staticFolderEmbedded, "static") + var err error if helper.FolderExists("static") { fmt.Println("Found folder 'static', using local folder instead of internal static folder") http.Handle("/", http.FileServer(http.Dir("static"))) + imageExpiredPicture, err = os.ReadFile(expiredFile) + helper.Check(err) } else { http.Handle("/", http.FileServer(http.FS(webserverDir))) + imageExpiredPicture, err = fs.ReadFile(staticFolderEmbedded, expiredFile) + helper.Check(err) } http.HandleFunc("/index", showIndex) http.HandleFunc("/d", showDownload) + http.HandleFunc("/hotlink/", showHotlink) http.HandleFunc("/error", showError) http.HandleFunc("/login", showLogin) http.HandleFunc("/logout", doLogout) @@ -132,19 +141,12 @@ 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) { keyId := queryUrl(w, r, "error") - if keyId == "" { - return - } - file := configuration.ServerSettings.Files[keyId] - if file.ExpireAt < time.Now().Unix() || file.DownloadsRemaining < 1 { + file, ok := storage.GetFile(keyId) + if !ok { redirect(w, "error") return } - if !helper.FileExists(configuration.ServerSettings.DataDir + "/" + file.SHA256) { - redirect(w, "error") - return - } view := DownloadView{ Name: file.Name, Size: file.Size, @@ -176,6 +178,20 @@ func showDownload(w http.ResponseWriter, r *http.Request) { helper.Check(err) } +// Handling of /hotlink/ +// Hotlinks an image or returns a static error image if image has expired +func showHotlink(w http.ResponseWriter, r *http.Request) { + hotlinkId := strings.Replace(r.URL.Path, "/hotlink/", "", 1) + file, ok := storage.GetFileByHotlink(hotlinkId) + if !ok { + w.Header().Set("Content-Type", r.Header.Get("Content-Type")) + _, err := w.Write(imageExpiredPicture) + helper.Check(err) + return + } + storage.ServeFile(file, w, r, false) +} + // Handling of /delete // User needs to be admin. Deleted the requested file func deleteFile(w http.ResponseWriter, r *http.Request) { @@ -227,6 +243,7 @@ type DownloadView struct { type UploadView struct { Items []filestructure.File Url string + HotlinkUrl string TimeNow int64 DefaultDownloads int DefaultExpiry int @@ -244,6 +261,7 @@ func (u *UploadView) convertGlobalConfig() *UploadView { return result[i].ExpireAt > result[j].ExpireAt }) u.Url = configuration.ServerSettings.ServerUrl + "d?id=" + u.HotlinkUrl = configuration.ServerSettings.ServerUrl + "hotlink/" u.DefaultPassword = configuration.ServerSettings.DefaultPassword u.Items = result u.DefaultExpiry = configuration.ServerSettings.DefaultExpiry @@ -295,11 +313,8 @@ 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 == "" { - return - } - savedFile := configuration.ServerSettings.Files[keyId] - if savedFile.DownloadsRemaining == 0 || savedFile.ExpireAt < time.Now().Unix() || !helper.FileExists(configuration.ServerSettings.DataDir+"/"+savedFile.SHA256) { + savedFile, ok := storage.GetFile(keyId) + if !ok { redirect(w, "error") return } @@ -309,17 +324,7 @@ func downloadFile(w http.ResponseWriter, r *http.Request) { return } } - savedFile.DownloadsRemaining = savedFile.DownloadsRemaining - 1 - configuration.ServerSettings.Files[keyId] = savedFile - configuration.Save() - - w.Header().Set("Content-Disposition", "attachment; filename=\""+savedFile.Name+"\"") - w.Header().Set("Content-Type", r.Header.Get("Content-Type")) - file, err := os.OpenFile(configuration.ServerSettings.DataDir+"/"+savedFile.SHA256, os.O_RDONLY, 0644) - defer file.Close() - helper.Check(err) - _, err = io.Copy(w, file) - helper.Check(err) + storage.ServeFile(savedFile, w, r, true) } // Checks if the user is logged in as an admin diff --git a/static/expired.png b/static/expired.png new file mode 100644 index 0000000000000000000000000000000000000000..f2c515cb6e6e7b47c5fbaa18cd04739d6fc59a7b GIT binary patch literal 5774 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 literal 0 HcmV?d00001 diff --git a/static/js/admin.js b/static/js/admin.js index ca40c4c..6116930 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -57,7 +57,16 @@ function addRow(jsonText) { cell3.innerText = item.DownloadsRemaining; cell4.innerText = item.ExpireAtString; cell5.innerHTML = ''+jsonObject.Url+item.Id+''+lockIcon; - cell6.innerHTML = " "; + + let buttons = " "; + if (item.HotlinkId != "") { + buttons = buttons + ' '; + } else { + buttons = buttons + ' '; + } + buttons = buttons + ""; + + cell6.innerHTML = buttons; cell1.style.backgroundColor="green" cell2.style.backgroundColor="green" cell3.style.backgroundColor="green" diff --git a/templates/html_admin.tmpl b/templates/html_admin.tmpl index dbeaecb..c6d59b2 100644 --- a/templates/html_admin.tmpl +++ b/templates/html_admin.tmpl @@ -52,7 +52,12 @@ {{ .DownloadsRemaining }} {{ .ExpireAtString }} {{ $.Url }}{{ .Id }}{{ if ne .PasswordHash "" }} 🔒{{ end }} - + +{{ if ne .HotlinkId "" }} + +{{ else }} + +{{ end }} {{ end }} {{ end }} diff --git a/templates/string_constants.tmpl b/templates/string_constants.tmpl index 96d3306..51c135c 100644 --- a/templates/string_constants.tmpl +++ b/templates/string_constants.tmpl @@ -1,2 +1,2 @@ {{define "app_name"}}Gokapi{{end}} -{{define "version"}}1.1.2{{end}} +{{define "version"}}1.1.3-dev{{end}}