diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..36514c68 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,24 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally declare the Python requirements required to build your docs +python: + system_packages: true diff --git a/cmd/gokapi/Main.go b/cmd/gokapi/Main.go index 34368d77..73852aac 100644 --- a/cmd/gokapi/Main.go +++ b/cmd/gokapi/Main.go @@ -30,7 +30,7 @@ import ( // versionGokapi is the current version in readable form. // Other version numbers can be modified in /build/go-generate/updateVersionNumbers.go -const versionGokapi = "1.7.1" +const versionGokapi = "1.7.2" // The following calls update the version numbers, update documentation, minify Js/CSS and build the WASM modules //go:generate go run "../../build/go-generate/updateVersionNumbers.go" diff --git a/docs/advanced.rst b/docs/advanced.rst index 09dd2696..3810c6d9 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -147,4 +147,3 @@ By default, all files are included in the executable. If you want to change the 3. Make changes to the folders. ``static`` contains images, CSS files and JavaScript. ``templates`` contains the HTML code. 4. Restart the server. If the folders exist, the server will use the local files instead of the embedded files. 5. Optional: To embed the files permanently, copy the modified files back to the original folders and recompile with ``go build Gokapi/cmd/gokapi``. - diff --git a/internal/configuration/Configuration.go b/internal/configuration/Configuration.go index 36348bb3..40c1676d 100644 --- a/internal/configuration/Configuration.go +++ b/internal/configuration/Configuration.go @@ -60,6 +60,9 @@ func Load() { if envMaxMem != "" { serverSettings.MaxMemory = Environment.MaxMemory } + if serverSettings.PublicName == "" { + serverSettings.PublicName = "Gokapi" + } helper.CreateDir(serverSettings.DataDir) filesystem.Init(serverSettings.DataDir) log.Init(Environment.DataDir) diff --git a/internal/configuration/configupgrade/Upgrade.go b/internal/configuration/configupgrade/Upgrade.go index d97fd0e5..a22bd07c 100644 --- a/internal/configuration/configupgrade/Upgrade.go +++ b/internal/configuration/configupgrade/Upgrade.go @@ -16,7 +16,7 @@ import ( ) // CurrentConfigVersion is the version of the configuration structure. Used for upgrading -const CurrentConfigVersion = 13 +const CurrentConfigVersion = 14 // DoUpgrade checks if an old version is present and updates it to the current version if required func DoUpgrade(settings *models.Configuration, env *environment.Environment) bool { @@ -66,6 +66,10 @@ func updateConfig(settings *models.Configuration, env *environment.Environment) } } } + // < v1.7.2 + if settings.ConfigVersion < 14 { + settings.PublicName = "Gokapi" + } } func legacyFileToCurrentFile(input []byte) models.File { diff --git a/internal/configuration/setup/Setup.go b/internal/configuration/setup/Setup.go index 22becedd..80ceca8d 100644 --- a/internal/configuration/setup/Setup.go +++ b/internal/configuration/setup/Setup.go @@ -114,6 +114,11 @@ func startSetupWebserver() { WriteTimeout: 2 * time.Minute, Handler: mux, } + if debugDisableAuth { + srv.Addr = "127.0.0.1:" + port + fmt.Println("Authentication is disabled by debug flag. Setup only accessible by localhost") + fmt.Println("Please open http://127.0.0.1:" + port + "/setup to setup Gokapi.") + } fmt.Println("Please open http://" + resolveHostIp() + ":" + port + "/setup to setup Gokapi.") listener, err := net.Listen("tcp", ":"+port) if err != nil { @@ -337,6 +342,10 @@ func parseServerSettings(result *models.Configuration, formObjects *[]jsonFormOb result.Port = ":" + strconv.Itoa(port) } + result.PublicName, err = getFormValueString(formObjects, "public_name") + if err != nil { + return err + } result.ServerUrl, err = getFormValueString(formObjects, "url") if err != nil { return err diff --git a/internal/configuration/setup/Setup_test.go b/internal/configuration/setup/Setup_test.go index 6a9788b2..fd17f318 100644 --- a/internal/configuration/setup/Setup_test.go +++ b/internal/configuration/setup/Setup_test.go @@ -458,6 +458,7 @@ type setupValues struct { BindLocalhost setupEntry `form:"localhost_sel" isBool:"true"` UseSsl setupEntry `form:"ssl_sel" isBool:"true"` Port setupEntry `form:"port" isInt:"true"` + PublicName setupEntry `form:"public_name"` ExtUrl setupEntry `form:"url"` RedirectUrl setupEntry `form:"url_redirection"` AuthenticationMode setupEntry `form:"authentication_sel" isInt:"true"` @@ -572,6 +573,7 @@ func createInputInternalAuth() setupValues { values.init() values.BindLocalhost.Value = "1" + values.PublicName.Value = "Test Name" values.UseSsl.Value = "0" values.Port.Value = "53842" values.ExtUrl.Value = "http://127.0.0.1:53842/" @@ -596,6 +598,7 @@ func createInputHeaderAuth() setupValues { values.init() values.BindLocalhost.Value = "0" + values.PublicName.Value = "Test Name" values.UseSsl.Value = "1" values.Port.Value = "53842" values.ExtUrl.Value = "http://127.0.0.1:53842/" diff --git a/internal/configuration/setup/templates/setup.tmpl b/internal/configuration/setup/templates/setup.tmpl index 2c5fa903..ed277b8a 100644 --- a/internal/configuration/setup/templates/setup.tmpl +++ b/internal/configuration/setup/templates/setup.tmpl @@ -106,6 +106,11 @@
+ +
+ + +



{{ if .IsDocker }} @@ -122,6 +127,7 @@



+
@@ -616,6 +622,7 @@ function TestAWS(button) { {{ end }} document.getElementById("port").value = "{{ .Port }}"; document.getElementById("url").value = "{{ .Settings.ServerUrl }}"; + document.getElementById("public_name").value = "{{ .Settings.PublicName }}"; document.getElementById("url_redirection").value = "{{ .Settings.RedirectUrl }}"; document.getElementById("authentication_sel").value = "{{ .Auth.Method }}"; authSelectionChanged("{{ .Auth.Method }}") diff --git a/internal/models/Configuration.go b/internal/models/Configuration.go index c8db8568..c0b2dd45 100644 --- a/internal/models/Configuration.go +++ b/internal/models/Configuration.go @@ -11,6 +11,7 @@ type Configuration struct { Port string `json:"Port"` ServerUrl string `json:"ServerUrl"` RedirectUrl string `json:"RedirectUrl"` + PublicName string `json:"PublicName"` ConfigVersion int `json:"ConfigVersion"` LengthId int `json:"LengthId"` DataDir string `json:"DataDir"` diff --git a/internal/models/Configuration_test.go b/internal/models/Configuration_test.go index 9bc4d260..af778707 100644 --- a/internal/models/Configuration_test.go +++ b/internal/models/Configuration_test.go @@ -23,12 +23,13 @@ var testConfig = Configuration{ Port: ":12345", ServerUrl: "https://testserver.com/", RedirectUrl: "https://test.com", - ConfigVersion: 11, + ConfigVersion: 14, LengthId: 5, DataDir: "test", MaxMemory: 50, UseSsl: true, MaxFileSizeMB: 20, + PublicName: "public-name", Encryption: Encryption{ Level: 1, Cipher: []byte{0x00}, @@ -47,4 +48,4 @@ func TestConfiguration_ToString(t *testing.T) { test.IsEqualString(t, testConfig.ToString(), exptectedUnidentedOutput) } -const exptectedUnidentedOutput = `{"Authentication":{"Method":0,"SaltAdmin":"saltadmin","SaltFiles":"saltfiles","Username":"admin","Password":"adminpwhashed","HeaderKey":"","OauthProvider":"","OAuthClientId":"","OAuthClientSecret":"","HeaderUsers":null,"OauthUsers":null},"Port":":12345","ServerUrl":"https://testserver.com/","RedirectUrl":"https://test.com","ConfigVersion":11,"LengthId":5,"DataDir":"test","MaxMemory":50,"UseSsl":true,"MaxFileSizeMB":20,"Encryption":{"Level":1,"Cipher":"AA==","Salt":"encsalt","Checksum":"encsum","ChecksumSalt":"encsumsalt"},"PicturesAlwaysLocal":true}` +const exptectedUnidentedOutput = `{"Authentication":{"Method":0,"SaltAdmin":"saltadmin","SaltFiles":"saltfiles","Username":"admin","Password":"adminpwhashed","HeaderKey":"","OauthProvider":"","OAuthClientId":"","OAuthClientSecret":"","HeaderUsers":null,"OauthUsers":null},"Port":":12345","ServerUrl":"https://testserver.com/","RedirectUrl":"https://test.com","PublicName":"public-name","ConfigVersion":14,"LengthId":5,"DataDir":"test","MaxMemory":50,"UseSsl":true,"MaxFileSizeMB":20,"Encryption":{"Level":1,"Cipher":"AA==","Salt":"encsalt","Checksum":"encsum","ChecksumSalt":"encsumsalt"},"PicturesAlwaysLocal":true}` diff --git a/internal/webserver/Webserver.go b/internal/webserver/Webserver.go index 607bc2a3..f9030f66 100644 --- a/internal/webserver/Webserver.go +++ b/internal/webserver/Webserver.go @@ -5,6 +5,7 @@ Handling of webserver and requests / uploads */ import ( + "bytes" "context" "embed" "encoding/base64" @@ -35,6 +36,7 @@ import ( "os" "sort" "strings" + templatetext "text/template" "time" ) @@ -71,8 +73,6 @@ var templateFolder *template.Template var imageExpiredPicture []byte -const expiredFile = "static/expired.png" - var srv http.Server var sseServer *sse.Server @@ -90,13 +90,12 @@ func Start() { if helper.FolderExists("static") { fmt.Println("Found folder 'static', using local folder instead of internal static folder") mux.Handle("/", http.FileServer(http.Dir("static"))) - imageExpiredPicture, err = os.ReadFile(expiredFile) - helper.Check(err) } else { mux.Handle("/", http.FileServer(http.FS(webserverDir))) - imageExpiredPicture, err = fs.ReadFile(staticFolderEmbedded, "web/"+expiredFile) helper.Check(err) } + loadExpiryImage() + mux.HandleFunc("/admin", requireLogin(showAdminMenu, false)) mux.HandleFunc("/api/", processApi) mux.HandleFunc("/apiDelete", requireLogin(deleteApiKey, false)) @@ -157,6 +156,16 @@ func Start() { } } +func loadExpiryImage() { + svgTemplate, err := templatetext.ParseFS(templateFolderEmbedded, "web/templates/expired_file_svg.tmpl") + helper.Check(err) + var buf bytes.Buffer + view := UploadView{} + err = svgTemplate.Execute(&buf, view.convertGlobalConfig(ViewMain)) + helper.Check(err) + imageExpiredPicture = buf.Bytes() +} + // Shutdown closes the webserver gracefully func Shutdown() { sseServer.Close() @@ -211,7 +220,7 @@ func doLogout(w http.ResponseWriter, r *http.Request) { // Handling of /index and redirecting to globalConfig.RedirectUrl func showIndex(w http.ResponseWriter, r *http.Request) { - err := templateFolder.ExecuteTemplate(w, "index", genericView{RedirectUrl: configuration.Get().RedirectUrl}) + err := templateFolder.ExecuteTemplate(w, "index", genericView{RedirectUrl: configuration.Get().RedirectUrl, PublicName: configuration.Get().PublicName}) helper.Check(err) } @@ -228,19 +237,19 @@ func showError(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Has("key") { errorReason = wrongCipher } - err := templateFolder.ExecuteTemplate(w, "error", genericView{ErrorId: errorReason}) + err := templateFolder.ExecuteTemplate(w, "error", genericView{ErrorId: errorReason, PublicName: configuration.Get().PublicName}) helper.Check(err) } // Handling of /error-auth func showErrorAuth(w http.ResponseWriter, r *http.Request) { - err := templateFolder.ExecuteTemplate(w, "error_auth", genericView{}) + err := templateFolder.ExecuteTemplate(w, "error_auth", genericView{PublicName: configuration.Get().PublicName}) helper.Check(err) } // Handling of /forgotpw func forgotPassword(w http.ResponseWriter, r *http.Request) { - err := templateFolder.ExecuteTemplate(w, "forgotpw", genericView{}) + err := templateFolder.ExecuteTemplate(w, "forgotpw", genericView{PublicName: configuration.Get().PublicName}) helper.Check(err) } @@ -303,15 +312,18 @@ func showLogin(w http.ResponseWriter, r *http.Request) { IsFailedLogin: failedLogin, User: user, IsAdminView: false, + PublicName: configuration.Get().PublicName, }) helper.Check(err) } // LoginView contains variables for the login template type LoginView struct { - IsFailedLogin bool - User string - IsAdminView bool + IsFailedLogin bool + IsAdminView bool + IsDownloadView bool + User string + PublicName string } // Handling of /d @@ -330,7 +342,9 @@ func showDownload(w http.ResponseWriter, r *http.Request) { Name: file.Name, Size: file.Size, Id: file.Id, + IsDownloadView: true, EndToEndEncryption: file.Encryption.IsEndToEndEncrypted, + PublicName: configuration.Get().PublicName, IsFailedLogin: false, UsesHttps: configuration.UsesHttps(), } @@ -354,6 +368,7 @@ func showDownload(w http.ResponseWriter, r *http.Request) { case <-time.After(1 * time.Second): } } + view.IsPasswordView = true err := templateFolder.ExecuteTemplate(w, "download_password", view) helper.Check(err) return @@ -376,7 +391,7 @@ func showHotlink(w http.ResponseWriter, r *http.Request) { hotlinkId := strings.Replace(r.URL.Path, "/hotlink/", "", 1) file, ok := storage.GetFileByHotlink(hotlinkId) if !ok { - w.Header().Set("Content-Type", "image/png") + w.Header().Set("Content-Type", "image/svg+xml") _, err := w.Write(imageExpiredPicture) helper.Check(err) return @@ -487,7 +502,7 @@ func showE2ESetup(w http.ResponseWriter, r *http.Request) { return } e2einfo := database.GetEnd2EndInfo() - err := templateFolder.ExecuteTemplate(w, "e2esetup", e2ESetupView{HasBeenSetup: e2einfo.HasBeenSetUp()}) + err := templateFolder.ExecuteTemplate(w, "e2esetup", e2ESetupView{HasBeenSetup: e2einfo.HasBeenSetUp(), PublicName: configuration.Get().PublicName}) helper.Check(err) } @@ -496,17 +511,22 @@ type DownloadView struct { Name string Size string Id string + Cipher string + PublicName string IsFailedLogin bool IsAdminView bool + IsDownloadView bool + IsPasswordView bool ClientSideDecryption bool EndToEndEncryption bool UsesHttps bool - Cipher string } type e2ESetupView struct { - IsAdminView bool - HasBeenSetup bool + IsAdminView bool + IsDownloadView bool + HasBeenSetup bool + PublicName string } // UploadView contains parameters for the admin menu template @@ -516,25 +536,27 @@ type UploadView struct { Url string HotlinkUrl string GenericHotlinkUrl string - TimeNow int64 + DefaultPassword string + Logs string + PublicName string IsAdminView bool + IsDownloadView bool IsApiView bool - MaxFileSize int IsLogoutAvailable bool - DefaultDownloads int - DefaultExpiry int - DefaultPassword string DefaultUnlimitedDownload bool DefaultUnlimitedTime bool EndToEndEncryption bool + MaxFileSize int + DefaultDownloads int + DefaultExpiry int ActiveView int - Logs string + TimeNow int64 } // ViewMain is the identifier for the main menu const ViewMain = 0 -// ViewLogs is the identifier for the log viever menu +// ViewLogs is the identifier for the log viewer menu const ViewLogs = 1 // ViewAPI is the identifier for the API menu @@ -583,15 +605,18 @@ func (u *UploadView) convertGlobalConfig(view int) *UploadView { } } - u.Url = configuration.Get().ServerUrl + "d?id=" - u.HotlinkUrl = configuration.Get().ServerUrl + "hotlink/" - u.GenericHotlinkUrl = configuration.Get().ServerUrl + "downloadFile?id=" + config := configuration.Get() + + u.Url = config.ServerUrl + "d?id=" + u.HotlinkUrl = config.ServerUrl + "hotlink/" + u.GenericHotlinkUrl = config.ServerUrl + "downloadFile?id=" u.Items = result + u.PublicName = config.PublicName u.ApiKeys = resultApi u.TimeNow = time.Now().Unix() u.IsAdminView = true u.ActiveView = view - u.MaxFileSize = configuration.Get().MaxFileSizeMB + u.MaxFileSize = config.MaxFileSizeMB u.IsLogoutAvailable = authentication.IsLogoutAvailable() defaultValues := database.GetUploadDefaults() u.DefaultDownloads = defaultValues.Downloads @@ -599,7 +624,7 @@ func (u *UploadView) convertGlobalConfig(view int) *UploadView { u.DefaultPassword = defaultValues.Password u.DefaultUnlimitedDownload = defaultValues.UnlimitedDownload u.DefaultUnlimitedTime = defaultValues.UnlimitedTime - u.EndToEndEncryption = configuration.Get().Encryption.Level == encryption.EndToEndEncryption + u.EndToEndEncryption = config.Encryption.Level == encryption.EndToEndEncryption return u } @@ -698,7 +723,9 @@ func addNoCacheHeader(w http.ResponseWriter) { // A view containing parameters for a generic template type genericView struct { - IsAdminView bool - RedirectUrl string - ErrorId int + IsAdminView bool + IsDownloadView bool + PublicName string + RedirectUrl string + ErrorId int } diff --git a/internal/webserver/Webserver_test.go b/internal/webserver/Webserver_test.go index d85b6062..09d2c741 100644 --- a/internal/webserver/Webserver_test.go +++ b/internal/webserver/Webserver_test.go @@ -12,7 +12,6 @@ import ( "github.com/forceu/gokapi/internal/webserver/authentication" "github.com/r3labs/sse/v2" "html/template" - "io/fs" "os" "strings" "testing" @@ -35,13 +34,9 @@ func TestEmbedFs(t *testing.T) { if err != nil { t.Error("Unable to read templates") } - if !strings.Contains(templates.DefinedTemplates(), "app_name") { + if !strings.Contains(templates.DefinedTemplates(), "header") { t.Error("Unable to parse templates") } - _, err = fs.Stat(staticFolderEmbedded, "web/static/expired.png") - if err != nil { - t.Error("Static webdir incomplete") - } } func TestIndexRedirect(t *testing.T) { @@ -326,7 +321,7 @@ func TestDownloadHotlink(t *testing.T) { // Download expired hotlink test.HttpPageResult(t, test.HttpTestConfig{ Url: "http://127.0.0.1:53843/hotlink/PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg", - RequiredContent: []string{"Created with GIMP"}, + RequiredContent: []string{"The requested image has expired"}, }) } diff --git a/internal/webserver/web/static/expired.png b/internal/webserver/web/static/expired.png deleted file mode 100644 index f2c515cb..00000000 Binary files a/internal/webserver/web/static/expired.png and /dev/null differ diff --git a/internal/webserver/web/static/js/end2end_download.js b/internal/webserver/web/static/js/end2end_download.js index 61dd686a..3f6d2707 100644 --- a/internal/webserver/web/static/js/end2end_download.js +++ b/internal/webserver/web/static/js/end2end_download.js @@ -1,6 +1,6 @@ function parseHashValue(id) { - let key = localStorage.getItem("key-" + id); - let filename = localStorage.getItem("fn-" + id); + let key = sessionStorage.getItem("key-" + id); + let filename = sessionStorage.getItem("fn-" + id); if (key === null || filename === null) { hash = window.location.hash.substr(1); @@ -20,8 +20,8 @@ function parseHashValue(id) { redirectToE2EError(); return; } - localStorage.setItem("key-" + id, info.c); - localStorage.setItem("fn-" + id, info.f); + sessionStorage.setItem("key-" + id, info.c); + sessionStorage.setItem("fn-" + id, info.f); } } diff --git a/internal/webserver/web/static/js/min/end2end_download.min.js b/internal/webserver/web/static/js/min/end2end_download.min.js index 2173f110..7f40697b 100644 --- a/internal/webserver/web/static/js/min/end2end_download.min.js +++ b/internal/webserver/web/static/js/min/end2end_download.min.js @@ -1 +1 @@ -function parseHashValue(e){let t=localStorage.getItem("key-"+e),n=localStorage.getItem("fn-"+e);if(t===null||n===null){if(hash=window.location.hash.substr(1),hash.length<50){redirectToE2EError();return}let t;try{let e=atob(hash);t=JSON.parse(e)}catch{redirectToE2EError();return}if(!isCorrectJson(t)){redirectToE2EError();return}localStorage.setItem("key-"+e,t.c),localStorage.setItem("fn-"+e,t.f)}}function isCorrectJson(e){return e.f!==void 0&&e.c!==void 0&&typeof e.f=="string"&&typeof e.c=="string"&&e.f!=""&&e.c!=""}function redirectToE2EError(){window.location="./error?e2e"} \ No newline at end of file +function parseHashValue(e){let t=sessionStorage.getItem("key-"+e),n=sessionStorage.getItem("fn-"+e);if(t===null||n===null){if(hash=window.location.hash.substr(1),hash.length<50){redirectToE2EError();return}let t;try{let e=atob(hash);t=JSON.parse(e)}catch{redirectToE2EError();return}if(!isCorrectJson(t)){redirectToE2EError();return}sessionStorage.setItem("key-"+e,t.c),sessionStorage.setItem("fn-"+e,t.f)}}function isCorrectJson(e){return e.f!==void 0&&e.c!==void 0&&typeof e.f=="string"&&typeof e.c=="string"&&e.f!=""&&e.c!=""}function redirectToE2EError(){window.location="./error?e2e"} \ No newline at end of file diff --git a/internal/webserver/web/templates/expired_file_svg.tmpl b/internal/webserver/web/templates/expired_file_svg.tmpl new file mode 100644 index 00000000..d216f46f --- /dev/null +++ b/internal/webserver/web/templates/expired_file_svg.tmpl @@ -0,0 +1,5 @@ + + + {{.PublicName}} + The requested image has expired + diff --git a/internal/webserver/web/templates/html_download.tmpl b/internal/webserver/web/templates/html_download.tmpl index ef0cdc0f..1a847cab 100644 --- a/internal/webserver/web/templates/html_download.tmpl +++ b/internal/webserver/web/templates/html_download.tmpl @@ -78,10 +78,10 @@ async function DownloadEncrypted() { try { {{ if .EndToEndEncryption }} - let key = localStorage.getItem("key-{{ .Id }}"); - localStorage.removeItem("key-{{ .Id }}"); - let filename = localStorage.getItem("fn-{{ .Id }}"); - localStorage.removeItem("fn-{{ .Id }}"); + let key = sessionStorage.getItem("key-{{ .Id }}"); + sessionStorage.removeItem("key-{{ .Id }}"); + let filename = sessionStorage.getItem("fn-{{ .Id }}"); + sessionStorage.removeItem("fn-{{ .Id }}"); {{ else }} let key = "{{ .Cipher }}"; {{ end }} @@ -141,7 +141,9 @@ {{ end }} {{ if .EndToEndEncryption }} {{ end }} diff --git a/internal/webserver/web/templates/html_header.tmpl b/internal/webserver/web/templates/html_header.tmpl index d1681fbd..53b72a32 100644 --- a/internal/webserver/web/templates/html_header.tmpl +++ b/internal/webserver/web/templates/html_header.tmpl @@ -13,7 +13,7 @@ {{ if .IsAdminView }} - {{template "app_name"}} Admin + {{.PublicName}} Admin @@ -31,7 +31,15 @@ } {{ else }} - {{template "app_name"}} + {{ if .IsDownloadView }} + {{ if .IsPasswordView }} + {{.PublicName}}: Password required + {{ else }} + {{.PublicName}}: {{.Name}} + {{end }} + {{ else }} + {{.PublicName}} + {{end }}