package main import ( // "fmt" "golang.org/x/net/html" "mime" "net/http" "os" "path/filepath" "strings" "time" ) func LoggingWrapper(log *os.File, handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { handler(w, r) t := time.Now() log.WriteString(r.RemoteAddr + " " + t.Format(time.UnixDate) + " " + r.RequestURI + "\n") } } func GetHandler(config Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Security first! w.Header().Set("Strict-Transport-Security", "max-age=31536000") // Default to text/plain, as used for errors w.Header().Set("Content-Type", "text/plain") // Fail if request method is not GET or HEAD if r.Method != "GET" && r.Method != "HEAD" { http.Error(w, "Unimplemented HTTP method: use HEAD or GET.", http.StatusNotImplemented) return } // Fail if there are dots in the path if strings.Contains(r.URL.Path, "..") { http.Error(w, "Your directory traversal technique has been defeated!", http.StatusForbidden) return } // Resolve URI path to actual filesystem path path := r.URL.Path if strings.HasPrefix(path, "/~") { bits := strings.Split(r.URL.Path, "/") username := bits[1][1:] //strings.Replace(bits[1], "~", "", 1) new_prefix := filepath.Join("home", username, config.HomeDocBase) path = strings.Replace(path, bits[1], new_prefix, 1) } else { path = filepath.Join(config.DocBase, r.URL.Path) } //fmt.Println(path) // Fail if file does not exist info, err := os.Stat(path) if os.IsNotExist(err) { http.Error(w, "Not found.", http.StatusNotFound) return } if os.IsPermission(err) { http.Error(w, "Permission denied.", http.StatusForbidden) return } // Handle URLS which map to a directory if info.IsDir() { // Redirect to add trailing slash if missing // (otherwise relative links don't work properly) if !strings.HasSuffix(r.URL.Path, "/") { http.Redirect(w, r, r.URL.String()+"/", http.StatusMovedPermanently) return } // Add index.html to directory paths, if it exists index_path := filepath.Join(path, "index.html") index_info, err := os.Stat(index_path) if !os.IsNotExist(err) { path = index_path info = index_info } } // Fail if file is not world readable if uint64(info.Mode().Perm())&0444 != 0444 { http.Error(w, "File not world-readable.", http.StatusForbidden) return } // If this is a directory, nothing below makes sense, so just // serve it! if info.IsDir() { http.ServeFile(w, r, path) return } // Get MIME type ext := filepath.Ext(path) mime := mime.TypeByExtension(ext) if mime == "" { http.Error(w, "MIME type detection error.", http.StatusInternalServerError) return } // Fail if MIME type is forbidden _, ok := config.BadMimesMap[mime] if ok { http.Error(w, "Forbidden MIME type: "+mime, http.StatusForbidden) return } // Fail if file is too large if !strings.HasPrefix(mime, "text/plain") && ((strings.HasPrefix(mime, "text/") && info.Size() > 1024*1024) || info.Size() > 32*1024) { http.Error(w, "File too big: text/plain files have no limit, other text/* files may be up to 1 MiB, non-text files may be up to 32 KiB.", http.StatusForbidden) return } // Fail if a HTML file doesn't measure up if strings.HasPrefix(mime, "text/html") { reader, err := os.Open(path) if err != nil { http.Error(w, "Some kind of file error!", http.StatusInternalServerError) return } doc, err := html.Parse(reader) if err != nil { http.Error(w, "Some kind of HTML parsing error!", http.StatusInternalServerError) return } var imgcount int ok, msg := ValidateHtml(config, doc, 0, &imgcount) if ok == false { http.Error(w, "HTML document contains forbidden tag: "+msg, http.StatusForbidden) return } } // At this point everything is fine and we'll actually try to serve // the file. w.Header().Set("Content-Type", mime) http.ServeFile(w, r, path) } } func GetRedirectTLSHandler(config Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Don't redirect Let's Encrypt requests if strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") { path := filepath.Join(config.DocBase, r.URL.Path) http.ServeFile(w, r, path) // But redirect everything else } else { http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently) } } }