diff --git a/build/generateCoverage.sh b/build/generateCoverage.sh index 75c5d06..7a099c4 100644 --- a/build/generateCoverage.sh +++ b/build/generateCoverage.sh @@ -1,3 +1,3 @@ #!/bin/sh cd .. -go test ./... -coverprofile=/tmp/coverage.out --tags=test && go tool cover -html=/tmp/coverage.out +go test ./... -parallel 8 -coverprofile=/tmp/coverage.out --tags=test && go tool cover -html=/tmp/coverage.out diff --git a/internal/configuration/Configuration.go b/internal/configuration/Configuration.go index c9b3d5f..c5d562d 100644 --- a/internal/configuration/Configuration.go +++ b/internal/configuration/Configuration.go @@ -34,26 +34,27 @@ var Environment environment.Environment var ServerSettings Configuration // Version of the configuration structure. Used for upgrading -const currentConfigVersion = 4 +const currentConfigVersion = 5 // 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]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"` - LengthId int `json:"LengthId"` - DataDir string `json:"DataDir"` + 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]sessionstructure.Session `json:"Sessions"` + Files map[string]filestructure.File `json:"Files"` + Hotlinks map[string]filestructure.Hotlink `json:"Hotlinks"` + DownloadStatus map[string]filestructure.DownloadStatus `json:"DownloadStatus"` + ConfigVersion int `json:"ConfigVersion"` + SaltAdmin string `json:"SaltAdmin"` + SaltFiles string `json:"SaltFiles"` + LengthId int `json:"LengthId"` + DataDir string `json:"DataDir"` } // Load loads the configuration or creates the folder structure and a default configuration @@ -92,6 +93,16 @@ func updateConfig() { ServerSettings.Hotlinks = make(map[string]filestructure.Hotlink) } + // < v1.1.4 + if ServerSettings.ConfigVersion < 5 { + ServerSettings.LengthId = 15 + ServerSettings.DownloadStatus = make(map[string]filestructure.DownloadStatus) + for _, file := range ServerSettings.Files { + file.ContentType = "application/octet-stream" + ServerSettings.Files[file.Id] = file + } + } + if ServerSettings.ConfigVersion < currentConfigVersion { fmt.Println("Successfully upgraded database") ServerSettings.ConfigVersion = currentConfigVersion diff --git a/internal/configuration/downloadStatus/DownloadStatus.go b/internal/configuration/downloadStatus/DownloadStatus.go new file mode 100644 index 0000000..fdaa6b9 --- /dev/null +++ b/internal/configuration/downloadStatus/DownloadStatus.go @@ -0,0 +1,52 @@ +package downloadStatus + +import ( + "Gokapi/internal/configuration" + "Gokapi/internal/helper" + "Gokapi/internal/storage/filestructure" + "time" +) + +// SetDownload creates a new DownloadStatus struct and returns its Id +func SetDownload(file filestructure.File) string { + status := newDownloadStatus(file) + configuration.ServerSettings.DownloadStatus[status.Id] = status + return status.Id +} + +// SetComplete removes the download object +func SetComplete(id string) { + delete(configuration.ServerSettings.DownloadStatus, id) +} + +// Clean removes all expires status objects +func Clean() { + now := time.Now().Unix() + for _, item := range configuration.ServerSettings.DownloadStatus { + if item.ExpireAt < now { + delete(configuration.ServerSettings.DownloadStatus, item.Id) + } + } +} + +// newDownloadStatus initialises the a new DownloadStatus item +func newDownloadStatus(file filestructure.File) filestructure.DownloadStatus { + s := filestructure.DownloadStatus{ + Id: helper.GenerateRandomString(30), + FileId: file.Id, + ExpireAt: time.Now().Add(24 * time.Hour).Unix(), + } + return s +} + +// IsCurrentlyDownloading returns true if file is currently being downloaded +func IsCurrentlyDownloading(file filestructure.File) bool { + for _, status := range configuration.ServerSettings.DownloadStatus { + if status.FileId == file.Id { + if status.ExpireAt > time.Now().Unix() { + return true + } + } + } + return false +} diff --git a/internal/configuration/downloadStatus/DownloadStatus_test.go b/internal/configuration/downloadStatus/DownloadStatus_test.go new file mode 100644 index 0000000..0d05706 --- /dev/null +++ b/internal/configuration/downloadStatus/DownloadStatus_test.go @@ -0,0 +1,70 @@ +package downloadStatus + +import ( + "Gokapi/internal/configuration" + "Gokapi/internal/storage/filestructure" + "Gokapi/pkg/test" + "os" + "testing" + "time" +) + +var testFile filestructure.File +var statusId string + +func TestMain(m *testing.M) { + configuration.ServerSettings.DownloadStatus = make(map[string]filestructure.DownloadStatus) + testFile = filestructure.File{ + Id: "test", + Name: "testName", + Size: "3 B", + SHA256: "123456", + ExpireAt: 500, + ExpireAtString: "expire", + DownloadsRemaining: 1, + } + exitVal := m.Run() + os.Exit(exitVal) +} + +func TestNewDownloadStatus(t *testing.T) { + status := newDownloadStatus(filestructure.File{Id: "testId"}) + test.IsNotEmpty(t, status.Id) + test.IsEqualString(t, status.FileId, "testId") + test.IsEqualBool(t, status.ExpireAt > time.Now().Unix(), true) +} + +func TestSetDownload(t *testing.T) { + statusId = SetDownload(testFile) + status := configuration.ServerSettings.DownloadStatus[statusId] + test.IsNotEmpty(t, status.Id) + test.IsEqualString(t, status.Id, statusId) + test.IsEqualString(t, status.FileId, testFile.Id) + test.IsEqualBool(t, status.ExpireAt > time.Now().Unix(), true) +} + +func TestSetComplete(t *testing.T) { + status := configuration.ServerSettings.DownloadStatus[statusId] + test.IsNotEmpty(t, status.Id) + SetComplete(statusId) + status = configuration.ServerSettings.DownloadStatus[statusId] + test.IsEmpty(t, status.Id) +} + +func TestIsCurrentlyDownloading(t *testing.T) { + statusId = SetDownload(testFile) + test.IsEqualBool(t, IsCurrentlyDownloading(testFile), true) + test.IsEqualBool(t, IsCurrentlyDownloading(filestructure.File{Id: "notDownloading"}), false) +} + +func TestClean(t *testing.T) { + test.IsEqualInt(t, len(configuration.ServerSettings.DownloadStatus), 1) + Clean() + test.IsEqualInt(t, len(configuration.ServerSettings.DownloadStatus), 1) + status := configuration.ServerSettings.DownloadStatus[statusId] + status.ExpireAt = 1 + configuration.ServerSettings.DownloadStatus[statusId] = status + test.IsEqualInt(t, len(configuration.ServerSettings.DownloadStatus), 1) + Clean() + test.IsEqualInt(t, len(configuration.ServerSettings.DownloadStatus), 0) +} diff --git a/internal/storage/FileServing.go b/internal/storage/FileServing.go index 3a4b699..b458d32 100644 --- a/internal/storage/FileServing.go +++ b/internal/storage/FileServing.go @@ -6,12 +6,12 @@ Serving and processing uploaded files import ( "Gokapi/internal/configuration" + "Gokapi/internal/configuration/downloadStatus" "Gokapi/internal/helper" "Gokapi/internal/storage/filestructure" "crypto/sha1" "encoding/hex" "fmt" - "io" "io/ioutil" "mime/multipart" "net/http" @@ -47,6 +47,7 @@ func processUpload(fileContent *[]byte, fileHeader *multipart.FileHeader, expire ExpireAtString: time.Unix(expireAt, 0).Format("2006-01-02 15:04"), DownloadsRemaining: downloads, PasswordHash: configuration.HashPassword(password, true), + ContentType: fileHeader.Header.Get("Content-Type"), } addHotlink(&file) configuration.ServerSettings.Files[id] = file @@ -111,33 +112,33 @@ func GetFileByHotlink(id string) (filestructure.File, bool) { 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) helper.Check(err) defer storageData.Close() size, err := helper.GetFileSize(storageData) - if err == nil { - w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) - } helper.Check(err) - _, _ = io.Copy(w, storageData) + if forceDownload { + w.Header().Set("Content-Disposition", "attachment; filename=\""+file.Name+"\"") + } + w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) + w.Header().Set("Content-Type", file.ContentType) + statusId := downloadStatus.SetDownload(file) + configuration.Save() + http.ServeContent(w, r, file.Name, time.Now(), storageData) + downloadStatus.SetComplete(statusId) + configuration.Save() } // CleanUp 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 CleanUp(periodic bool) { + downloadStatus.Clean() timeNow := time.Now().Unix() wasItemDeleted := false for key, element := range configuration.ServerSettings.Files { fileExists := helper.FileExists(configuration.ServerSettings.DataDir + "/" + element.SHA256) - if element.ExpireAt < timeNow || element.DownloadsRemaining < 1 || !fileExists { + if (element.ExpireAt < timeNow || element.DownloadsRemaining < 1 || !fileExists) && !downloadStatus.IsCurrentlyDownloading(element) { deleteFile := true for _, secondLoopElement := range configuration.ServerSettings.Files { if element.Id != secondLoopElement.Id && element.SHA256 == secondLoopElement.SHA256 { diff --git a/internal/storage/FileServing_test.go b/internal/storage/FileServing_test.go index 06aa1eb..b54305c 100644 --- a/internal/storage/FileServing_test.go +++ b/internal/storage/FileServing_test.go @@ -100,6 +100,7 @@ func TestServeFile(t *testing.T) { test.IsEqualString(t, w.Result().Header.Get("Content-Disposition"), "attachment; filename=\"test.dat\"") test.IsEqualString(t, w.Result().Header.Get("Content-Length"), "35") + test.IsEqualString(t, w.Result().Header.Get("Content-Type"), "text") content, err := ioutil.ReadAll(w.Result().Body) test.IsNil(t, err) test.IsEqualString(t, string(content), "This is a file for testing purposes") @@ -107,13 +108,17 @@ func TestServeFile(t *testing.T) { } func TestCleanUp(t *testing.T) { + test.IsEqualString(t, configuration.ServerSettings.Files["cleanuptest123456789"].Name, "cleanup") test.IsEqualString(t, configuration.ServerSettings.Files["Wzol7LyY2QVczXynJtVo"].Name, "smallfile2") test.IsEqualString(t, configuration.ServerSettings.Files["e4TjE7CokWK0giiLNxDL"].Name, "smallfile2") test.IsEqualString(t, configuration.ServerSettings.Files["wefffewhtrhhtrhtrhtr"].Name, "smallfile3") test.IsEqualString(t, configuration.ServerSettings.Files["n1tSTAGj8zan9KaT4u6p"].Name, "picture.jpg") test.IsEqualString(t, configuration.ServerSettings.Files["deletedfile123456789"].Name, "DeletedFile") + test.IsEqualBool(t, helper.FileExists("test/data/2341354656543213246465465465432456898794"), true) CleanUp(false) + test.IsEqualString(t, configuration.ServerSettings.Files["cleanuptest123456789"].Name, "cleanup") + test.IsEqualBool(t, helper.FileExists("test/data/2341354656543213246465465465432456898794"), true) test.IsEqualString(t, configuration.ServerSettings.Files["deletedfile123456789"].Name, "") test.IsEqualString(t, configuration.ServerSettings.Files["Wzol7LyY2QVczXynJtVo"].Name, "smallfile2") test.IsEqualString(t, configuration.ServerSettings.Files["e4TjE7CokWK0giiLNxDL"].Name, "smallfile2") @@ -159,4 +164,10 @@ func TestCleanUp(t *testing.T) { test.IsEqualString(t, configuration.ServerSettings.Files["e4TjE7CokWK0giiLNxDL"].Name, "") test.IsEqualString(t, configuration.ServerSettings.Files["wefffewhtrhhtrhtrhtr"].Name, "") + test.IsEqualString(t, configuration.ServerSettings.Files["cleanuptest123456789"].Name, "cleanup") + test.IsEqualBool(t, helper.FileExists("test/data/2341354656543213246465465465432456898794"), true) + configuration.ServerSettings.DownloadStatus = make(map[string]filestructure.DownloadStatus) + CleanUp(false) + test.IsEqualString(t, configuration.ServerSettings.Files["cleanuptest123456789"].Name, "") + test.IsEqualBool(t, helper.FileExists("test/data/2341354656543213246465465465432456898794"), false) } diff --git a/internal/storage/filestructure/FileList.go b/internal/storage/filestructure/FileList.go index 849f7b4..dde59b2 100644 --- a/internal/storage/filestructure/FileList.go +++ b/internal/storage/filestructure/FileList.go @@ -16,6 +16,7 @@ type File struct { DownloadsRemaining int `json:"DownloadsRemaining"` PasswordHash string `json:"PasswordHash"` HotlinkId string `json:"HotlinkId"` + ContentType string `json:"ContentType"` } // Hotlink is a struct containing hotlink ids @@ -47,3 +48,11 @@ type Result struct { Url string `json:"Url"` HotlinkUrl string `json:"HotlinkUrl"` } + + +// DownloadStatus contains current downloads, so they do not get removed during cleanup +type DownloadStatus struct { + Id string + FileId string + ExpireAt int64 +} diff --git a/internal/storage/filestructure/FileList_test.go b/internal/storage/filestructure/FileList_test.go index 553535b..6e712b4 100644 --- a/internal/storage/filestructure/FileList_test.go +++ b/internal/storage/filestructure/FileList_test.go @@ -16,6 +16,7 @@ func TestToJsonResult(t *testing.T) { DownloadsRemaining: 1, PasswordHash: "pwhash", HotlinkId: "hotlinkid", + ContentType: "test/html", } - test.IsEqualString(t, file.ToJsonResult("serverurl/"), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","SHA256":"sha256","ExpireAt":50,"ExpireAtString":"future","DownloadsRemaining":1,"PasswordHash":"pwhash","HotlinkId":"hotlinkid"},"Url":"serverurl/d?id=","HotlinkUrl":"serverurl/hotlink/"}`) + test.IsEqualString(t, file.ToJsonResult("serverurl/"), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","SHA256":"sha256","ExpireAt":50,"ExpireAtString":"future","DownloadsRemaining":1,"PasswordHash":"pwhash","HotlinkId":"hotlinkid","ContentType":"test/html"},"Url":"serverurl/d?id=","HotlinkUrl":"serverurl/hotlink/"}`) } diff --git a/internal/test/TestConfiguration.go b/internal/test/TestConfiguration.go index c6686a8..45dbdb0 100644 --- a/internal/test/TestConfiguration.go +++ b/internal/test/TestConfiguration.go @@ -18,6 +18,7 @@ func Create(initFiles bool) { os.WriteFile("test/data/a8fdc205a9f19cc1c7507a60c4f01b13d11d7fd0", []byte("123"), 0777) os.WriteFile("test/data/c4f9375f9834b4e7f0a528cc65c055702bf5f24a", []byte("456"), 0777) os.WriteFile("test/data/e017693e4a04a59d0b0f400fe98177fe7ee13cf7", []byte("789"), 0777) + os.WriteFile("test/data/2341354656543213246465465465432456898794", []byte("abc"), 0777) os.WriteFile("test/fileupload.jpg", []byte("abc"), 0777) } } @@ -64,6 +65,10 @@ var configTestFile = []byte(`{ "RenewAt":2147483645, "ValidUntil":2147483646 }, + "logoutsession":{ + "RenewAt":2147483645, + "ValidUntil":2147483646 + }, "needsRenewal":{ "RenewAt":0, "ValidUntil":2147483646 @@ -83,6 +88,7 @@ var configTestFile = []byte(`{ "ExpireAtString":"2021-05-04 15:19", "DownloadsRemaining":1, "PasswordHash":"", + "ContentType":"text/html", "HotlinkId":"" }, "e4TjE7CokWK0giiLNxDL":{ @@ -94,6 +100,7 @@ var configTestFile = []byte(`{ "ExpireAtString":"2021-05-04 15:19", "DownloadsRemaining":2, "PasswordHash":"", + "ContentType":"text/html", "HotlinkId":"" }, "wefffewhtrhhtrhtrhtr":{ @@ -105,6 +112,7 @@ var configTestFile = []byte(`{ "ExpireAtString":"2021-05-04 15:19", "DownloadsRemaining":1, "PasswordHash":"", + "ContentType":"text/html", "HotlinkId":"" }, "deletedfile123456789":{ @@ -116,6 +124,7 @@ var configTestFile = []byte(`{ "ExpireAtString":"2021-05-04 15:19", "DownloadsRemaining":2, "PasswordHash":"", + "ContentType":"text/html", "HotlinkId":"" }, "jpLXGJKigM4hjtA6T6sN":{ @@ -126,6 +135,19 @@ var configTestFile = []byte(`{ "ExpireAt":2147483646, "ExpireAtString":"2021-05-04 15:18", "DownloadsRemaining":1, + "ContentType":"text/html", + "PasswordHash":"7b30508aa9b233ab4b8a11b2af5816bdb58ca3e7", + "HotlinkId":"" + }, + "jpLXGJKigM4hjtA6T6sN2":{ + "Id":"jpLXGJKigM4hjtA6T6sN2", + "Name":"smallfile", + "Size":"7 B", + "SHA256":"c4f9375f9834b4e7f0a528cc65c055702bf5f24a", + "ExpireAt":2147483646, + "ExpireAtString":"2021-05-04 15:18", + "DownloadsRemaining":1, + "ContentType":"text/html", "PasswordHash":"7b30508aa9b233ab4b8a11b2af5816bdb58ca3e7", "HotlinkId":"" }, @@ -138,7 +160,20 @@ var configTestFile = []byte(`{ "ExpireAtString":"2021-05-04 15:19", "DownloadsRemaining":1, "PasswordHash":"", + "ContentType":"text/html", "HotlinkId":"PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg" + }, + "cleanuptest123456789":{ + "Id":"cleanuptest123456789", + "Name":"cleanup", + "Size":"4 B", + "SHA256":"2341354656543213246465465465432456898794", + "ExpireAt":2147483646, + "ExpireAtString":"2021-05-04 15:19", + "DownloadsRemaining":0, + "PasswordHash":"", + "ContentType":"text/html", + "HotlinkId":"" } }, "Hotlinks":{ @@ -147,7 +182,14 @@ var configTestFile = []byte(`{ "FileId":"n1tSTAGj8zan9KaT4u6p" } }, - "ConfigVersion":4, + "DownloadStatus":{ + "69JCbLVxx2KxfvB6FYkrDn3oCU7BWT":{ + "Id":"69JCbLVxx2KxfvB6FYkrDn3oCU7BWT", + "FileId":"cleanuptest123456789", + "ExpireAt":2147483646 + } + }, + "ConfigVersion":5, "SaltAdmin":"LW6fW4Pjv8GtdWVLSZD66gYEev6NAaXxOVBw7C", "SaltFiles":"lL5wMTtnVCn5TPbpRaSe4vAQodWW0hgk00WCZE", "LengthId":20, diff --git a/internal/webserver/Webserver.go b/internal/webserver/Webserver.go index 4ea072a..7962143 100644 --- a/internal/webserver/Webserver.go +++ b/internal/webserver/Webserver.go @@ -34,7 +34,7 @@ var staticFolderEmbedded embed.FS //go:embed web/templates var templateFolderEmbedded embed.FS -const timeOutWebserver = 2 * time.Hour +const timeOutWebserver = 12 * time.Hour // Variable containing all parsed templates var templateFolder *template.Template @@ -74,7 +74,7 @@ func Start() { srv := &http.Server{ Addr: configuration.ServerSettings.Port, ReadTimeout: timeOutWebserver, - WriteTimeout: 10 * time.Second, + WriteTimeout: timeOutWebserver, } log.Fatal(srv.ListenAndServe()) } diff --git a/internal/webserver/Webserver_test.go b/internal/webserver/Webserver_test.go index 23bd0e4..02ff38a 100644 --- a/internal/webserver/Webserver_test.go +++ b/internal/webserver/Webserver_test.go @@ -14,6 +14,9 @@ import ( func TestMain(m *testing.M) { testconfiguration.Create(true) + configuration.Load() + go Start() + time.Sleep(1 * time.Second) exitVal := m.Run() testconfiguration.Delete() os.Exit(exitVal) @@ -33,64 +36,75 @@ func TestEmbedFs(t *testing.T) { } } -func TestWebserverEmbedFs(t *testing.T) { - configuration.Load() - go Start() - - time.Sleep(1 * time.Second) - // Index redirect +func TestIndexRedirect(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/", - RequiredContent: "", + RequiredContent: []string{""}, IsHtml: true, }) - // Index file +} +func TestIndexFile(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/index", - RequiredContent: configuration.ServerSettings.RedirectUrl, + RequiredContent: []string{configuration.ServerSettings.RedirectUrl}, IsHtml: true, }) - // CSS file +} +func TestStaticDirs(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/css/cover.css", - RequiredContent: ".btn-secondary:hover", + RequiredContent: []string{".btn-secondary:hover"}, }) - // Login page +} +func TestLogin(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/login", - RequiredContent: "id=\"uname_hidden\"", + RequiredContent: []string{"id=\"uname_hidden\""}, IsHtml: true, }) - // Admin without auth +} +func TestAdminNoAuth(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", - RequiredContent: "URL=./login\"", + RequiredContent: []string{"URL=./login\""}, IsHtml: true, }) - // Admin with auth +} +func TestAdminAuth(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", - RequiredContent: "Downloads remaining", + RequiredContent: []string{"Downloads remaining"}, IsHtml: true, Cookies: []test.Cookie{{ Name: "session_token", Value: "validsession", }}, }) - // Admin with expired session +} +func TestAdminExpiredAuth(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", - RequiredContent: "URL=./login\"", + RequiredContent: []string{"URL=./login\""}, IsHtml: true, Cookies: []test.Cookie{{ Name: "session_token", Value: "expiredsession", }}, }) - // Admin with auth needing renewal +} + +func TestAdminRenewalAuth(t *testing.T) { + t.Parallel() cookies := test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", - RequiredContent: "Downloads remaining", + RequiredContent: []string{"Downloads remaining"}, IsHtml: true, Cookies: []test.Cookie{{ Name: "session_token", @@ -109,124 +123,209 @@ func TestWebserverEmbedFs(t *testing.T) { } test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", - RequiredContent: "Downloads remaining", + RequiredContent: []string{"Downloads remaining"}, IsHtml: true, Cookies: []test.Cookie{{ Name: "session_token", Value: sessionCookie, }}, }) +} - // Admin with invalid auth +func TestAdminInvalidAuth(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", - RequiredContent: "URL=./login\"", + RequiredContent: []string{"URL=./login\""}, IsHtml: true, Cookies: []test.Cookie{{ Name: "session_token", Value: "invalid", }}, }) - // Invalid link +} + +func TestInvalidLink(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/d?id=123", - RequiredContent: "URL=./error\"", + RequiredContent: []string{"URL=./error\""}, IsHtml: true, }) - // Error +} +func TestError(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/error", - RequiredContent: "this file cannot be found", + RequiredContent: []string{"this file cannot be found"}, IsHtml: true, }) - // Forgot pw +} +func TestForgotPw(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/forgotpw", - RequiredContent: "--reset-pw", + RequiredContent: []string{"--reset-pw"}, IsHtml: true, }) - // Login correct +} +func TestLoginCorrect(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/login", - RequiredContent: "URL=./admin\"", + RequiredContent: []string{"URL=./admin\""}, IsHtml: true, Method: "POST", PostValues: []test.PostBody{{"username", "test"}, {"password", "testtest"}}, }) - // Login incorrect +} + +func TestLoginIncorrectPassword(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/login", - RequiredContent: "Incorrect username or password", + RequiredContent: []string{"Incorrect username or password"}, IsHtml: true, Method: "POST", PostValues: []test.PostBody{{"username", "test"}, {"password", "incorrect"}}, }) - // Login incorrect +} +func TestLoginIncorrectUsername(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/login", - RequiredContent: "Incorrect username or password", + RequiredContent: []string{"Incorrect username or password"}, IsHtml: true, Method: "POST", PostValues: []test.PostBody{{"username", "incorrect"}, {"password", "incorrect"}}, }) - // Download hotlink +} + +func TestLogout(t *testing.T) { + t.Parallel() + test.HttpPageResult(t, test.HttpTestConfig{ + Url: "http://localhost:53843/admin", + RequiredContent: []string{"Downloads remaining"}, + IsHtml: true, + Cookies: []test.Cookie{{ + Name: "session_token", + Value: "logoutsession", + }}, + }) + // Logout + test.HttpPageResult(t, test.HttpTestConfig{ + Url: "http://localhost:53843/logout", + RequiredContent: []string{"URL=./login\""}, + IsHtml: true, + Cookies: []test.Cookie{{ + Name: "session_token", + Value: "logoutsession", + }}, + }) + // Admin after logout + test.HttpPageResult(t, test.HttpTestConfig{ + Url: "http://localhost:53843/admin", + RequiredContent: []string{"URL=./login\""}, + IsHtml: true, + Cookies: []test.Cookie{{ + Name: "session_token", + Value: "logoutsession", + }}, + }) +} + +func TestDownloadHotlink(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/hotlink/PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg", - RequiredContent: "123", + RequiredContent: []string{"123"}, }) // Download expired hotlink test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/hotlink/PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg", - RequiredContent: "Created with GIMP", + RequiredContent: []string{"Created with GIMP"}, }) - // Show download page no password +} + +func TestDownloadNoPassword(t *testing.T) { + t.Parallel() + // Show download page test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=Wzol7LyY2QVczXynJtVo", IsHtml: true, - RequiredContent: "smallfile2", + RequiredContent: []string{"smallfile2"}, }) - // Download file no password + // Download test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/downloadFile?id=Wzol7LyY2QVczXynJtVo", - RequiredContent: "789", + RequiredContent: []string{"789"}, }) // Show download page expired file test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=Wzol7LyY2QVczXynJtVo", IsHtml: true, - RequiredContent: "URL=./error\"", + RequiredContent: []string{"URL=./error\""}, }) // Download expired file test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/downloadFile?id=Wzol7LyY2QVczXynJtVo", IsHtml: true, - RequiredContent: "URL=./error\"", + RequiredContent: []string{"URL=./error\""}, }) - // Show download page password +} + +func TestDownloadPagePassword(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN", IsHtml: true, - RequiredContent: "Password required", + RequiredContent: []string{"Password required"}, }) - // Show download page incorrect password +} +func TestDownloadPageIncorrectPassword(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN", IsHtml: true, - RequiredContent: "Incorrect password!", + RequiredContent: []string{"Incorrect password!"}, Method: "POST", PostValues: []test.PostBody{{"password", "incorrect"}}, }) - // Submit download page correct password - cookies = test.HttpPageResult(t, test.HttpTestConfig{ +} + +func TestDownloadIncorrectPasswordCookie(t *testing.T) { + t.Parallel() + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN", IsHtml: true, - RequiredContent: "URL=./d?id=jpLXGJKigM4hjtA6T6sN", + RequiredContent: []string{"Password required"}, + Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN", "invalid"}}, + }) +} + +func TestDownloadIncorrectPassword(t *testing.T) { + t.Parallel() + test.HttpPageResult(t, test.HttpTestConfig{ + Url: "http://127.0.0.1:53843/downloadFile?id=jpLXGJKigM4hjtA6T6sN", + IsHtml: true, + RequiredContent: []string{"URL=./d?id=jpLXGJKigM4hjtA6T6sN"}, + Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN", "invalid"}}, + }) +} + +func TestDownloadCorrectPassword(t *testing.T) { + t.Parallel() + // Submit download page correct password + cookies := test.HttpPageResult(t, test.HttpTestConfig{ + Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN2", + IsHtml: true, + RequiredContent: []string{"URL=./d?id=jpLXGJKigM4hjtA6T6sN2"}, Method: "POST", PostValues: []test.PostBody{{"password", "123"}}, }) pwCookie := "" for _, cookie := range cookies { - if (*cookie).Name == "pjpLXGJKigM4hjtA6T6sN" { + if (*cookie).Name == "pjpLXGJKigM4hjtA6T6sN2" { pwCookie = (*cookie).Value break } @@ -236,79 +335,79 @@ func TestWebserverEmbedFs(t *testing.T) { } // Show download page correct password test.HttpPageResult(t, test.HttpTestConfig{ - Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN", + Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN2", IsHtml: true, - RequiredContent: "smallfile", - Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN", pwCookie}}, - }) - // Show download page incorrect password cookie - test.HttpPageResult(t, test.HttpTestConfig{ - Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN", - IsHtml: true, - RequiredContent: "Password required", - Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN", "invalid"}}, - }) - // Download incorrect password - test.HttpPageResult(t, test.HttpTestConfig{ - Url: "http://127.0.0.1:53843/downloadFile?id=jpLXGJKigM4hjtA6T6sN", - IsHtml: true, - RequiredContent: "URL=./d?id=jpLXGJKigM4hjtA6T6sN", - Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN", "invalid"}}, + RequiredContent: []string{"smallfile"}, + Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN2", pwCookie}}, }) // Download correct password test.HttpPageResult(t, test.HttpTestConfig{ - Url: "http://127.0.0.1:53843/downloadFile?id=jpLXGJKigM4hjtA6T6sN", - RequiredContent: "456", - Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN", pwCookie}}, + Url: "http://127.0.0.1:53843/downloadFile?id=jpLXGJKigM4hjtA6T6sN2", + RequiredContent: []string{"456"}, + Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN2", pwCookie}}, }) - // Delete file non-auth +} + +func TestDeleteFileNonAuth(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/delete?id=e4TjE7CokWK0giiLNxDL", IsHtml: true, - RequiredContent: "URL=./login", + RequiredContent: []string{"URL=./login"}, }) - // Delete file authorised - test.HttpPageResult(t, test.HttpTestConfig{ - Url: "http://127.0.0.1:53843/delete?id=e4TjE7CokWK0giiLNxDL", - IsHtml: true, - RequiredContent: "URL=./admin", - Cookies: []test.Cookie{{ - Name: "session_token", - Value: "validsession", - }}, - }) - // Delete file authorised, invalid key +} + +func TestDeleteFileInvalidKey(t *testing.T) { + t.Parallel() test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/delete", IsHtml: true, - RequiredContent: "URL=./admin", + RequiredContent: []string{"URL=./admin"}, Cookies: []test.Cookie{{ Name: "session_token", Value: "validsession", }}, }) - // Post upload unauthorized - test.HttpPostRequest(t, "http://127.0.0.1:53843/upload", "test/fileupload.jpg", "file", "{\"Result\":\"error\",\"ErrorMessage\":\"Not authenticated\"}", []test.Cookie{}) - // Post upload authorized - test.HttpPostRequest(t, "http://127.0.0.1:53843/upload", "test/fileupload.jpg", "file", "fileupload.jpg", []test.Cookie{{ - Name: "session_token", - Value: "validsession", - }}) - // Logout test.HttpPageResult(t, test.HttpTestConfig{ - Url: "http://localhost:53843/logout", - RequiredContent: "URL=./login\"", - IsHtml: true, - Cookies: []test.Cookie{{ - Name: "session_token", - Value: "validsession", - }}, - }) - // Admin after logout - test.HttpPageResult(t, test.HttpTestConfig{ - Url: "http://localhost:53843/admin", - RequiredContent: "URL=./login\"", + Url: "http://127.0.0.1:53843/delete?id=", IsHtml: true, + RequiredContent: []string{"URL=./admin"}, + Cookies: []test.Cookie{{ + Name: "session_token", + Value: "validsession", + }}, + }) +} + +func TestDeleteFile(t *testing.T) { + t.Parallel() + test.HttpPageResult(t, test.HttpTestConfig{ + Url: "http://127.0.0.1:53843/delete?id=e4TjE7CokWK0giiLNxDL", + IsHtml: true, + RequiredContent: []string{"URL=./admin"}, + Cookies: []test.Cookie{{ + Name: "session_token", + Value: "validsession", + }}, + }) +} + +func TestPostUploadNoAuth(t *testing.T) { + t.Parallel() + test.HttpPostRequest(t, test.HttpTestConfig{ + Url: "http://127.0.0.1:53843/upload", + UploadFileName: "test/fileupload.jpg", + UploadFieldName: "file", + RequiredContent: []string{"{\"Result\":\"error\",\"ErrorMessage\":\"Not authenticated\"}"}, + }) +} +func TestPostUpload(t *testing.T) { + test.HttpPostRequest(t, test.HttpTestConfig{ + Url: "http://127.0.0.1:53843/upload", + UploadFileName: "test/fileupload.jpg", + UploadFieldName: "file", + RequiredContent: []string{"{\"Result\":\"OK\"", "\"Name\":\"fileupload.jpg\"", "\"SHA256\":\"a9993e364706816aba3e25717850c26c9cd0d89d\"", "DownloadsRemaining\":3"}, + ExcludedContent: []string{"\"Id\":\"\"", "HotlinkId\":\"\""}, Cookies: []test.Cookie{{ Name: "session_token", Value: "validsession", diff --git a/pkg/test/TestHelper.go b/pkg/test/TestHelper.go index d468984..fe10171 100644 --- a/pkg/test/TestHelper.go +++ b/pkg/test/TestHelper.go @@ -36,6 +36,20 @@ func IsEqualInt(t *testing.T, got, want int) { } } +// IsNotEmpty fails test if string is empty +func IsNotEmpty(t *testing.T, s string) { + if s == "" { + t.Errorf("Assertion failed, got: %s, want: empty.", s) + } +} + +// IsEmpty fails test if string is not empty +func IsEmpty(t *testing.T, s string) { + if s != "" { + t.Errorf("Assertion failed, got: %s, want: empty.", s) + } +} + // IsNil fails test if error not nil func IsNil(t *testing.T, got error) { if got != nil { @@ -44,22 +58,22 @@ func IsNil(t *testing.T, got error) { } // HttpPageResult tests if a http server is outputting the correct result -func HttpPageResult(t *testing.T, configuration HttpTestConfig) []*http.Cookie { - configuration.init() +func HttpPageResult(t *testing.T, config HttpTestConfig) []*http.Cookie { + config.init() client := &http.Client{} data := url.Values{} - for _, value := range configuration.PostValues { + for _, value := range config.PostValues { data.Add(value.Key, value.Value) } - req, err := http.NewRequest(configuration.Method, configuration.Url, strings.NewReader(data.Encode())) + req, err := http.NewRequest(config.Method, config.Url, strings.NewReader(data.Encode())) IsNil(t, err) - for _, cookie := range configuration.Cookies { + for _, cookie := range config.Cookies { req.Header.Set("Cookie", cookie.toString()) } - if len(configuration.PostValues) > 0 { + if len(config.PostValues) > 0 { req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) } @@ -69,13 +83,20 @@ func HttpPageResult(t *testing.T, configuration HttpTestConfig) []*http.Cookie { if resp.StatusCode != 200 { t.Errorf("Status %d != 200", resp.StatusCode) } - bs, err := ioutil.ReadAll(resp.Body) + content, err := ioutil.ReadAll(resp.Body) IsNil(t, err) - if configuration.IsHtml && !bytes.Contains(bs, []byte("")) { - t.Error(configuration.Url + ": Incorrect response") + if config.IsHtml && !bytes.Contains(content, []byte("")) { + t.Error(config.Url + ": Incorrect response") } - if configuration.RequiredContent != "" && !bytes.Contains(bs, []byte(configuration.RequiredContent)) { - t.Error(configuration.Url + ": Incorrect response. Got:\n" + string(bs)) + for _, requiredString := range config.RequiredContent { + if !bytes.Contains(content, []byte(requiredString)) { + t.Error(config.Url + ": Incorrect response. Got:\n" + string(content)) + } + } + for _, excludedString := range config.ExcludedContent { + if bytes.Contains(content, []byte(excludedString)) { + t.Error(config.Url + ": Incorrect response. Got:\n" + string(content)) + } } resp.Body.Close() return resp.Cookies() @@ -84,11 +105,14 @@ func HttpPageResult(t *testing.T, configuration HttpTestConfig) []*http.Cookie { // HttpTestConfig is a struct for http test init type HttpTestConfig struct { Url string - RequiredContent string + RequiredContent []string + ExcludedContent []string IsHtml bool Method string PostValues []PostBody Cookies []Cookie + UploadFileName string + UploadFieldName string } func (c *HttpTestConfig) init() { @@ -117,21 +141,21 @@ type PostBody struct { } // HttpPostRequest sends a post request -func HttpPostRequest(t *testing.T, url, filename, fieldName, requiredText string, cookies []Cookie) { - file, err := os.Open(filename) +func HttpPostRequest(t *testing.T, config HttpTestConfig) { + file, err := os.Open(config.UploadFileName) IsNil(t, err) defer file.Close() body := &bytes.Buffer{} writer := multipart.NewWriter(body) - part, err := writer.CreateFormFile(fieldName, filepath.Base(file.Name())) + part, err := writer.CreateFormFile(config.UploadFieldName, filepath.Base(file.Name())) IsNil(t, err) io.Copy(part, file) writer.Close() - request, err := http.NewRequest("POST", url, body) + request, err := http.NewRequest("POST", config.Url, body) IsNil(t, err) - for _, cookie := range cookies { + for _, cookie := range config.Cookies { request.Header.Set("Cookie", cookie.toString()) } request.Header.Add("Content-Type", writer.FormDataContentType()) @@ -144,7 +168,14 @@ func HttpPostRequest(t *testing.T, url, filename, fieldName, requiredText string content, err := ioutil.ReadAll(response.Body) IsNil(t, err) - if requiredText != "" && !bytes.Contains(content, []byte(requiredText)) { - t.Error(url + ": Incorrect response. Got:\n" + string(content)) + for _, requiredString := range config.RequiredContent { + if !bytes.Contains(content, []byte(requiredString)) { + t.Error(config.Url + ": Incorrect response. Got:\n" + string(content)) + } + } + for _, excludedString := range config.ExcludedContent { + if bytes.Contains(content, []byte(excludedString)) { + t.Error(config.Url + ": Incorrect response. Got:\n" + string(content)) + } } }