mirror of
https://github.com/Forceu/Gokapi.git
synced 2026-03-09 19:48:51 -05:00
Refactoring, early implementation of API
This commit is contained in:
9
internal/models/FileUpload.go
Normal file
9
internal/models/FileUpload.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
type UploadRequest struct {
|
||||
AllowedDownloads int
|
||||
Expiry int
|
||||
ExpiryTimestamp int64
|
||||
Password string
|
||||
ExternalUrl string
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)) }()
|
||||
|
||||
@@ -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, "<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=./"+url+"\"></head></html>")
|
||||
_, _ = io.WriteString(w, "<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=./"+url+"\"></head></html>")
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
@@ -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{"<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=./index\"></head></html>"},
|
||||
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",
|
||||
}},
|
||||
|
||||
97
internal/webserver/api/Api.go
Normal file
97
internal/webserver/api/Api.go
Normal file
@@ -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),
|
||||
}
|
||||
}
|
||||
23
internal/webserver/api/Api_test.go
Normal file
23
internal/webserver/api/Api_test.go
Normal file
@@ -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)
|
||||
}
|
||||
73
internal/webserver/fileupload/FileUpload.go
Normal file
73
internal/webserver/fileupload/FileUpload.go
Normal file
@@ -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
|
||||
}
|
||||
93
internal/webserver/fileupload/FileUpload_test.go
Normal file
93
internal/webserver/fileupload/FileUpload_test.go
Normal file
@@ -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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user