354 lines
9.0 KiB
Go
Executable File
354 lines
9.0 KiB
Go
Executable File
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"local/args"
|
|
"local/gziphttp"
|
|
"local/notes-server/notes/md"
|
|
"local/simpleserve/simpleserve"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
ENDPOINT_UPLOAD = "__upload__"
|
|
ENDPOINT_DELETE = "__delete__"
|
|
)
|
|
|
|
var (
|
|
fs *args.ArgSet
|
|
)
|
|
|
|
func main() {
|
|
fs = args.NewArgSet()
|
|
fs.Append(args.STRING, "p", "port to serve", "8100")
|
|
fs.Append(args.STRING, "u", "user:pass for basic auth", "")
|
|
fs.Append(args.BOOL, "md", "whether to render markdown as html", true)
|
|
fs.Append(args.BOOL, "ro", "read only mode", false)
|
|
fs.Append(args.BOOL, "https", "https only", false)
|
|
fs.Append(args.STRING, "md-css", "css to load for md", "/dev/null")
|
|
fs.Append(args.STRING, "md-class", "class to wrap md", "phb")
|
|
fs.Append(args.STRING, "d", "static path to serve", "./public")
|
|
if err := fs.Parse(); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
d := fs.Get("d").GetString()
|
|
userPass := fs.Get("u").GetString()
|
|
md := fs.Get("md").GetBool()
|
|
ro := fs.Get("ro").GetBool()
|
|
https := fs.Get("https").GetBool()
|
|
mdCss := fs.Get("md-css").GetString()
|
|
mdClass := fs.Get("md-class").GetString()
|
|
if mdCss != "" {
|
|
b, err := ioutil.ReadFile(mdCss)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
mdCss = fmt.Sprintf(`
|
|
<style>
|
|
body > div {
|
|
margin: auto;
|
|
overflow: auto;
|
|
}
|
|
.%s {
|
|
width: auto !important;
|
|
/*height: auto !important;*/
|
|
/*height: 100%% !important;
|
|
column-count: auto !important;
|
|
-webkit-column-count: auto !important;*/
|
|
overflow: auto !important;
|
|
background-repeat: repeat-x;
|
|
}
|
|
.%s h1 {
|
|
/*
|
|
column-span: none !important;
|
|
-webkit-column-span: none !important;
|
|
*/
|
|
}
|
|
.%s hr+table {
|
|
margin-top: 40px !important;
|
|
}
|
|
.%s nav {
|
|
/*
|
|
background: none !important;
|
|
*/
|
|
display: none;
|
|
}
|
|
%s
|
|
</style>`,
|
|
mdClass,
|
|
mdClass,
|
|
mdClass,
|
|
mdClass,
|
|
b,
|
|
)
|
|
}
|
|
p := strings.TrimPrefix(fs.Get("p").GetString(), ":")
|
|
|
|
http.Handle("/", http.HandlerFunc(handler(userPass, https, ro, d, md, mdCss, mdClass)))
|
|
|
|
log.Printf("Serving %s on HTTP port: %s\n", d, p)
|
|
|
|
log.Fatal(http.ListenAndServe(":"+p, nil))
|
|
}
|
|
|
|
func handler(userPass string, https, ro bool, d string, md bool, mdCss, mdClass string) http.HandlerFunc {
|
|
return httpsOnly(https, gzip(basicAuth(userPass, endpoints(ro, withDel(ro, withMD(d, md, mdCss, mdClass, fserve(d)))))))
|
|
}
|
|
|
|
func writeMeta(w http.ResponseWriter) {
|
|
fmt.Fprintf(w, `
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
</head>
|
|
`)
|
|
}
|
|
|
|
func writeForm(w http.ResponseWriter) {
|
|
fmt.Fprintf(w, `
|
|
<form enctype="multipart/form-data" action="./%s" method="post">
|
|
<input type="file" name="file" required/>
|
|
<input type="submit" value="upload"/>
|
|
</form>
|
|
`, ENDPOINT_UPLOAD)
|
|
}
|
|
|
|
func basicAuth(userPass string, foo http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if userPass != "" {
|
|
u, p, ok := r.BasicAuth()
|
|
if !ok || u+":"+p != userPass {
|
|
w.Header().Set("WWW-Authenticate", "Basic")
|
|
w.WriteHeader(401)
|
|
return
|
|
}
|
|
}
|
|
foo(w, r)
|
|
}
|
|
}
|
|
|
|
func httpsOnly(https bool, foo http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if https && r.URL.Scheme != "https" {
|
|
log.Printf("redirecting: %+v", r.URL)
|
|
r.URL.Scheme = "https"
|
|
http.Redirect(w, r, r.URL.String(), http.StatusSeeOther)
|
|
return
|
|
}
|
|
foo(w, r)
|
|
}
|
|
}
|
|
|
|
func gzip(foo http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if gziphttp.Can(r) {
|
|
gz := gziphttp.New(w)
|
|
defer gz.Close()
|
|
w = gz
|
|
}
|
|
foo(w, r)
|
|
}
|
|
}
|
|
|
|
func endpoints(ro bool, foo http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if isDir(r) {
|
|
writeMeta(w)
|
|
if !ro {
|
|
writeForm(w)
|
|
}
|
|
}
|
|
if !ro && isUploaded(r) {
|
|
if err := upload(w, r); err != nil {
|
|
fmt.Fprintln(w, err.Error())
|
|
}
|
|
} else if !ro && isDeleted(r) {
|
|
if err := del(w, r); err != nil {
|
|
fmt.Fprintln(w, err.Error())
|
|
}
|
|
} else {
|
|
simpleserve.SetContentTypeIfMedia(w, r)
|
|
foo(w, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func isUploaded(r *http.Request) bool {
|
|
return path.Base(r.URL.Path) == ENDPOINT_UPLOAD
|
|
}
|
|
|
|
func isDeleted(r *http.Request) bool {
|
|
return path.Base(r.URL.Path) == ENDPOINT_DELETE
|
|
}
|
|
|
|
func isDir(r *http.Request) bool {
|
|
d := toRealPath(r.URL.Path)
|
|
fi, err := os.Stat(d)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if !fi.IsDir() {
|
|
return false
|
|
}
|
|
if _, err := os.Stat(path.Join(d, "index.html")); err == nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func withMD(dir string, enabled bool, mdCss, mdClass string, foo http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
realpath := toRealPath(r.URL.Path)
|
|
if enabled && !isDir(r) && path.Ext(realpath) == ".md" {
|
|
b, err := ioutil.ReadFile(realpath)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
s, err := md.Gomarkdown(b, nil)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
fmt.Fprintln(w, mdCss)
|
|
fmt.Fprintf(w, "<div class=%q>", mdClass)
|
|
s = strings.ReplaceAll(s, "<nav>", `<nav class="toc">`)
|
|
fmt.Fprintln(w, s)
|
|
fmt.Fprintf(w, "</div>")
|
|
} else {
|
|
foo(w, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func withDel(ro bool, foo http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !isDir(r) {
|
|
foo(w, r)
|
|
return
|
|
}
|
|
fmt.Fprintln(w, `<a href=".."><input type="button" style="padding: .15em 4em .35em 4em" value=".."/></a>`)
|
|
w2 := httptest.NewRecorder()
|
|
foo(w2, r)
|
|
b := bytes.Split(w2.Body.Bytes(), []byte("\n"))
|
|
buff := bytes.NewBuffer(nil)
|
|
for i := range b {
|
|
if !ro && bytes.Contains(b[i], []byte("<a href=")) {
|
|
re := regexp.MustCompile(`href="[^"]*"`)
|
|
match := re.Find(b[i])
|
|
if len(match) > 0 {
|
|
match = bytes.Split(match, []byte(`href="`))[1]
|
|
match = match[:len(match)-1]
|
|
b[i] = []byte(fmt.Sprintf(`<a href="%s/%s"><input type="button" value="❌" style="padding: .40em 1em .10em 1em; margin-right: .5em" onclick='return confirm("Delete "+%q+"?");'></input></a> %s`, match, ENDPOINT_DELETE, match, b[i]))
|
|
}
|
|
}
|
|
buff.Write(b[i])
|
|
buff.Write([]byte("\n"))
|
|
}
|
|
io.Copy(w, buff)
|
|
}
|
|
}
|
|
|
|
func fserve(d string) http.HandlerFunc {
|
|
h := http.FileServer(http.Dir(d))
|
|
return h.ServeHTTP
|
|
}
|
|
|
|
func upload(w http.ResponseWriter, r *http.Request) error {
|
|
r.ParseMultipartForm(100 << 20)
|
|
file, handler, err := r.FormFile("file")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
p := toRealPath(path.Join(path.Dir(r.URL.Path), handler.Filename))
|
|
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
|
|
return errors.New("already exists")
|
|
}
|
|
f, err := os.Create(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
if _, err := io.Copy(f, file); err != nil {
|
|
return err
|
|
}
|
|
http.Redirect(w, r, path.Dir(r.URL.Path)+"/", http.StatusSeeOther)
|
|
return nil
|
|
}
|
|
|
|
func del(w http.ResponseWriter, r *http.Request) error {
|
|
p := toRealPath(path.Dir(r.URL.Path))
|
|
_, err := os.Stat(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.RemoveAll(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
http.Redirect(w, r, path.Dir(path.Dir(r.URL.Path))+"/", http.StatusSeeOther)
|
|
return nil
|
|
}
|
|
|
|
func toRealPath(p string) string {
|
|
d := path.Join(fs.Get("d").GetString())
|
|
return path.Join(d, p)
|
|
}
|
|
|
|
func setContentTypeIfMedia(w http.ResponseWriter, r *http.Request) {
|
|
switch strings.ToLower(path.Ext(r.URL.Path)) {
|
|
case ".mp4":
|
|
w.Header().Set("Content-Type", "video/mp4")
|
|
case ".mkv":
|
|
w.Header().Set("Content-Type", "video/x-matroska")
|
|
case ".mp3":
|
|
w.Header().Set("Content-Type", "audio/mpeg3")
|
|
case ".epub", ".mobi":
|
|
w.Header().Set("Content-Disposition", "attachment")
|
|
case ".jpg", ".jpeg":
|
|
w.Header().Set("Content-Type", "image/jpeg")
|
|
case ".gif":
|
|
w.Header().Set("Content-Type", "image/gif")
|
|
case ".png":
|
|
w.Header().Set("Content-Type", "image/png")
|
|
case ".ico":
|
|
w.Header().Set("Content-Type", "image/x-icon")
|
|
case ".svg":
|
|
w.Header().Set("Content-Type", "image/svg+xml")
|
|
case ".css":
|
|
w.Header().Set("Content-Type", "text/css")
|
|
case ".js":
|
|
w.Header().Set("Content-Type", "text/javascript")
|
|
case ".json":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
case ".html", ".htm":
|
|
w.Header().Set("Content-Type", "text/html")
|
|
case ".pdf":
|
|
w.Header().Set("Content-Type", "application/pdf")
|
|
case ".webm":
|
|
w.Header().Set("Content-Type", "video/webm")
|
|
case ".weba":
|
|
w.Header().Set("Content-Type", "audio/webm")
|
|
case ".webp":
|
|
w.Header().Set("Content-Type", "image/webp")
|
|
case ".zip":
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
case ".7z":
|
|
w.Header().Set("Content-Type", "application/x-7z-compressed")
|
|
case ".tar":
|
|
w.Header().Set("Content-Type", "application/x-tar")
|
|
}
|
|
}
|