144 lines
4.4 KiB
Go
144 lines
4.4 KiB
Go
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)
|
|
}
|
|
} }
|