345 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			345 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
package http
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"html/template"
 | 
						|
	"log"
 | 
						|
	"net/http"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	fm "github.com/hacdias/filemanager"
 | 
						|
)
 | 
						|
 | 
						|
// Handler returns a function compatible with http.HandleFunc.
 | 
						|
func Handler(m *fm.FileManager) http.Handler {
 | 
						|
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		code, err := serve(&fm.Context{
 | 
						|
			FileManager: m,
 | 
						|
			User:        nil,
 | 
						|
			File:        nil,
 | 
						|
		}, w, r)
 | 
						|
 | 
						|
		if code >= 400 {
 | 
						|
			w.WriteHeader(code)
 | 
						|
 | 
						|
			txt := http.StatusText(code)
 | 
						|
			log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
 | 
						|
			w.Write([]byte(txt + "\n"))
 | 
						|
		}
 | 
						|
 | 
						|
		if err != nil {
 | 
						|
			log.Print(err)
 | 
						|
		}
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// serve is the main entry point of this HTML application.
 | 
						|
func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
 | 
						|
	// Checks if the URL contains the baseURL and strips it. Otherwise, it just
 | 
						|
	// returns a 404 fm.Error because we're not supposed to be here!
 | 
						|
	p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
 | 
						|
 | 
						|
	if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
 | 
						|
		return http.StatusNotFound, nil
 | 
						|
	}
 | 
						|
 | 
						|
	r.URL.Path = p
 | 
						|
 | 
						|
	// Check if this request is made to the service worker. If so,
 | 
						|
	// pass it through a template to add the needed variables.
 | 
						|
	if r.URL.Path == "/sw.js" {
 | 
						|
		return renderFile(c, w, "sw.js")
 | 
						|
	}
 | 
						|
 | 
						|
	// Checks if this request is made to the static assets folder. If so, and
 | 
						|
	// if it is a GET request, returns with the asset. Otherwise, returns
 | 
						|
	// a status not implemented.
 | 
						|
	if matchURL(r.URL.Path, "/static") {
 | 
						|
		if r.Method != http.MethodGet {
 | 
						|
			return http.StatusNotImplemented, nil
 | 
						|
		}
 | 
						|
 | 
						|
		return staticHandler(c, w, r)
 | 
						|
	}
 | 
						|
 | 
						|
	// Checks if this request is made to the API and directs to the
 | 
						|
	// API handler if so.
 | 
						|
	if matchURL(r.URL.Path, "/api") {
 | 
						|
		r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
 | 
						|
		return apiHandler(c, w, r)
 | 
						|
	}
 | 
						|
 | 
						|
	// If it is a request to the preview and a static website generator is
 | 
						|
	// active, build the preview.
 | 
						|
	if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil {
 | 
						|
		r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview")
 | 
						|
		return c.StaticGen.Preview(c, w, r)
 | 
						|
	}
 | 
						|
 | 
						|
	if strings.HasPrefix(r.URL.Path, "/share/") {
 | 
						|
		r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
 | 
						|
		return sharePage(c, w, r)
 | 
						|
	}
 | 
						|
 | 
						|
	// Any other request should show the index.html file.
 | 
						|
	w.Header().Set("x-frame-options", "SAMEORIGIN")
 | 
						|
	w.Header().Set("x-content-type", "nosniff")
 | 
						|
	w.Header().Set("x-xss-protection", "1; mode=block")
 | 
						|
 | 
						|
	return renderFile(c, w, "index.html")
 | 
						|
}
 | 
						|
 | 
						|
// staticHandler handles the static assets path.
 | 
						|
func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
 | 
						|
	if r.URL.Path != "/static/manifest.json" {
 | 
						|
		http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
 | 
						|
		return 0, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return renderFile(c, w, "static/manifest.json")
 | 
						|
}
 | 
						|
 | 
						|
// apiHandler is the main entry point for the /api endpoint.
 | 
						|
func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
 | 
						|
	if r.URL.Path == "/auth/get" {
 | 
						|
		return authHandler(c, w, r)
 | 
						|
	}
 | 
						|
 | 
						|
	if r.URL.Path == "/auth/renew" {
 | 
						|
		return renewAuthHandler(c, w, r)
 | 
						|
	}
 | 
						|
 | 
						|
	valid, _ := validateAuth(c, r)
 | 
						|
	if !valid {
 | 
						|
		return http.StatusForbidden, nil
 | 
						|
	}
 | 
						|
 | 
						|
	c.Router, r.URL.Path = splitURL(r.URL.Path)
 | 
						|
 | 
						|
	if !c.User.Allowed(r.URL.Path) {
 | 
						|
		return http.StatusForbidden, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if c.StaticGen != nil {
 | 
						|
		// If we are using the 'magic url' for the settings,
 | 
						|
		// we should redirect the request for the acutual path.
 | 
						|
		if r.URL.Path == "/settings" {
 | 
						|
			r.URL.Path = c.StaticGen.SettingsPath()
 | 
						|
		}
 | 
						|
 | 
						|
		// Executes the Static website generator hook.
 | 
						|
		code, err := c.StaticGen.Hook(c, w, r)
 | 
						|
		if code != 0 || err != nil {
 | 
						|
			return code, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if c.Router == "checksum" || c.Router == "download" {
 | 
						|
		var err error
 | 
						|
		c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User)
 | 
						|
		if err != nil {
 | 
						|
			return ErrorToHTTP(err, false), err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	var code int
 | 
						|
	var err error
 | 
						|
 | 
						|
	switch c.Router {
 | 
						|
	case "download":
 | 
						|
		code, err = downloadHandler(c, w, r)
 | 
						|
	case "checksum":
 | 
						|
		code, err = checksumHandler(c, w, r)
 | 
						|
	case "command":
 | 
						|
		code, err = command(c, w, r)
 | 
						|
	case "search":
 | 
						|
		code, err = search(c, w, r)
 | 
						|
	case "resource":
 | 
						|
		code, err = resourceHandler(c, w, r)
 | 
						|
	case "users":
 | 
						|
		code, err = usersHandler(c, w, r)
 | 
						|
	case "settings":
 | 
						|
		code, err = settingsHandler(c, w, r)
 | 
						|
	case "share":
 | 
						|
		code, err = shareHandler(c, w, r)
 | 
						|
	default:
 | 
						|
		code = http.StatusNotFound
 | 
						|
	}
 | 
						|
 | 
						|
	return code, err
 | 
						|
}
 | 
						|
 | 
						|
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
 | 
						|
func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
 | 
						|
	query := r.URL.Query().Get("algo")
 | 
						|
 | 
						|
	val, err := c.File.Checksum(query)
 | 
						|
	if err == fm.ErrInvalidOption {
 | 
						|
		return http.StatusBadRequest, err
 | 
						|
	} else if err != nil {
 | 
						|
		return http.StatusInternalServerError, err
 | 
						|
	}
 | 
						|
 | 
						|
	w.Write([]byte(val))
 | 
						|
	return 0, nil
 | 
						|
}
 | 
						|
 | 
						|
// splitURL splits the path and returns everything that stands
 | 
						|
// before the first slash and everything that goes after.
 | 
						|
func splitURL(path string) (string, string) {
 | 
						|
	if path == "" {
 | 
						|
		return "", ""
 | 
						|
	}
 | 
						|
 | 
						|
	path = strings.TrimPrefix(path, "/")
 | 
						|
 | 
						|
	i := strings.Index(path, "/")
 | 
						|
	if i == -1 {
 | 
						|
		return "", path
 | 
						|
	}
 | 
						|
 | 
						|
	return path[0:i], path[i:]
 | 
						|
}
 | 
						|
 | 
						|
// renderFile renders a file using a template with some needed variables.
 | 
						|
func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) {
 | 
						|
	tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file)))
 | 
						|
 | 
						|
	var contentType string
 | 
						|
	switch filepath.Ext(file) {
 | 
						|
	case ".html":
 | 
						|
		contentType = "text/html"
 | 
						|
	case ".js":
 | 
						|
		contentType = "application/javascript"
 | 
						|
	case ".json":
 | 
						|
		contentType = "application/json"
 | 
						|
	default:
 | 
						|
		contentType = "text"
 | 
						|
	}
 | 
						|
 | 
						|
	w.Header().Set("Content-Type", contentType+"; charset=utf-8")
 | 
						|
 | 
						|
	data := map[string]interface{}{
 | 
						|
		"BaseURL":         c.RootURL(),
 | 
						|
		"NoAuth":          c.NoAuth,
 | 
						|
		"Version":         fm.Version,
 | 
						|
		"CSS":             template.CSS(c.CSS),
 | 
						|
		"ReCaptcha":       c.ReCaptchaKey != "" && c.ReCaptchaSecret != "",
 | 
						|
		"ReCaptchaKey":    c.ReCaptchaKey,
 | 
						|
		"ReCaptchaSecret": c.ReCaptchaSecret,
 | 
						|
	}
 | 
						|
 | 
						|
	if c.StaticGen != nil {
 | 
						|
		data["StaticGen"] = c.StaticGen.Name()
 | 
						|
	}
 | 
						|
 | 
						|
	err := tpl.Execute(w, data)
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		return http.StatusInternalServerError, err
 | 
						|
	}
 | 
						|
 | 
						|
	return 0, nil
 | 
						|
}
 | 
						|
 | 
						|
// sharePage build the share page.
 | 
						|
func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
 | 
						|
	s, err := c.Store.Share.Get(r.URL.Path)
 | 
						|
	if err == fm.ErrNotExist {
 | 
						|
		w.WriteHeader(http.StatusNotFound)
 | 
						|
		return renderFile(c, w, "static/share/404.html")
 | 
						|
	}
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		return http.StatusInternalServerError, err
 | 
						|
	}
 | 
						|
 | 
						|
	if s.Expires && s.ExpireDate.Before(time.Now()) {
 | 
						|
		c.Store.Share.Delete(s.Hash)
 | 
						|
		w.WriteHeader(http.StatusNotFound)
 | 
						|
		return renderFile(c, w, "static/share/404.html")
 | 
						|
	}
 | 
						|
 | 
						|
	r.URL.Path = s.Path
 | 
						|
 | 
						|
	info, err := os.Stat(s.Path)
 | 
						|
	if err != nil {
 | 
						|
		c.Store.Share.Delete(s.Hash)
 | 
						|
		return ErrorToHTTP(err, false), err
 | 
						|
	}
 | 
						|
 | 
						|
	c.File = &fm.File{
 | 
						|
		Path:    s.Path,
 | 
						|
		Name:    info.Name(),
 | 
						|
		ModTime: info.ModTime(),
 | 
						|
		Mode:    info.Mode(),
 | 
						|
		IsDir:   info.IsDir(),
 | 
						|
		Size:    info.Size(),
 | 
						|
	}
 | 
						|
 | 
						|
	dl := r.URL.Query().Get("dl")
 | 
						|
 | 
						|
	if dl == "" || dl == "0" {
 | 
						|
		tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html")))
 | 
						|
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
 | 
						|
 | 
						|
		err := tpl.Execute(w, map[string]interface{}{
 | 
						|
			"BaseURL": c.RootURL(),
 | 
						|
			"File":    c.File,
 | 
						|
		})
 | 
						|
 | 
						|
		if err != nil {
 | 
						|
			return http.StatusInternalServerError, err
 | 
						|
		}
 | 
						|
		return 0, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return downloadHandler(c, w, r)
 | 
						|
}
 | 
						|
 | 
						|
// renderJSON prints the JSON version of data to the browser.
 | 
						|
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
 | 
						|
	marsh, err := json.Marshal(data)
 | 
						|
	if err != nil {
 | 
						|
		return http.StatusInternalServerError, err
 | 
						|
	}
 | 
						|
 | 
						|
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
 | 
						|
	if _, err := w.Write(marsh); err != nil {
 | 
						|
		return http.StatusInternalServerError, err
 | 
						|
	}
 | 
						|
 | 
						|
	return 0, nil
 | 
						|
}
 | 
						|
 | 
						|
// matchURL checks if the first URL matches the second.
 | 
						|
func matchURL(first, second string) bool {
 | 
						|
	first = strings.ToLower(first)
 | 
						|
	second = strings.ToLower(second)
 | 
						|
 | 
						|
	return strings.HasPrefix(first, second)
 | 
						|
}
 | 
						|
 | 
						|
// ErrorToHTTP converts errors to HTTP Status Code.
 | 
						|
func ErrorToHTTP(err error, gone bool) int {
 | 
						|
	switch {
 | 
						|
	case err == nil:
 | 
						|
		return http.StatusOK
 | 
						|
	case os.IsPermission(err):
 | 
						|
		return http.StatusForbidden
 | 
						|
	case os.IsNotExist(err):
 | 
						|
		if !gone {
 | 
						|
			return http.StatusNotFound
 | 
						|
		}
 | 
						|
 | 
						|
		return http.StatusGone
 | 
						|
	case os.IsExist(err):
 | 
						|
		return http.StatusConflict
 | 
						|
	default:
 | 
						|
		return http.StatusInternalServerError
 | 
						|
	}
 | 
						|
}
 |