diff --git a/server/handlers.go b/server/handlers.go index 178c374..570e9a6 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -25,9 +25,6 @@ THE SOFTWARE. package server import ( - // _ "transfer.sh/app/handlers" - // _ "transfer.sh/app/utils" - "archive/tar" "archive/zip" "bytes" @@ -42,7 +39,6 @@ import ( "log" "math/rand" "mime" - "net" "net/http" "net/url" "os" @@ -62,8 +58,6 @@ import ( "github.com/skip2/go-qrcode" ) -const getPathPart = "get" - var ( htmlTemplates = initHTMLTemplates() textTemplates = initTextTemplates() @@ -90,18 +84,85 @@ func initHTMLTemplates() *html_template.Template { return templates } -func healthHandler(w http.ResponseWriter, r *http.Request) { +// Create a log handler for every request it receives. +func LoveHandler(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("x-made-with", "<3 by DutchCoders") + w.Header().Set("x-served-by", "Proudly served by DutchCoders") + w.Header().Set("Server", "Transfer.sh HTTP Server 1.0") + h.ServeHTTP(w, r) + } +} + +func IPFilterHandler(h http.Handler, ipFilterOptions *IPFilterOptions) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if ipFilterOptions == nil { + h.ServeHTTP(w, r) + } else { + WrapIPFilter(h, *ipFilterOptions).ServeHTTP(w, r) + } + return + } +} + +func (s *Server) RedirectHandler(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !s.forceHTTPs { + // we don't want to enforce https + } else if r.URL.Path == "/health.html" { + // health check url won't redirect + } else if strings.HasSuffix(ipAddrFromRemoteAddr(r.Host), ".onion") { + // .onion addresses cannot get a valid certificate, so don't redirect + } else if r.Header.Get("X-Forwarded-Proto") == "https" { + } else if r.URL.Scheme == "https" { + } else { + u := getURL(r) + u.Scheme = "https" + + http.Redirect(w, r, u.String(), http.StatusPermanentRedirect) + return + } + + h.ServeHTTP(w, r) + } +} + +func (s *Server) BasicAuthHandler(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if s.AuthUser == "" || s.AuthPass == "" { + h.ServeHTTP(w, r) + return + } + + w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"") + + username, password, authOK := r.BasicAuth() + if authOK == false { + http.Error(w, "Not authorized", 401) + return + } + + if username != s.AuthUser || password != s.AuthPass { + http.Error(w, "Not authorized", 401) + return + } + + h.ServeHTTP(w, r) + } +} + +func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("Approaching Neutral Zone, all systems normal and functioning.")) } -/* The preview handler will show a preview of the content for browsers (accept type text/html), and referer is not transfer.sh */ +// The preview handler will show a preview of the content for browsers (accept type text/html), and referer is not transfer.sh func (s *Server) previewHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) token := vars["token"] filename := vars["filename"] - metadata, err := s.CheckMetadata(token, filename, false) + metadata, err := s.checkMetadata(token, filename, false) if err != nil { log.Printf("Error metadata: %s", err.Error()) @@ -153,7 +214,7 @@ func (s *Server) previewHandler(w http.ResponseWriter, r *http.Request) { relativeURL, _ := url.Parse(path.Join(s.proxyPath, token, filename)) resolvedURL := resolveURL(r, relativeURL) - relativeURLGet, _ := url.Parse(path.Join(s.proxyPath, getPathPart, token, filename)) + relativeURLGet, _ := url.Parse(path.Join(s.proxyPath, "get", token, filename)) resolvedURLGet := resolveURL(r, relativeURLGet) var png []byte png, err = qrcode.Encode(resolvedURL, qrcode.High, 150) @@ -200,9 +261,7 @@ func (s *Server) previewHandler(w http.ResponseWriter, r *http.Request) { } -// this handler will output html or text, depending on the -// support of the client (Accept header). - +// this handler will output html or text, depending on the support of the client (Accept header). func (s *Server) viewHandler(w http.ResponseWriter, r *http.Request) { // vars := mux.Vars(r) @@ -238,10 +297,6 @@ func (s *Server) notFoundHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(404), 404) } -func sanitize(fileName string) string { - return path.Clean(path.Base(fileName)) -} - func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(_24K); nil != err { log.Printf("%s", err.Error()) @@ -321,46 +376,6 @@ func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) { } } -func cleanTmpFile(f *os.File) { - if f != nil { - err := f.Close() - if err != nil { - log.Printf("Error closing tmpfile: %s (%s)", err, f.Name()) - } - - err = os.Remove(f.Name()) - if err != nil { - log.Printf("Error removing tmpfile: %s (%s)", err, f.Name()) - } - } -} - -func (s *Server) metadataForRequest(contentType string, contentLength int64, r *http.Request) storage.Metadata { - metadata := storage.Metadata{ - ContentType: contentType, - ContentLength: contentLength, - MaxDate: time.Now().Add(s.lifetime), - Downloads: 0, - MaxDownloads: -1, - DeletionToken: Encode(10000000+int64(rand.Intn(1000000000))) + Encode(10000000+int64(rand.Intn(1000000000))), - } - - if v := r.Header.Get("Max-Downloads"); v == "" { - } else if v, err := strconv.Atoi(v); err != nil { - } else { - metadata.MaxDownloads = v - } - - if maxDays := r.Header.Get("Max-Days"); maxDays != "" { - v, err := strconv.Atoi(maxDays) - if err != nil { - return metadata - } - metadata.MaxDate = time.Now().Add(time.Hour * 24 * time.Duration(v)) - } - return metadata -} - func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -456,136 +471,6 @@ func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprint(w, resolveURL(r, relativeURL)) } -func resolveURL(r *http.Request, u *url.URL) string { - r.URL.Path = "" - - return getURL(r).ResolveReference(u).String() -} - -func resolveKey(key, proxyPath string) string { - if strings.HasPrefix(key, "/") { - key = key[1:] - } - - if strings.HasPrefix(key, proxyPath) { - key = key[len(proxyPath):] - } - - key = strings.Replace(key, "\\", "/", -1) - - return key -} - -func resolveWebAddress(r *http.Request, proxyPath string) string { - rUrl := getURL(r) - - var webAddress string - - if len(proxyPath) == 0 { - webAddress = fmt.Sprintf("%s://%s/", - rUrl.ResolveReference(rUrl).Scheme, - rUrl.ResolveReference(rUrl).Host) - } else { - webAddress = fmt.Sprintf("%s://%s/%s", - rUrl.ResolveReference(rUrl).Scheme, - rUrl.ResolveReference(rUrl).Host, - proxyPath) - } - - return webAddress -} - -func getURL(r *http.Request) *url.URL { - u, _ := url.Parse(r.URL.String()) - - if r.TLS != nil { - u.Scheme = "https" - } else if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { - u.Scheme = proto - } else { - u.Scheme = "http" - } - - if u.Host != "" { - } else if host, port, err := net.SplitHostPort(r.Host); err != nil { - u.Host = r.Host - } else { - if port == "80" && u.Scheme == "http" { - u.Host = host - } else if port == "443" && u.Scheme == "https" { - u.Host = host - } else { - u.Host = net.JoinHostPort(host, port) - } - } - - return u -} - -func (s *Server) Lock(token, filename string) { - key := path.Join(token, filename) - - if _, ok := s.locks[key]; !ok { - s.locks[key] = &sync.Mutex{} - } - - s.locks[key].Lock() -} - -func (s *Server) Unlock(token, filename string) { - key := path.Join(token, filename) - s.locks[key].Unlock() -} - -func (s *Server) CheckMetadata(token, filename string, increaseDownload bool) (metadata storage.Metadata, err error) { - s.Lock(token, filename) - defer s.Unlock(token, filename) - - metadata, err = s.storage.Head(token, filename) - if s.storage.IsNotExist(err) { - return metadata, nil - } else if err != nil { - return metadata, err - } - - if metadata.MaxDownloads != -1 && metadata.Downloads >= metadata.MaxDownloads { - return metadata, errors.New("max downloads exceeded") - } else if !metadata.MaxDate.IsZero() && time.Now().After(metadata.MaxDate) { - return metadata, errors.New("file access expired") - } else { - - // update number of downloads - if increaseDownload { - metadata.Downloads++ - } - - if err := s.storage.Meta(token, filename, metadata); err != nil { - return metadata, errors.New("could not save metadata") - } - } - - return metadata, nil -} - -func (s *Server) CheckDeletionToken(deletionToken, token, filename string) error { - s.Lock(token, filename) - defer s.Unlock(token, filename) - - metadata, err := s.storage.Head(token, filename) - if s.storage.IsNotExist(err) { - return nil - } - if err != nil { - return err - } - - if metadata.DeletionToken != deletionToken { - return errors.New("deletion token does not match") - } - - return nil -} - func (s *Server) deleteHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -593,7 +478,7 @@ func (s *Server) deleteHandler(w http.ResponseWriter, r *http.Request) { filename := vars["filename"] deletionToken := vars["deletionToken"] - if err := s.CheckDeletionToken(deletionToken, token, filename); err != nil { + if err := s.checkDeletionToken(deletionToken, token, filename); err != nil { log.Printf("Error metadata: %s", err.Error()) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return @@ -629,7 +514,7 @@ func (s *Server) zipHandler(w http.ResponseWriter, r *http.Request) { token := strings.Split(key, "/")[0] filename := sanitize(strings.Split(key, "/")[1]) - if _, err := s.CheckMetadata(token, filename, true); err != nil { + if _, err := s.checkMetadata(token, filename, true); err != nil { log.Printf("Error metadata: %s", err.Error()) continue } @@ -700,7 +585,7 @@ func (s *Server) tarGzHandler(w http.ResponseWriter, r *http.Request) { token := strings.Split(key, "/")[0] filename := sanitize(strings.Split(key, "/")[1]) - if _, err := s.CheckMetadata(token, filename, true); err != nil { + if _, err := s.checkMetadata(token, filename, true); err != nil { log.Printf("Error metadata: %s", err.Error()) continue } @@ -759,7 +644,7 @@ func (s *Server) tarHandler(w http.ResponseWriter, r *http.Request) { token := strings.Split(key, "/")[0] filename := strings.Split(key, "/")[1] - if _, err := s.CheckMetadata(token, filename, true); err != nil { + if _, err := s.checkMetadata(token, filename, true); err != nil { log.Printf("Error metadata: %s", err.Error()) continue } @@ -804,7 +689,7 @@ func (s *Server) headHandler(w http.ResponseWriter, r *http.Request) { token := vars["token"] filename := vars["filename"] - metadata, err := s.CheckMetadata(token, filename, false) + metadata, err := s.checkMetadata(token, filename, false) if err != nil { log.Printf("Error metadata: %s", err.Error()) @@ -837,7 +722,7 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) { token := vars["token"] filename := vars["filename"] - metadata, err := s.CheckMetadata(token, filename, true) + metadata, err := s.checkMetadata(token, filename, true) if err != nil { log.Printf("Error metadata: %s", err.Error()) @@ -911,69 +796,92 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) { http.ServeContent(w, r, filename, time.Now(), file) } -func (s *Server) RedirectHandler(h http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if !s.forceHTTPs { - // we don't want to enforce https - } else if r.URL.Path == "/health.html" { - // health check url won't redirect - } else if strings.HasSuffix(ipAddrFromRemoteAddr(r.Host), ".onion") { - // .onion addresses cannot get a valid certificate, so don't redirect - } else if r.Header.Get("X-Forwarded-Proto") == "https" { - } else if r.URL.Scheme == "https" { - } else { - u := getURL(r) - u.Scheme = "https" +func (s *Server) metadataForRequest(contentType string, contentLength int64, r *http.Request) storage.Metadata { + metadata := storage.Metadata{ + ContentType: contentType, + ContentLength: contentLength, + MaxDate: time.Now().Add(s.lifetime), + Downloads: 0, + MaxDownloads: -1, + DeletionToken: Encode(10000000+int64(rand.Intn(1000000000))) + Encode(10000000+int64(rand.Intn(1000000000))), + } - http.Redirect(w, r, u.String(), http.StatusPermanentRedirect) - return - } + if v := r.Header.Get("Max-Downloads"); v == "" { + } else if v, err := strconv.Atoi(v); err != nil { + } else { + metadata.MaxDownloads = v + } - h.ServeHTTP(w, r) + if maxDays := r.Header.Get("Max-Days"); maxDays != "" { + v, err := strconv.Atoi(maxDays) + if err != nil { + return metadata + } + metadata.MaxDate = time.Now().Add(time.Hour * 24 * time.Duration(v)) } + return metadata } -// Create a log handler for every request it receives. -func LoveHandler(h http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("x-made-with", "<3 by DutchCoders") - w.Header().Set("x-served-by", "Proudly served by DutchCoders") - w.Header().Set("Server", "Transfer.sh HTTP Server 1.0") - h.ServeHTTP(w, r) +func (s *Server) checkMetadata(token, filename string, increaseDownload bool) (metadata storage.Metadata, err error) { + s.Lock(token, filename) + defer s.Unlock(token, filename) + + metadata, err = s.storage.Head(token, filename) + if s.storage.IsNotExist(err) { + return metadata, nil + } else if err != nil { + return metadata, err } -} -func IPFilterHandler(h http.Handler, ipFilterOptions *IPFilterOptions) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if ipFilterOptions == nil { - h.ServeHTTP(w, r) - } else { - WrapIPFilter(h, *ipFilterOptions).ServeHTTP(w, r) + if metadata.MaxDownloads != -1 && metadata.Downloads >= metadata.MaxDownloads { + return metadata, errors.New("max downloads exceeded") + } else if !metadata.MaxDate.IsZero() && time.Now().After(metadata.MaxDate) { + return metadata, errors.New("file access expired") + } else { + + // update number of downloads + if increaseDownload { + metadata.Downloads++ + } + + if err := s.storage.Meta(token, filename, metadata); err != nil { + return metadata, errors.New("could not save metadata") } - return } + + return metadata, nil } -func (s *Server) BasicAuthHandler(h http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if s.AuthUser == "" || s.AuthPass == "" { - h.ServeHTTP(w, r) - return - } +func (s *Server) checkDeletionToken(deletionToken, token, filename string) error { + s.Lock(token, filename) + defer s.Unlock(token, filename) - w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"") + metadata, err := s.storage.Head(token, filename) + if s.storage.IsNotExist(err) { + return nil + } + if err != nil { + return err + } - username, password, authOK := r.BasicAuth() - if authOK == false { - http.Error(w, "Not authorized", 401) - return - } + if metadata.DeletionToken != deletionToken { + return errors.New("deletion token does not match") + } - if username != s.AuthUser || password != s.AuthPass { - http.Error(w, "Not authorized", 401) - return - } + return nil +} - h.ServeHTTP(w, r) +func (s *Server) Lock(token, filename string) { + key := path.Join(token, filename) + + if _, ok := s.locks[key]; !ok { + s.locks[key] = &sync.Mutex{} } + + s.locks[key].Lock() +} + +func (s *Server) Unlock(token, filename string) { + key := path.Join(token, filename) + s.locks[key].Unlock() } diff --git a/server/server.go b/server/server.go index 0280f67..c3bda9d 100644 --- a/server/server.go +++ b/server/server.go @@ -359,7 +359,7 @@ func (s *Server) Run() { r.HandleFunc("/{filename:(?:favicon\\.ico|robots\\.txt|health\\.html)}", s.BasicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT") - r.HandleFunc("/health.html", healthHandler).Methods("GET") + r.HandleFunc("/health.html", s.healthHandler).Methods("GET") r.HandleFunc("/", s.viewHandler).Methods("GET") r.HandleFunc("/({files:.*}).zip", s.zipHandler).Methods("GET") diff --git a/server/utils.go b/server/utils.go index b947f39..0d2c4dd 100644 --- a/server/utils.go +++ b/server/utils.go @@ -25,16 +25,106 @@ THE SOFTWARE. package server import ( + "fmt" + "log" "math" + "net" "net/http" "net/mail" + "net/url" + "os" + "path" "strconv" "strings" "github.com/golang/gddo/httputil/header" ) -func formatNumber(format string, s uint64) string { +func cleanTmpFile(f *os.File) { + if f != nil { + err := f.Close() + if err != nil { + log.Printf("Error closing tmpfile: %s (%s)", err, f.Name()) + } + + err = os.Remove(f.Name()) + if err != nil { + log.Printf("Error removing tmpfile: %s (%s)", err, f.Name()) + } + } +} + +func sanitize(fileName string) string { + return path.Clean(path.Base(fileName)) +} + +func resolveURL(r *http.Request, u *url.URL) string { + r.URL.Path = "" + + return getURL(r).ResolveReference(u).String() +} + +func resolveKey(key, proxyPath string) string { + if strings.HasPrefix(key, "/") { + key = key[1:] + } + + if strings.HasPrefix(key, proxyPath) { + key = key[len(proxyPath):] + } + + key = strings.Replace(key, "\\", "/", -1) + + return key +} + +func resolveWebAddress(r *http.Request, proxyPath string) string { + rUrl := getURL(r) + + var webAddress string + + if len(proxyPath) == 0 { + webAddress = fmt.Sprintf("%s://%s/", + rUrl.ResolveReference(rUrl).Scheme, + rUrl.ResolveReference(rUrl).Host) + } else { + webAddress = fmt.Sprintf("%s://%s/%s", + rUrl.ResolveReference(rUrl).Scheme, + rUrl.ResolveReference(rUrl).Host, + proxyPath) + } + + return webAddress +} + +func getURL(r *http.Request) *url.URL { + u, _ := url.Parse(r.URL.String()) + + if r.TLS != nil { + u.Scheme = "https" + } else if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { + u.Scheme = proto + } else { + u.Scheme = "http" + } + + if u.Host != "" { + } else if host, port, err := net.SplitHostPort(r.Host); err != nil { + u.Host = r.Host + } else { + if port == "80" && u.Scheme == "http" { + u.Host = host + } else if port == "443" && u.Scheme == "https" { + u.Host = host + } else { + u.Host = net.JoinHostPort(host, port) + } + } + + return u +} + +func formatNumber(format string, s int64) string { return RenderFloat(format, float64(s)) } @@ -187,10 +277,6 @@ func RenderFloat(format string, n float64) string { return signStr + intStr + decimalStr + fracStr } -func RenderInteger(format string, n int) string { - return RenderFloat(format, float64(n)) -} - // Request.RemoteAddress contains port, which we want to remove i.e.: // "[::1]:58292" => "[::1]" func ipAddrFromRemoteAddr(s string) string {