From c9ad7064fa3d88880e8f38304e22bc4a19827309 Mon Sep 17 00:00:00 2001 From: Marc Ole Bulling Date: Thu, 6 May 2021 12:35:07 +0200 Subject: [PATCH] Refactoring, early implementation of API --- internal/models/FileUpload.go | 9 ++ internal/storage/FileServing.go | 22 ++- internal/storage/FileServing_test.go | 14 +- internal/test/TestHelper_test.go | 24 +++- internal/webserver/Webserver.go | 42 ++---- internal/webserver/Webserver_test.go | 127 +++++++++--------- internal/webserver/api/Api.go | 97 +++++++++++++ internal/webserver/api/Api_test.go | 23 ++++ internal/webserver/fileupload/FileUpload.go | 73 ++++++++++ .../webserver/fileupload/FileUpload_test.go | 93 +++++++++++++ 10 files changed, 420 insertions(+), 104 deletions(-) create mode 100644 internal/models/FileUpload.go create mode 100644 internal/webserver/api/Api.go create mode 100644 internal/webserver/api/Api_test.go create mode 100644 internal/webserver/fileupload/FileUpload.go create mode 100644 internal/webserver/fileupload/FileUpload_test.go diff --git a/internal/models/FileUpload.go b/internal/models/FileUpload.go new file mode 100644 index 0000000..eac5f1e --- /dev/null +++ b/internal/models/FileUpload.go @@ -0,0 +1,9 @@ +package models + +type UploadRequest struct { + AllowedDownloads int + Expiry int + ExpiryTimestamp int64 + Password string + ExternalUrl string +} diff --git a/internal/storage/FileServing.go b/internal/storage/FileServing.go index 0189d5a..f98339a 100644 --- a/internal/storage/FileServing.go +++ b/internal/storage/FileServing.go @@ -26,7 +26,7 @@ import ( // NewFile 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 an ID and saves // it into the global configuration. -func NewFile(fileContent io.Reader, fileHeader *multipart.FileHeader, expireAt int64, downloads int, password string) (models.File, error) { +func NewFile(fileContent io.Reader, fileHeader *multipart.FileHeader, uploadRequest models.UploadRequest) (models.File, error) { fileBytes, err := ioutil.ReadAll(fileContent) if err != nil { return models.File{}, err @@ -39,10 +39,10 @@ func NewFile(fileContent io.Reader, fileHeader *multipart.FileHeader, expireAt i Name: fileHeader.Filename, SHA256: hex.EncodeToString(hash.Sum(nil)), Size: helper.ByteCountSI(fileHeader.Size), - ExpireAt: expireAt, - ExpireAtString: time.Unix(expireAt, 0).Format("2006-01-02 15:04"), - DownloadsRemaining: downloads, - PasswordHash: configuration.HashPassword(password, true), + ExpireAt: uploadRequest.ExpiryTimestamp, + ExpireAtString: time.Unix(uploadRequest.ExpiryTimestamp, 0).Format("2006-01-02 15:04"), + DownloadsRemaining: uploadRequest.AllowedDownloads, + PasswordHash: configuration.HashPassword(uploadRequest.Password, true), ContentType: fileHeader.Header.Get("Content-Type"), } addHotlink(&file) @@ -175,11 +175,19 @@ func CleanUp(periodic bool) { } // DeleteFile is called when an admin requests deletion of a file -func DeleteFile(keyId string) { +// Returns true if file was deleted or false if ID did not exist +func DeleteFile(keyId string) bool { + if keyId == "" { + return false + } settings := configuration.GetServerSettings() - item := settings.Files[keyId] + item, ok := settings.Files[keyId] + if !ok { + return false + } item.ExpireAt = 0 settings.Files[keyId] = item configuration.Release() CleanUp(false) + return true } diff --git a/internal/storage/FileServing_test.go b/internal/storage/FileServing_test.go index 2827dfa..a503911 100644 --- a/internal/storage/FileServing_test.go +++ b/internal/storage/FileServing_test.go @@ -78,7 +78,12 @@ func TestNewFile(t *testing.T) { Header: mimeHeader, Size: int64(len(content)), } - file, err := NewFile(bytes.NewReader(content), &header, 2147483600, 1, "") + request := models.UploadRequest{ + AllowedDownloads: 1, + Expiry: 999, + ExpiryTimestamp: 2147483600, + } + file, err := NewFile(bytes.NewReader(content), &header, request) test.IsNil(t, err) test.IsEqualString(t, file.Name, "test.dat") test.IsEqualString(t, file.SHA256, "f1474c19eff0fc8998fa6e1b1f7bf31793b103a6") @@ -183,7 +188,12 @@ func TestDeleteFile(t *testing.T) { configuration.Release() test.IsEqualString(t, settings.Files["n1tSTAGj8zan9KaT4u6p"].Name, "picture.jpg") test.IsEqualBool(t, helper.FileExists("test/data/a8fdc205a9f19cc1c7507a60c4f01b13d11d7fd0"), true) - DeleteFile("n1tSTAGj8zan9KaT4u6p") + result := DeleteFile("n1tSTAGj8zan9KaT4u6p") + test.IsEqualBool(t, result, true) test.IsEqualString(t, settings.Files["n1tSTAGj8zan9KaT4u6p"].Name, "") test.IsEqualBool(t, helper.FileExists("test/data/a8fdc205a9f19cc1c7507a60c4f01b13d11d7fd0"), false) + result = DeleteFile("invalid") + test.IsEqualBool(t, result, false) + result = DeleteFile("") + test.IsEqualBool(t, result, false) } diff --git a/internal/test/TestHelper_test.go b/internal/test/TestHelper_test.go index 10b6a98..83c3043 100644 --- a/internal/test/TestHelper_test.go +++ b/internal/test/TestHelper_test.go @@ -2,7 +2,7 @@ package test import ( "errors" - "fmt" + "io" "log" "net/http" "os" @@ -137,18 +137,34 @@ func TestHttpPostRequest(t *testing.T) { Value: "testValue", }}, }) + mockT := MockTest{reference: t} + mockT.WantFail() + HttpPostRequest(mockT, HttpTestConfig{ + Url: "http://127.0.0.1:9999/test", + UploadFileName: "testfile", + UploadFieldName: "file", + ExcludedContent: []string{"TestContent"}}, + ) + mockT.WantFail() + HttpPostRequest(mockT, HttpTestConfig{ + Url: "http://127.0.0.1:9999/test", + UploadFileName: "testfile", + UploadFieldName: "file", + RequiredContent: []string{"invalid"}}, + ) + mockT.Check() os.Remove("testfile") } func startTestServer() { http.HandleFunc("/test", func(writer http.ResponseWriter, request *http.Request) { - fmt.Fprint(writer, "TestContent\n") + io.WriteString(writer, "TestContent\n") for _, cookie := range request.Cookies() { - fmt.Fprint(writer, "cookie name: "+cookie.Name+" cookie value: "+cookie.Value+"\n") + io.WriteString(writer, "cookie name: "+cookie.Name+" cookie value: "+cookie.Value+"\n") } request.ParseForm() if request.Form.Get("testPostKey") != "" { - fmt.Fprint(writer, "testPostKey: "+request.Form.Get("testPostKey")+"\n") + io.WriteString(writer, "testPostKey: "+request.Form.Get("testPostKey")+"\n") } }) go func() { log.Fatal(http.ListenAndServe("127.0.0.1:9999", nil)) }() diff --git a/internal/webserver/Webserver.go b/internal/webserver/Webserver.go index 9eecd54..6031650 100644 --- a/internal/webserver/Webserver.go +++ b/internal/webserver/Webserver.go @@ -9,16 +9,18 @@ import ( "Gokapi/internal/helper" "Gokapi/internal/models" "Gokapi/internal/storage" + "Gokapi/internal/webserver/api" + "Gokapi/internal/webserver/fileupload" "Gokapi/internal/webserver/sessionmanager" "embed" "fmt" "html/template" + "io" "io/fs" "log" "net/http" "os" "sort" - "strconv" "strings" "time" ) @@ -79,6 +81,7 @@ func Start() { http.HandleFunc("/delete", deleteFile) http.HandleFunc("/downloadFile", downloadFile) http.HandleFunc("/forgotpw", forgotPassword) + http.HandleFunc("/api/", processApi) fmt.Println("Binding webserver to " + webserverPort) fmt.Println("Webserver can be accessed at " + webserverExtUrl + "admin") srv := &http.Server{ @@ -116,7 +119,7 @@ func initTemplates(templateFolderEmbedded embed.FS) { // Sends a redirect HTTP output to the client. Variable url is used to redirect to ./url func redirect(w http.ResponseWriter, url string) { - _, _ = fmt.Fprint(w, "") + _, _ = io.WriteString(w, "") } // Handling of /logout @@ -143,6 +146,11 @@ func forgotPassword(w http.ResponseWriter, r *http.Request) { helper.Check(err) } +// Handling of /api/ +func processApi(w http.ResponseWriter, r *http.Request) { + api.Process(w, r) +} + // 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 @@ -326,40 +334,18 @@ func (u *UploadView) convertGlobalConfig() *UploadView { // adds it to the system. func uploadFile(w http.ResponseWriter, r *http.Request) { addNoCacheHeader(w) + w.Header().Set("Content-Type", "application/json; charset=UTF-8") if !isAuthenticated(w, r, true) { return } - err := r.ParseMultipartForm(20 * 1024 * 1024) + err := fileupload.Process(w, r, true) responseError(w, err) - allowedDownloads := r.Form.Get("allowedDownloads") - expiryDays := r.Form.Get("expiryDays") - password := r.Form.Get("password") - allowedDownloadsInt, err := strconv.Atoi(allowedDownloads) - settings := configuration.GetServerSettings() - if err != nil { - allowedDownloadsInt = settings.DefaultDownloads - } - expiryDaysInt, err := strconv.Atoi(expiryDays) - if err != nil { - expiryDaysInt = settings.DefaultExpiry - } - settings.DefaultExpiry = expiryDaysInt - settings.DefaultDownloads = allowedDownloadsInt - settings.DefaultPassword = password - configuration.Release() - file, header, err := r.FormFile("file") - responseError(w, err) - result, err := storage.NewFile(file, header, time.Now().Add(time.Duration(expiryDaysInt)*time.Hour*24).Unix(), allowedDownloadsInt, password) - responseError(w, err) - defer file.Close() - _, err = fmt.Fprint(w, result.ToJsonResult(webserverExtUrl)) - helper.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()+"\"}") + _, _ = io.WriteString(w, "{\"Result\":\"error\",\"ErrorMessage\":\""+err.Error()+"\"}") helper.Check(err) } } @@ -388,7 +374,7 @@ func isAuthenticated(w http.ResponseWriter, r *http.Request, isUpload bool) bool return true } if isUpload { - _, err := fmt.Fprint(w, "{\"Result\":\"error\",\"ErrorMessage\":\"Not authenticated\"}") + _, err := io.WriteString(w, "{\"Result\":\"error\",\"ErrorMessage\":\"Not authenticated\"}") helper.Check(err) } else { redirect(w, "login") diff --git a/internal/webserver/Webserver_test.go b/internal/webserver/Webserver_test.go index fca349c..1c40881 100644 --- a/internal/webserver/Webserver_test.go +++ b/internal/webserver/Webserver_test.go @@ -2,8 +2,8 @@ package webserver import ( "Gokapi/internal/configuration" - testconfiguration "Gokapi/internal/test" - testconfiguration2 "Gokapi/internal/test/testconfiguration" + "Gokapi/internal/test" + "Gokapi/internal/test/testconfiguration" "html/template" "io/fs" "os" @@ -16,12 +16,12 @@ import ( // causes data race. It will be fixed in Go 1.17, see https://github.com/golang/go/issues/39807 func TestMain(m *testing.M) { - testconfiguration2.Create(true) + testconfiguration.Create(true) configuration.Load() go Start() time.Sleep(1 * time.Second) exitVal := m.Run() - testconfiguration2.Delete() + testconfiguration.Delete() os.Exit(exitVal) } @@ -41,7 +41,7 @@ func TestEmbedFs(t *testing.T) { func TestIndexRedirect(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/", RequiredContent: []string{""}, IsHtml: true, @@ -50,7 +50,7 @@ func TestIndexRedirect(t *testing.T) { func TestIndexFile(t *testing.T) { t.Parallel() settings := configuration.GetServerSettings() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/index", RequiredContent: []string{settings.RedirectUrl}, IsHtml: true, @@ -59,14 +59,14 @@ func TestIndexFile(t *testing.T) { } func TestStaticDirs(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/css/cover.css", RequiredContent: []string{".btn-secondary:hover"}, }) } func TestLogin(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/login", RequiredContent: []string{"id=\"uname_hidden\""}, IsHtml: true, @@ -74,7 +74,7 @@ func TestLogin(t *testing.T) { } func TestAdminNoAuth(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", RequiredContent: []string{"URL=./login\""}, IsHtml: true, @@ -82,11 +82,11 @@ func TestAdminNoAuth(t *testing.T) { } func TestAdminAuth(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", RequiredContent: []string{"Downloads remaining"}, IsHtml: true, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "validsession", }}, @@ -94,11 +94,11 @@ func TestAdminAuth(t *testing.T) { } func TestAdminExpiredAuth(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", RequiredContent: []string{"URL=./login\""}, IsHtml: true, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "expiredsession", }}, @@ -107,11 +107,11 @@ func TestAdminExpiredAuth(t *testing.T) { func TestAdminRenewalAuth(t *testing.T) { t.Parallel() - cookies := testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + cookies := test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", RequiredContent: []string{"Downloads remaining"}, IsHtml: true, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "needsRenewal", }}, @@ -126,11 +126,11 @@ func TestAdminRenewalAuth(t *testing.T) { if sessionCookie == "needsRenewal" { t.Error("Session not renewed") } - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", RequiredContent: []string{"Downloads remaining"}, IsHtml: true, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: sessionCookie, }}, @@ -139,11 +139,11 @@ func TestAdminRenewalAuth(t *testing.T) { func TestAdminInvalidAuth(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", RequiredContent: []string{"URL=./login\""}, IsHtml: true, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "invalid", }}, @@ -152,7 +152,7 @@ func TestAdminInvalidAuth(t *testing.T) { func TestInvalidLink(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/d?id=123", RequiredContent: []string{"URL=./error\""}, IsHtml: true, @@ -160,7 +160,7 @@ func TestInvalidLink(t *testing.T) { } func TestError(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/error", RequiredContent: []string{"this file cannot be found"}, IsHtml: true, @@ -168,7 +168,7 @@ func TestError(t *testing.T) { } func TestForgotPw(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/forgotpw", RequiredContent: []string{"--reset-pw"}, IsHtml: true, @@ -176,63 +176,63 @@ func TestForgotPw(t *testing.T) { } func TestLoginCorrect(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/login", RequiredContent: []string{"URL=./admin\""}, IsHtml: true, Method: "POST", - PostValues: []testconfiguration.PostBody{{"username", "test"}, {"password", "testtest"}}, + PostValues: []test.PostBody{{"username", "test"}, {"password", "testtest"}}, }) } func TestLoginIncorrectPassword(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/login", RequiredContent: []string{"Incorrect username or password"}, IsHtml: true, Method: "POST", - PostValues: []testconfiguration.PostBody{{"username", "test"}, {"password", "incorrect"}}, + PostValues: []test.PostBody{{"username", "test"}, {"password", "incorrect"}}, }) } func TestLoginIncorrectUsername(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/login", RequiredContent: []string{"Incorrect username or password"}, IsHtml: true, Method: "POST", - PostValues: []testconfiguration.PostBody{{"username", "incorrect"}, {"password", "incorrect"}}, + PostValues: []test.PostBody{{"username", "incorrect"}, {"password", "incorrect"}}, }) } func TestLogout(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", RequiredContent: []string{"Downloads remaining"}, IsHtml: true, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "logoutsession", }}, }) // Logout - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/logout", RequiredContent: []string{"URL=./login\""}, IsHtml: true, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "logoutsession", }}, }) // Admin after logout - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://localhost:53843/admin", RequiredContent: []string{"URL=./login\""}, IsHtml: true, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "logoutsession", }}, @@ -241,12 +241,12 @@ func TestLogout(t *testing.T) { func TestDownloadHotlink(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/hotlink/PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg", RequiredContent: []string{"123"}, }) // Download expired hotlink - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/hotlink/PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg", RequiredContent: []string{"Created with GIMP"}, }) @@ -255,24 +255,24 @@ func TestDownloadHotlink(t *testing.T) { func TestDownloadNoPassword(t *testing.T) { t.Parallel() // Show download page - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=Wzol7LyY2QVczXynJtVo", IsHtml: true, RequiredContent: []string{"smallfile2"}, }) // Download - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/downloadFile?id=Wzol7LyY2QVczXynJtVo", RequiredContent: []string{"789"}, }) // Show download page expired file - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=Wzol7LyY2QVczXynJtVo", IsHtml: true, RequiredContent: []string{"URL=./error\""}, }) // Download expired file - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/downloadFile?id=Wzol7LyY2QVczXynJtVo", IsHtml: true, RequiredContent: []string{"URL=./error\""}, @@ -281,7 +281,7 @@ func TestDownloadNoPassword(t *testing.T) { func TestDownloadPagePassword(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN", IsHtml: true, RequiredContent: []string{"Password required"}, @@ -289,44 +289,44 @@ func TestDownloadPagePassword(t *testing.T) { } func TestDownloadPageIncorrectPassword(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN", IsHtml: true, RequiredContent: []string{"Incorrect password!"}, Method: "POST", - PostValues: []testconfiguration.PostBody{{"password", "incorrect"}}, + PostValues: []test.PostBody{{"password", "incorrect"}}, }) } func TestDownloadIncorrectPasswordCookie(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN", IsHtml: true, RequiredContent: []string{"Password required"}, - Cookies: []testconfiguration.Cookie{{"pjpLXGJKigM4hjtA6T6sN", "invalid"}}, + Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN", "invalid"}}, }) } func TestDownloadIncorrectPassword(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/downloadFile?id=jpLXGJKigM4hjtA6T6sN", IsHtml: true, RequiredContent: []string{"URL=./d?id=jpLXGJKigM4hjtA6T6sN"}, - Cookies: []testconfiguration.Cookie{{"pjpLXGJKigM4hjtA6T6sN", "invalid"}}, + Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN", "invalid"}}, }) } func TestDownloadCorrectPassword(t *testing.T) { t.Parallel() // Submit download page correct password - cookies := testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + 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: []testconfiguration.PostBody{{"password", "123"}}, + PostValues: []test.PostBody{{"password", "123"}}, }) pwCookie := "" for _, cookie := range cookies { @@ -339,23 +339,23 @@ func TestDownloadCorrectPassword(t *testing.T) { t.Error("Cookie not set") } // Show download page correct password - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN2", IsHtml: true, RequiredContent: []string{"smallfile"}, - Cookies: []testconfiguration.Cookie{{"pjpLXGJKigM4hjtA6T6sN2", pwCookie}}, + Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN2", pwCookie}}, }) // Download correct password - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/downloadFile?id=jpLXGJKigM4hjtA6T6sN2", RequiredContent: []string{"456"}, - Cookies: []testconfiguration.Cookie{{"pjpLXGJKigM4hjtA6T6sN2", pwCookie}}, + Cookies: []test.Cookie{{"pjpLXGJKigM4hjtA6T6sN2", pwCookie}}, }) } func TestDeleteFileNonAuth(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/delete?id=e4TjE7CokWK0giiLNxDL", IsHtml: true, RequiredContent: []string{"URL=./login"}, @@ -364,20 +364,20 @@ func TestDeleteFileNonAuth(t *testing.T) { func TestDeleteFileInvalidKey(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/delete", IsHtml: true, RequiredContent: []string{"URL=./admin"}, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "validsession", }}, }) - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/delete?id=", IsHtml: true, RequiredContent: []string{"URL=./admin"}, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "validsession", }}, @@ -386,11 +386,11 @@ func TestDeleteFileInvalidKey(t *testing.T) { func TestDeleteFile(t *testing.T) { t.Parallel() - testconfiguration.HttpPageResult(t, testconfiguration.HttpTestConfig{ + test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/delete?id=e4TjE7CokWK0giiLNxDL", IsHtml: true, RequiredContent: []string{"URL=./admin"}, - Cookies: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "validsession", }}, @@ -399,21 +399,22 @@ func TestDeleteFile(t *testing.T) { func TestPostUploadNoAuth(t *testing.T) { t.Parallel() - testconfiguration.HttpPostRequest(t, testconfiguration.HttpTestConfig{ + 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) { - testconfiguration.HttpPostRequest(t, testconfiguration.HttpTestConfig{ + 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: []testconfiguration.Cookie{{ + Cookies: []test.Cookie{{ Name: "session_token", Value: "validsession", }}, diff --git a/internal/webserver/api/Api.go b/internal/webserver/api/Api.go new file mode 100644 index 0000000..eea3615 --- /dev/null +++ b/internal/webserver/api/Api.go @@ -0,0 +1,97 @@ +package api + +import ( + "Gokapi/internal/configuration" + "Gokapi/internal/helper" + "Gokapi/internal/storage" + "encoding/json" + "net/http" + "strings" +) + +// Process parses the request and executes the API call or returns an error message to the sender +func Process(w http.ResponseWriter, r *http.Request) { + w.Header().Set("cache-control", "no-store") + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + request := parseRequest(r) + if !isAuthorised(w, request) { + return + } + switch request.requestUrl { + case "/files": + list(w) + case "/files/add": + upload(w, request) + case "/files/delete": + deleteFile(w, request) + default: + sendError(w, http.StatusBadRequest, "Invalid request") + } +} + +func deleteFile(w http.ResponseWriter, request apiRequest) { + ok := storage.DeleteFile(request.headerId) + if ok { + sendOk(w) + } else { + sendError(w, http.StatusBadRequest, "Invalid id provided.") + } +} + +func list(w http.ResponseWriter) { + sendOk(w) + settings := configuration.GetServerSettings() + result, err := json.Marshal(settings.Files) + configuration.Release() + helper.Check(err) + _, _ = w.Write(result) +} + +func upload(w http.ResponseWriter, request apiRequest) { + sendOk(w) + // TODO +} + +func isValidApiKey(key string) bool { + if key == "" { + return false + } + settings := configuration.GetServerSettings() + savedKey := settings.ApiKeys[key] + configuration.Release() + return savedKey.Id != "" +} + +func isAuthorised(w http.ResponseWriter, request apiRequest) bool { + if true { + return true + } + if isValidApiKey(request.apiKey) { + return true + } + sendError(w, http.StatusUnauthorized, "Unauthorized") + return false +} + +func sendError(w http.ResponseWriter, errorInt int, errorMessage string) { + w.WriteHeader(errorInt) + _, _ = w.Write([]byte("{\"Result\":\"error\",\"ErrorMessage\":\"" + errorMessage + "\"}")) +} + +func sendOk(w http.ResponseWriter) { + w.WriteHeader(http.StatusOK) +} + +type apiRequest struct { + apiKey string + requestUrl string + headerId string +} + +func parseRequest(r *http.Request) apiRequest { + return apiRequest{ + apiKey: r.Header.Get("apikey"), + headerId: r.Header.Get("id"), + requestUrl: strings.Replace(r.URL.String(), "/api", "", 1), + } +} diff --git a/internal/webserver/api/Api_test.go b/internal/webserver/api/Api_test.go new file mode 100644 index 0000000..ca61da0 --- /dev/null +++ b/internal/webserver/api/Api_test.go @@ -0,0 +1,23 @@ +package api + +import ( + "Gokapi/internal/configuration" + "Gokapi/internal/test" + "Gokapi/internal/test/testconfiguration" + "os" + "testing" +) + +func TestMain(m *testing.M) { + testconfiguration.Create(false) + configuration.Load() + exitVal := m.Run() + testconfiguration.Delete() + os.Exit(exitVal) +} + +func TestIsValidApiKey(t *testing.T) { + test.IsEqualBool(t, isValidApiKey(""), false) + test.IsEqualBool(t, isValidApiKey("invalid"), false) + test.IsEqualBool(t, isValidApiKey("validkey"), true) +} diff --git a/internal/webserver/fileupload/FileUpload.go b/internal/webserver/fileupload/FileUpload.go new file mode 100644 index 0000000..76764cf --- /dev/null +++ b/internal/webserver/fileupload/FileUpload.go @@ -0,0 +1,73 @@ +package fileupload + +import ( + "Gokapi/internal/configuration" + "Gokapi/internal/helper" + "Gokapi/internal/models" + "Gokapi/internal/storage" + "io" + "net/http" + "strconv" + "time" +) + +func Process(w http.ResponseWriter, r *http.Request, isWeb bool) error { + err := r.ParseMultipartForm(20 * 1024 * 1024) + if err != nil { + return err + } + var config models.UploadRequest + if isWeb { + config = parseConfig(r.Form, true) + } else { + config = parseConfig(r.Header, false) + } + file, header, err := r.FormFile("file") + if err != nil { + return err + } + + result, err := storage.NewFile(file, header, config) + if err != nil { + return err + } + defer file.Close() + _, err = io.WriteString(w, result.ToJsonResult(config.ExternalUrl)) + if err != nil { + helper.Check(err) + } + return nil +} + +func parseConfig(values formOrHeader, setNewDefaults bool) models.UploadRequest { + allowedDownloads := values.Get("allowedDownloads") + expiryDays := values.Get("expiryDays") + password := values.Get("password") + allowedDownloadsInt, err := strconv.Atoi(allowedDownloads) + settings := configuration.GetServerSettings() + if err != nil { + allowedDownloadsInt = settings.DefaultDownloads + } + expiryDaysInt, err := strconv.Atoi(expiryDays) + if err != nil { + expiryDaysInt = settings.DefaultExpiry + } + if setNewDefaults { + settings.DefaultExpiry = expiryDaysInt + settings.DefaultDownloads = allowedDownloadsInt + settings.DefaultPassword = password + } + externalUrl := settings.ServerUrl + configuration.Release() + return models.UploadRequest{ + AllowedDownloads: allowedDownloadsInt, + Expiry: expiryDaysInt, + ExpiryTimestamp: time.Now().Add(time.Duration(expiryDaysInt) * time.Hour * 24).Unix(), + Password: password, + ExternalUrl: externalUrl, + } +} + +type formOrHeader interface { + Get(key string) string +} diff --git a/internal/webserver/fileupload/FileUpload_test.go b/internal/webserver/fileupload/FileUpload_test.go new file mode 100644 index 0000000..248a8b8 --- /dev/null +++ b/internal/webserver/fileupload/FileUpload_test.go @@ -0,0 +1,93 @@ +package fileupload + +import ( + "Gokapi/internal/configuration" + "Gokapi/internal/models" + "Gokapi/internal/test" + "Gokapi/internal/test/testconfiguration" + "bytes" + "encoding/json" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "reflect" + "testing" +) + +func TestMain(m *testing.M) { + testconfiguration.Create(false) + configuration.Load() + exitVal := m.Run() + testconfiguration.Delete() + os.Exit(exitVal) +} + +func TestParseConfig(t *testing.T) { + settings := configuration.GetServerSettings() + configuration.Release() + data := testData{ + allowedDownloads: "9", + expiryDays: "5", + password: "123", + } + config := parseConfig(data, false) + test.IsEqualInt(t, config.AllowedDownloads, 9) + test.IsEqualString(t, config.Password, "123") + test.IsEqualInt(t, config.Expiry, 5) + test.IsEqualInt(t, settings.DefaultDownloads, 3) + config = parseConfig(data, true) + test.IsEqualInt(t, settings.DefaultDownloads, 9) + settings.DefaultDownloads = 3 + settings.DefaultExpiry = 20 + data.allowedDownloads = "" + data.expiryDays = "invalid" + config = parseConfig(data, false) + test.IsEqualInt(t, config.AllowedDownloads, 3) + test.IsEqualInt(t, config.Expiry, 20) +} + +func TestProcess(t *testing.T) { + w := httptest.NewRecorder() + r := getRecorder() + err := Process(w, r, false) + test.IsNil(t, err) + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + result := models.Result{} + err = json.Unmarshal(body, &result) + test.IsNil(t, err) + test.IsEqualString(t, result.Result, "OK") + test.IsEqualString(t, result.Url, "http://127.0.0.1:53843/d?id=") + test.IsEqualString(t, result.HotlinkUrl, "http://127.0.0.1:53843/hotlink/") + test.IsEqualString(t, result.FileInfo.Name, "testFile") + test.IsEqualString(t, result.FileInfo.SHA256, "17513aad503256b7fdc94d613aeb87b8338c433a") + test.IsEqualString(t, result.FileInfo.Size, "11 B") +} + +func getRecorder() *http.Request { + var b bytes.Buffer + w := multipart.NewWriter(&b) + writer, _ := w.CreateFormFile("file", "testFile") + io.WriteString(writer, "testContent") + w.Close() + r := httptest.NewRequest("POST", "/upload", &b) + r.Header.Set("Content-Type", w.FormDataContentType()) + r.Header.Add("allowedDownloads", "9") + r.Header.Add("expiryDays", "5") + r.Header.Add("password", "123") + return r +} + +type testData struct { + allowedDownloads, expiryDays, password string +} + +func (t testData) Get(key string) string { + field := reflect.ValueOf(&t).Elem().FieldByName(key) + if field.IsValid() { + return field.String() + } + return "" +}