diff --git a/README.md b/README.md index f2f49e3..04bd345 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This project tries to replace all file servers that use python, since there are * Users can view a list of the uploaded files by visiting the root URL * Basic authentication is available to restrict access to the server. To use it, set the -user and -pass flags with the desired username and password. * Traffic via HTTPS. +* functionality: Generate a self-signed certificate by setting the -ssl flag. * Possibility to browse through folders and upload files... @@ -56,17 +57,17 @@ podman run --name upgopher -p 9090:9090 upgopher ./upgopher -h Usage of ./upgopher: -cert string - certificado para HTTPS + HTTPS certificate -dir string directory path (default "./uploads") -key string - clave privada para HTTPS + private key for HTTPS -pass string password for authentication -port int port number (default 9090) - -tls - utilizar HTTPS + -ssl + use HTTPS on port 443 by default. (If you don't put cert and key, it will generate a self-signed certificate) -user string username for authentication ``` diff --git a/upgopher.go b/upgopher.go index d282d36..54ead4a 100644 --- a/upgopher.go +++ b/upgopher.go @@ -2,19 +2,28 @@ package main import ( "archive/zip" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "crypto/subtle" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "embed" "encoding/base64" + "encoding/pem" "flag" "fmt" "html" "io" "io/fs" "log" + "math/big" "net/http" "os" "path/filepath" "strings" + "time" "github.com/wanetty/upgopher/internal/statics" ) @@ -22,50 +31,52 @@ import ( //go:embed static/favicon.ico var favicon embed.FS -func main() { - port := flag.Int("port", 9090, "port number") - dir := flag.String("dir", "./uploads", "directory path") - user := flag.String("user", "", "username for authentication") - pass := flag.String("pass", "", "password for authentication") - useTLS := flag.Bool("tls", false, "use HTTPS") - certFile := flag.String("cert", "", "HTTPS certificate") - keyFile := flag.String("key", "", "private key for HTTPS") - flag.Parse() - - if _, err := os.Stat(*dir); os.IsNotExist(err) { - os.MkdirAll(*dir, 0755) - } - - fileHandlerWithDir := func(w http.ResponseWriter, r *http.Request) { - fileHandler(w, r, *dir) +// Handlers ////////////////////////////////////////////////// +func fileHandlerWithDir(dir string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + fileHandler(w, r, dir) } +} +func rawHandler(dir string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return_code := "200" + path := strings.TrimPrefix(r.URL.Path, "/raw/") + fullPath := filepath.Join(dir, path) - rawHandlerWithDir := rawHandler(*dir) + isSafe, err := isSafePath(dir, fullPath) + if err != nil || !isSafe { + http.Error(w, "Bad path", http.StatusForbidden) + return_code = "403" + log.Printf("[%s - %s] %s %s\n", r.Method, return_code, r.URL.Path, r.RemoteAddr) + return + } - if *useTLS && (*certFile == "" || *keyFile == "") { - log.Fatalf("Must provide certificate and private key to use TLS") - } + fileInfo, err := os.Stat(fullPath) + if os.IsNotExist(err) || fileInfo.IsDir() { + http.Error(w, "File not found", http.StatusNotFound) + return_code = "404" + log.Printf("[%s - %s] %s %s\n", r.Method, return_code, r.URL.Path, r.RemoteAddr) + return + } - if (*user != "" && *pass == "") || (*user == "" && *pass != "") { - log.Fatalf("If you use the username or password you have to use both.") - return + log.Printf("[%s - %s] %s %s\n", r.Method, return_code, r.URL.Path, r.RemoteAddr) + http.ServeFile(w, r, fullPath) } +} - uploadHandler := func(w http.ResponseWriter, r *http.Request) { +func uploadHandler(dir string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { log.Printf("[%s] %s%s %s\n", r.Method, "/download/", r.URL.String(), r.RemoteAddr) encodedFilePath := r.URL.Query().Get("path") - decodedFilePath, err := base64.StdEncoding.DecodeString(encodedFilePath) - if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - fullFilePath := filepath.Join(*dir, string(decodedFilePath)) - - isSafe, err := isSafePath(*dir, fullFilePath) + fullFilePath := filepath.Join(dir, string(decodedFilePath)) + isSafe, err := isSafePath(dir, fullFilePath) if err != nil || !isSafe { http.Error(w, "Bad path", http.StatusForbidden) log.Printf("[%s - %s] %s %s\n", r.Method, "403", r.URL.Path, r.RemoteAddr) @@ -77,30 +88,25 @@ func main() { return } - // Extract filename from the full file path _, filename := filepath.Split(fullFilePath) - - // Set the 'Content-Disposition' header so the downloaded file has the original filename w.Header().Set("Content-Disposition", "attachment; filename="+filename) - http.ServeFile(w, r, fullFilePath) } +} - deleteHandler := func(w http.ResponseWriter, r *http.Request) { +func deleteHandler(dir string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { log.Printf("[%s] %s%s %s\n", r.Method, "/delete/", r.URL.String(), r.RemoteAddr) encodedFilePath := r.URL.Query().Get("path") - decodedFilePath, err := base64.StdEncoding.DecodeString(encodedFilePath) - if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - fullFilePath := filepath.Join(*dir, string(decodedFilePath)) - - isSafe, err := isSafePath(*dir, fullFilePath) + fullFilePath := filepath.Join(dir, string(decodedFilePath)) + isSafe, err := isSafePath(dir, fullFilePath) if err != nil || !isSafe { http.Error(w, "Bad path", http.StatusForbidden) log.Printf("[%s - %s] %s %s\n", r.Method, "403", r.URL.Path, r.RemoteAddr) @@ -112,9 +118,7 @@ func main() { return } - // Remove the file err = os.Remove(fullFilePath) - if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -122,90 +126,169 @@ func main() { if encodedFilePath == "" { http.Redirect(w, r, "/", http.StatusSeeOther) - } else { dirPath, _ := filepath.Split(string(decodedFilePath)) encodedDirPath := base64.StdEncoding.EncodeToString([]byte(dirPath)) http.Redirect(w, r, "/?path="+encodedDirPath, http.StatusSeeOther) } + } +} +func zipHandler(dir string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + currentPath := r.URL.Query().Get("path") + zipFilename, err := ZipFiles(dir, currentPath) + if err != nil { + http.Error(w, "Unable to create zip file", http.StatusInternalServerError) + return + } + defer os.Remove(zipFilename) + w.Header().Set("Content-Disposition", "attachment; filename=files.zip") + w.Header().Set("Content-Type", "application/zip") + http.ServeFile(w, r, zipFilename) + } +} + +func faviconHandler(w http.ResponseWriter, r *http.Request) { + faviconData, err := favicon.ReadFile("static/favicon.ico") + if err != nil { + http.Error(w, "Favicon not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "image/x-icon") + w.Write(faviconData) +} + +func applyBasicAuth(handler http.HandlerFunc, user, pass string) http.HandlerFunc { + userByte := []byte(user) + passByte := []byte(pass) + return basicAuth(handler, userByte, passByte) +} + +// Main ///////////////////////////////////////////////// +func main() { + port := flag.Int("port", 9090, "port number") + dir := flag.String("dir", "./uploads", "directory path") + user := flag.String("user", "", "username for authentication") + pass := flag.String("pass", "", "password for authentication") + useTLS := flag.Bool("ssl", false, "use HTTPS on port 443 by default. (If you don't put cert and key, it will generate a self-signed certificate)") + certFile := flag.String("cert", "", "HTTPS certificate") + keyFile := flag.String("key", "", "private key for HTTPS") + flag.Parse() + log.Printf("Executin version v1.6.0") + + if _, err := os.Stat(*dir); os.IsNotExist(err) { + os.MkdirAll(*dir, 0755) + } + + fileHandler := fileHandlerWithDir(*dir) + uploadHandler := uploadHandler(*dir) + deleteHandler := deleteHandler(*dir) + rawHandler := rawHandler(*dir) + zipHandler := zipHandler(*dir) + + if (*user != "" && *pass == "") || (*user == "" && *pass != "") { + log.Fatalf("If you use the username or password you have to use both.") + return } if *user != "" && *pass != "" { - userByte := []byte(*user) - passByte := []byte(*pass) - http.HandleFunc("/", basicAuth(fileHandlerWithDir, userByte, passByte)) - http.Handle("/delete/", http.StripPrefix("/delete/", basicAuth(http.HandlerFunc(deleteHandler), userByte, passByte))) - http.Handle("/download/", http.StripPrefix("/download/", basicAuth(http.HandlerFunc(uploadHandler), userByte, passByte))) - http.Handle("/raw/", http.StripPrefix("/raw/", basicAuth(rawHandlerWithDir, userByte, passByte))) - http.HandleFunc("/favicon.ico", basicAuth(func(w http.ResponseWriter, r *http.Request) { - faviconData, err := favicon.ReadFile("static/favicon.ico") + http.HandleFunc("/", applyBasicAuth(fileHandler, *user, *pass)) + http.Handle("/delete/", http.StripPrefix("/delete/", applyBasicAuth(deleteHandler, *user, *pass))) + http.Handle("/download/", http.StripPrefix("/download/", applyBasicAuth(uploadHandler, *user, *pass))) + http.Handle("/raw/", http.StripPrefix("/raw/", applyBasicAuth(rawHandler, *user, *pass))) + http.HandleFunc("/favicon.ico", applyBasicAuth(faviconHandler, *user, *pass)) + http.HandleFunc("/zip", applyBasicAuth(zipHandler, *user, *pass)) + } else { + http.HandleFunc("/", fileHandler) + http.Handle("/delete/", http.StripPrefix("/delete/", deleteHandler)) + http.Handle("/download/", http.StripPrefix("/download/", uploadHandler)) + http.Handle("/raw/", http.StripPrefix("/raw/", rawHandler)) + http.HandleFunc("/favicon.ico", faviconHandler) + http.HandleFunc("/zip", zipHandler) + } + if !isFlagPassed("port") && *useTLS { + *port = 443 + } + addr := fmt.Sprintf(":%d", *port) + startServer(addr, *useTLS, *certFile, *keyFile, *port) +} + +func startServer(addr string, useTLS bool, certFile, keyFile string, port int) { + if useTLS { + var cert tls.Certificate + var err error + + if certFile != "" && keyFile != "" { + cert, err = tls.LoadX509KeyPair(certFile, keyFile) if err != nil { - http.Error(w, "Favicon not found", http.StatusNotFound) - return + log.Fatalf("Failed to load certificate and key pair: %v", err) } - w.Header().Set("Content-Type", "image/x-icon") - w.Write(faviconData) - }, userByte, passByte)) - http.HandleFunc("/zip", basicAuth(func(w http.ResponseWriter, r *http.Request) { - currentPath := r.URL.Query().Get("path") - zipFilename, err := ZipFiles(*dir, currentPath) + } else { + log.Println("No certificate or key file provided, generating a self-signed certificate.") + certPEM, keyPEM, err := generateSelfSignedCert() if err != nil { - http.Error(w, "Unable to create zip file", http.StatusInternalServerError) - return + log.Fatalf("Failed to generate self-signed certificate: %v", err) } - defer os.Remove(zipFilename) - w.Header().Set("Content-Disposition", "attachment; filename=files.zip") - w.Header().Set("Content-Type", "application/zip") - - http.ServeFile(w, r, zipFilename) - }, userByte, passByte)) - } else { - http.HandleFunc("/", fileHandlerWithDir) - http.Handle("/delete/", http.StripPrefix("/delete/", http.HandlerFunc(deleteHandler))) - http.Handle("/download/", http.StripPrefix("/download/", http.HandlerFunc(uploadHandler))) - http.Handle("/raw/", http.StripPrefix("/raw/", rawHandlerWithDir)) - http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { - faviconData, err := favicon.ReadFile("static/favicon.ico") + cert, err = tls.X509KeyPair(certPEM, keyPEM) if err != nil { - http.Error(w, "Favicon not found", http.StatusNotFound) - return + log.Fatalf("Failed to create key pair from generated self-signed certificate: %v", err) } - w.Header().Set("Content-Type", "image/x-icon") - w.Write(faviconData) - }) - http.HandleFunc("/zip", func(w http.ResponseWriter, r *http.Request) { - currentPath := r.URL.Query().Get("path") - zipFilename, err := ZipFiles(*dir, currentPath) - if err != nil { - http.Error(w, "Unable to create zip file", http.StatusInternalServerError) - return - } - defer os.Remove(zipFilename) - w.Header().Set("Content-Disposition", "attachment; filename=files.zip") - w.Header().Set("Content-Type", "application/zip") - http.ServeFile(w, r, zipFilename) - }) - } + } - addr := fmt.Sprintf(":%d", *port) - log.Printf("Web server on %s", addr) - log.Printf("Executin version v1.5.1") + server := &http.Server{ + Addr: addr, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + } - if *useTLS { - log.Printf("Usando TLS") - if err := http.ListenAndServeTLS(addr, *certFile, *keyFile, nil); err != nil { + log.Printf("Starting HTTPS server on %s", addr) + if err := server.ListenAndServeTLS("", ""); err != nil { log.Fatalf("Error starting HTTPS server: %v", err) } } else { - log.Printf("Using HTTP") + log.Printf("Starting HTTP server on %s", addr) if err := http.ListenAndServe(addr, nil); err != nil { log.Fatalf("Error starting HTTP server: %v", err) } } } +func generateSelfSignedCert() ([]byte, []byte, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Self-signed"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, nil, err + } + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + keyDER, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, nil, err + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + + return certPEM, keyPEM, nil +} + func isSafePath(baseDir, userPath string) (bool, error) { absBaseDir, err := filepath.Abs(baseDir) if err != nil { @@ -220,33 +303,6 @@ func isSafePath(baseDir, userPath string) (bool, error) { return strings.HasPrefix(absUserPath, absBaseDir), nil } -func rawHandler(dir string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - return_code := "200" - path := strings.TrimPrefix(r.URL.Path, "/raw/") - fullPath := filepath.Join(dir, path) - - isSafe, err := isSafePath(dir, fullPath) - if err != nil || !isSafe { - http.Error(w, "Bad path", http.StatusForbidden) - return_code = "403" - log.Printf("[%s - %s] %s %s\n", r.Method, return_code, r.URL.Path, r.RemoteAddr) - return - } - - fileInfo, err := os.Stat(fullPath) - if os.IsNotExist(err) || fileInfo.IsDir() { - http.Error(w, "File not found", http.StatusNotFound) - return_code = "404" - log.Printf("[%s - %s] %s %s\n", r.Method, return_code, r.URL.Path, r.RemoteAddr) - return - } - - log.Printf("[%s - %s] %s %s\n", r.Method, return_code, r.URL.Path, r.RemoteAddr) - http.ServeFile(w, r, fullPath) - } -} - func basicAuth(handler http.HandlerFunc, username, password []byte) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() @@ -477,3 +533,13 @@ func addFileToZip(zipWriter *zip.Writer, filename string) error { _, err = io.Copy(wr, file) return err } + +func isFlagPassed(name string) bool { + found := false + flag.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + return found +}