summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@sbcglobal.net>2016-11-01 14:46:15 -0400
committerLuke Shumaker <lukeshu@sbcglobal.net>2016-11-01 14:46:15 -0400
commitc7eea383aeaf6748daf994e9e28e4d0c25350736 (patch)
treea0979cbd4e6be06385fd0340e050147fdacd6e35
initial commit
-rw-r--r--.gitignore2
-rw-r--r--.gitmodules3
-rw-r--r--Makefile3
-rw-r--r--got/edit.got16
-rw-r--r--got/login.got10
-rw-r--r--got/template.html.got31
-rw-r--r--got/upload.got11
-rw-r--r--src/edit/dir.go101
-rw-r--r--src/edit/git.go146
-rw-r--r--src/edit/main.go24
-rw-r--r--src/edit/util.go7
-rw-r--r--src/edit/views.go25
m---------src/lukeshu.com/git/go/libsystemd0
-rw-r--r--src/util/fd.go101
-rw-r--r--src/util/http.go63
-rw-r--r--src/util/template.go56
-rw-r--r--static/style.css2
17 files changed, 601 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4c48abf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/pkg/
+/bin/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..fe8941b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "src/lukeshu.com/git/go/libsystemd"]
+ path = src/lukeshu.com/git/go/libsystemd
+ url = https://lukeshu.com/git/go/libsystemd/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0ec6d38
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,3 @@
+all:
+ GOPATH=$$PWD go install edit
+
diff --git a/got/edit.got b/got/edit.got
new file mode 100644
index 0000000..3fcdd23
--- /dev/null
+++ b/got/edit.got
@@ -0,0 +1,16 @@
+<!-- -*- Mode: HTML -*- -->
+<h1>{{.path | html}}</h1>
+<p>Content-Type: {{.ctype | html}}</p>
+<form method="POST" enctype="multipart/form-data">
+ <input type="hidden" name="_method" value="PUT">
+ <input type="hidden" name="_xsrf_token" value="{{.xsrf_token | html}}">
+ <textarea name="_body">{{.content | html }}</textarea>
+ <input type="submit" value="Save">
+</form>
+<form method="POST" enctype="multipart/form-data">
+ <input type="hidden" name="_method" value="PUT">
+ <input type="hidden" name="_xsrf_token" value="{{.xsrf_token | html}}">
+ <p>Instead of editing this in your browser, you can just upload a new copy.</p>
+ <input type="file" name="_body" required=required>
+ <input type="submit" value="Upload">
+</form>
diff --git a/got/login.got b/got/login.got
new file mode 100644
index 0000000..546d2e3
--- /dev/null
+++ b/got/login.got
@@ -0,0 +1,10 @@
+<!-- -*- Mode: HTML -*- -->
+{{if neq .url ""}}
+<p>You must log in to do that.</p>
+{{endif}}
+<form method="POST" enctype="multipart/form-data">
+ <input type="hidden" name="_method" value="PUT">
+ <label>Username: <input type="text" name="userid" /></label>
+ <label>Password: <input type="password" name="password" /></label>
+ <input type="submit" value="Save">
+</form>
diff --git a/got/template.html.got b/got/template.html.got
new file mode 100644
index 0000000..ab9e633
--- /dev/null
+++ b/got/template.html.got
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <!-- the meta tags must come first -->
+
+ <title>{{.title | html}}</title>
+
+ <link href="/style.css" rel="stylesheet">
+ {{.head}}
+ </head>
+ <body>
+ <header>
+ {{if eq .userid ""}}
+ <p>You are not logged in</p>
+ {{else}}
+ <p>You are logged in as {{.session.userid | html}}</p>
+ <form action="/session" method="POST">
+ <input type="hidden" name="_method" value="DELETE" />
+ <input type="hidden" name="_xsrf_token" value="{{.session.id | html}}" />
+ <input type="submit" value="Log out" />
+ </form>
+ {{endif}}
+ </header>
+ <article>
+ {{.body}}
+ </article>
+ </body>
+</html>
diff --git a/got/upload.got b/got/upload.got
new file mode 100644
index 0000000..069d9f8
--- /dev/null
+++ b/got/upload.got
@@ -0,0 +1,11 @@
+<!-- -*- Mode: HTML -*- -->
+<h1>{{.path | html}}</h1>
+<p>Content-Type: {{.ctype | html}}</p>
+<form method="POST" enctype="multipart/form-data">
+ <input type="hidden" name="_method" value="PUT" />
+ <input type="hidden" name="_xsrf_token" value="{{.xsrf_token | html}}">
+ <p>You can't edit this type of file in your browser. But you can
+ upload a new version.</p>
+ <input type="file" name="_body" required=required />
+ <input type="submit" value="Upload" />
+</form>
diff --git a/src/edit/dir.go b/src/edit/dir.go
new file mode 100644
index 0000000..2be52d5
--- /dev/null
+++ b/src/edit/dir.go
@@ -0,0 +1,101 @@
+package main
+
+import (
+ "fmt"
+ "time"
+ "net/http"
+ "path"
+ "strings"
+ "os/user"
+)
+
+func ServeGit(out http.ResponseWriter, in *http.Request) {
+ upath := in.URL.Path
+ if strings.HasPrefix(upath, "/") {
+ upath = "/" + upath
+ }
+ upath = path.Clean(upath)
+ upath = upath[1:]
+
+
+ errcheck(GitPull())
+ tree, err := GitLsTree()
+ errcheck(err)
+ file, fileExists := tree[upath]
+ if in.Method != http.MethodPut {
+ if !fileExists {
+ http.NotFound(out, in)
+ return
+ }
+ if file.Type == "tree" && !strings.HasSuffix(in.URL.Path, "/") {
+ out.Header().Set("Location", path.Base(upath) + "/")
+ out.WriteHeader(http.StatusMovedPermanently)
+ return
+ }
+ }
+
+ switch in.Method {
+ case http.MethodGet, http.MethodHead:
+ out.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if file.Type == "tree" {
+ errcheck(renderViewTree(out, upath, tree))
+ } else {
+ errcheck(renderViewBlob(out, upath, file))
+ }
+ out.WriteHeader(http.StatusOK)
+ case http.MethodPut:
+ username := in.Header.Get("X-Nginx-User")
+ userinfo, err := user.Lookup(username)
+ errcheck(err)
+ msg := in.Header.Get("X-Commit-Message")
+ if msg == "" {
+ msg = fmt.Sprintf("web edit: create/modify %q", upath)
+ }
+ var content []byte // TODO
+ edit := Edit{
+ UserName: userinfo.Name,
+ UserEmail: username+"@edit.team4272.com",
+ Time: time.Now(),
+ Message: msg+"\n",
+ Files: map[string][]byte{
+ upath: content,
+ },
+ }
+ errcheck(GitCommit(edit))
+ errcheck(GitPush())
+ errcheck(renderModified(out, upath))
+ if fileExists {
+ out.WriteHeader(http.StatusOK)
+ } else {
+ out.WriteHeader(http.StatusCreated)
+ }
+ case http.MethodDelete:
+ username := in.Header.Get("X-Nginx-User")
+ userinfo, err := user.Lookup(username)
+ errcheck(err)
+ msg := in.Header.Get("X-Commit-Message")
+ if msg == "" {
+ msg = fmt.Sprintf("web edit: delete %q", upath)
+ }
+ edit := Edit{
+ UserName: userinfo.Name,
+ UserEmail: username+"@edit.team4272.com",
+ Time: time.Now(),
+ Message: msg+"\n",
+ Files: map[string][]byte{
+ upath: nil,
+ },
+ }
+ errcheck(GitCommit(edit))
+ errcheck(GitPush())
+ errcheck(renderDeleted(out, upath))
+ out.WriteHeader(http.StatusOK)
+ case http.MethodOptions:
+ // POST because PostHack
+ out.Header().Set("Allow", "GET, HEAD, PUT, POST, DELETE, OPTIONS")
+ out.WriteHeader(http.StatusOK)
+ default:
+ out.Header().Set("Allow", "GET, HEAD, PUT, POST, DELETE, OPTIONS")
+ out.WriteHeader(http.StatusMethodNotAllowed)
+ }
+}
diff --git a/src/edit/git.go b/src/edit/git.go
new file mode 100644
index 0000000..010c928
--- /dev/null
+++ b/src/edit/git.go
@@ -0,0 +1,146 @@
+package main
+
+import (
+ "strconv"
+ "io"
+ "bytes"
+ "fmt"
+ "os/exec"
+ "time"
+ "errors"
+)
+
+func GitPull() error {
+ return exec.Command("git", "pull").Run()
+}
+
+func GitPush() error {
+ return exec.Command("git", "push").Run()
+}
+
+type Edit struct {
+ UserName string
+ UserEmail string
+ Time time.Time
+ Message string
+ Files map[string][]byte
+}
+
+func gitTime(t time.Time) string {
+ return fmt.Sprintf("%d %s", t.Unix(), t.Format("-0700"))
+}
+
+func (edit Edit) WriteTo(w io.Writer) error {
+ var err error
+ commit := make([]string, len(edit.Files))
+ i := 0
+ for name, content := range edit.Files {
+ commit[i] = name
+ if content != nil {
+ if _, err = fmt.Fprintf(w, "blob\nmark :%d\ndata %d\n%s", i+1, len(content), content); err != nil {
+ return err
+ }
+ }
+ }
+ _, err = fmt.Fprintf(w, `commit HEAD
+author %s <%s> %s
+committer %s <%s> %s
+data %d
+%sfrom HEAD
+`,
+ edit.UserName, edit.UserEmail, gitTime(edit.Time),
+ edit.UserName, edit.UserEmail, gitTime(edit.Time),
+ len(edit.Message), edit.Message)
+ if err != nil {
+ return err
+ }
+ for i, filename := range commit {
+ if edit.Files[filename] != nil {
+ if _, err = fmt.Fprintf(w, "M 100644 :%d %s\n", i+1, filename); err != nil {
+ return err
+ }
+ } else {
+ if _, err = fmt.Fprintf(w, "D %s\n", filename); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+
+func GitCommit(edit Edit) error {
+ cmd := exec.Command("git", "fast-import")
+ pip, err := cmd.StdinPipe()
+ if err != nil {
+ return err
+ }
+ if err = cmd.Start(); err != nil {
+ return err
+ }
+ werr := edit.WriteTo(pip)
+ if err = cmd.Wait(); err == nil {
+ err = werr
+ }
+ return err
+}
+
+type GitFile struct {
+ Mode int32 // 18 bits unsigned
+ Type string
+ Hash string
+ Size int64
+}
+
+type GitTree map[string]GitFile
+
+var ParseError = errors.New("git ls-tree parse error")
+
+func GitLsTree() (GitTree, error) {
+ data, err := exec.Command("git", "ls-tree", "-trlz", "HEAD").Output()
+ if err != nil {
+ return nil, err
+ }
+ lines := bytes.Split(data, []byte{0})
+ ret := make(GitTree, len(lines)-1)
+ for _, line := range lines[:len(ret)] {
+ // trim the trailing "\0"
+ if line[len(line)-1] != 0 {
+ return nil, ParseError
+ }
+ line = line[:len(line)-1]
+ // line = mode SP type SP hash SP size TAB name
+ a := bytes.SplitN(line, []byte{' '}, 4)
+ if len(a) != 4 {
+ return nil, ParseError
+ }
+ b := bytes.SplitN(a[3], []byte{'\t'}, 2)
+ if len(b) != 2 {
+ return nil, ParseError
+ }
+ fmode := a[0]
+ ftype := a[1]
+ fhash := a[2]
+ fsize := b[0]
+ fname := b[1]
+ fmodeN, err := strconv.ParseInt(string(fmode), 10, 19)
+ if err != nil {
+ return nil, err
+ }
+ fsizeN := int64(-1)
+ fsizeS := string(fsize)
+ if fsizeS != "-" {
+ fsizeN, err = strconv.ParseInt(fsizeS, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ }
+ ret[string(fname)] = GitFile{
+ Mode: int32(fmodeN),
+ Type: string(ftype),
+ Hash: string(fhash),
+ Size: fsizeN,
+ }
+ }
+ return ret, nil
+}
diff --git a/src/edit/main.go b/src/edit/main.go
new file mode 100644
index 0000000..2a640d0
--- /dev/null
+++ b/src/edit/main.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+ "net/http"
+ "util"
+ "os"
+)
+
+func ServeIndex(out http.ResponseWriter, in *http.Request) {
+ if in.URL.Path != "/" {
+ http.NotFound(out, in)
+ }
+ http.Redirect(out, in, "/files/", http.StatusMovedPermanently)
+}
+
+func main() {
+ socket, err := util.StreamListener(os.Args[1], os.Args[2])
+ errcheck(err)
+ errcheck(os.Chdir("/srv/http/edit.team4272.com/www.git"))
+ http.Handle("/", util.SaneHTTPHandler{http.HandlerFunc(ServeIndex)})
+ http.Handle("/static/", util.SaneHTTPHandler{http.FileServer(http.Dir("static"))})
+ http.Handle("/files/", util.SaneHTTPHandler{http.StripPrefix("/files", http.HandlerFunc(ServeGit))})
+ errcheck(http.Serve(socket, nil))
+}
diff --git a/src/edit/util.go b/src/edit/util.go
new file mode 100644
index 0000000..519e46c
--- /dev/null
+++ b/src/edit/util.go
@@ -0,0 +1,7 @@
+package main
+
+func errcheck(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/src/edit/views.go b/src/edit/views.go
new file mode 100644
index 0000000..90b6f1b
--- /dev/null
+++ b/src/edit/views.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+ "io"
+)
+
+func renderViewTree(w io.Writer, upath string, tree GitTree) error {
+ // TODO
+ return nil
+}
+
+func renderViewBlob(w io.Writer, upath string, file GitFile) error {
+ // TODO
+ return nil
+}
+
+func renderModified(w io.Writer, upath string) error {
+ // TODO
+ return nil
+}
+
+func renderDeleted(w io.Writer, upath string) error {
+ // TODO
+ return nil
+}
diff --git a/src/lukeshu.com/git/go/libsystemd b/src/lukeshu.com/git/go/libsystemd
new file mode 160000
+Subproject eb6e8a6ca87879a6ca85788fcf6d3bf8848088e
diff --git a/src/util/fd.go b/src/util/fd.go
new file mode 100644
index 0000000..17c64cd
--- /dev/null
+++ b/src/util/fd.go
@@ -0,0 +1,101 @@
+// Copyright 2015-2016 Luke Shumaker
+
+package util
+
+import (
+ "net"
+ "os"
+ "strings"
+ "fmt"
+ "sync"
+ "strconv"
+
+ sd "lukeshu.com/git/go/libsystemd/sd_daemon"
+)
+
+var fdsLock sync.Mutex
+var fds = map[int]*os.File{}
+var sdFds = map[string]int{}
+
+func init() {
+ fds[0] = os.Stdin
+ fds[1] = os.Stdout
+ fds[2] = os.Stderr
+ fromSd := sd.ListenFds(true)
+ if fromSd == nil {
+ return
+ }
+ for i, file := range fromSd {
+ fds[i+3] = file
+ sdFds[file.Name()] = i+3
+ }
+}
+
+func FdNameToNum(name string) int {
+ switch name {
+ case "stdin":
+ return 0
+ case "stdout":
+ return 1
+ case "stderr":
+ return 2
+ case "systemd":
+ if len(sdFds) == 0 {
+ return -1
+ }
+ return 3
+ default:
+ if n, err := strconv.Atoi(name); err == nil {
+ if n >= 0 {
+ return n
+ }
+ } else if strings.HasPrefix(name, "systemd:") {
+ name = strings.TrimPrefix(name, "systemd")
+ n, ok := sdFds[name]
+ if ok {
+ return n
+ } else if n, err := strconv.Atoi(name); err == nil && n < len(sdFds) {
+ return n+3
+ }
+ }
+ return -1
+ }
+}
+
+func FdFile(fd int) *os.File {
+ fdsLock.Lock()
+ defer fdsLock.Unlock()
+ file, ok := fds[fd]
+ if ok {
+ return file
+ }
+ file = os.NewFile(uintptr(fd), fmt.Sprintf("/dev/fd/%d", fd))
+ fds[fd] = file
+ return file
+}
+
+func StreamListener(stype, saddr string) (net.Listener, error) {
+ switch stype {
+ case "fd":
+ return net.FileListener(FdFile(FdNameToNum(saddr)))
+ default: /* case "tcp", "tcp4", "tcp6", "unix", "unixpacket": */
+ return net.Listen(stype, saddr)
+ }
+}
+
+func PacketListener(stype, saddr string) (net.PacketConn, error) {
+ switch stype {
+ case "fd":
+ return net.FilePacketConn(FdFile(FdNameToNum(saddr)))
+ default: /* case "udp", "udp4", "udp6", "ip", "ip4", "ip6", "unixgram": */
+ return net.ListenPacket(stype, saddr)
+ }
+}
+
+// For completeless, I might want to implement methods for each of
+// these:
+// - FIFO
+// - Special
+// - Netlink
+// - MessageQueue
+// - USBFunction
diff --git a/src/util/http.go b/src/util/http.go
new file mode 100644
index 0000000..6c216cf
--- /dev/null
+++ b/src/util/http.go
@@ -0,0 +1,63 @@
+package util
+
+import (
+ "bytes"
+ "fmt"
+ "log"
+ "net/http"
+ "runtime"
+ "strconv"
+)
+
+type httpResponseBuffer struct {
+ inner http.ResponseWriter
+ buf bytes.Buffer
+ status int
+}
+
+func (o *httpResponseBuffer) Header() http.Header {
+ return o.inner.Header()
+}
+
+func (o *httpResponseBuffer) Write(data []byte) (int, error) {
+ return o.buf.Write(data)
+}
+
+func (o *httpResponseBuffer) WriteHeader(status int) {
+ o.status = status
+}
+
+type SaneHTTPHandler struct {
+ Inner http.Handler
+}
+
+func (h SaneHTTPHandler) ServeHTTP(out http.ResponseWriter, in *http.Request) {
+ HTTPHandlerFuncWrapper(out, in, h.Inner.ServeHTTP)
+}
+
+func HTTPHandlerFuncWrapper(out http.ResponseWriter, in *http.Request, fn http.HandlerFunc) {
+ buf := httpResponseBuffer{
+ inner: out,
+ status: http.StatusOK,
+ }
+ ok := true
+ func() {
+ defer func() {
+ if r := recover(); r != nil {
+ const size = 64 << 10
+ buf := make([]byte, size)
+ buf = buf[:runtime.Stack(buf, false)]
+ st := fmt.Sprintf("%[1]T(%#[1]v) => %[1]v\n\n%[2]s", r, string(buf))
+ log.Printf("panic serving %v\n\n%s", in.URL, st)
+ http.Error(out, fmt.Sprintf("500 Internal Server Error:\n\n%s", st), 500)
+ ok = false
+ }
+ }()
+ fn(&buf, in)
+ }()
+ if ok {
+ out.Header().Set("Content-Length", strconv.Itoa(buf.buf.Len()))
+ out.WriteHeader(buf.status)
+ buf.buf.WriteTo(out)
+ }
+}
diff --git a/src/util/template.go b/src/util/template.go
new file mode 100644
index 0000000..f42875a
--- /dev/null
+++ b/src/util/template.go
@@ -0,0 +1,56 @@
+package util
+
+import (
+ "path"
+ "text/template"
+ "time"
+)
+
+func NewTemplate(filenames ...string) *template.Template {
+ return template.Must(template.New(path.Base(filenames[0])).
+ Funcs(template.FuncMap{
+ // Form input helpers
+ "value": func(v interface{}) string {
+ if v == nil {
+ return ""
+ }
+ return "value=\"" + template.HTMLEscapeString(v.(string)) + "\""
+ },
+ "checked": func(v1 interface{}, v2 interface{}) string {
+ if v1 == nil || v2 == nil {
+ return ""
+ }
+ if v1 == v2 {
+ return "checked"
+ }
+ return ""
+ },
+ "selected": func(v1 interface{}, v2 interface{}) string {
+ if v1 == nil || v2 == nil {
+ return ""
+ }
+ if v1 == v2 {
+ return "selected"
+ }
+ return ""
+ },
+ // Form result helpers
+ "q": func(v interface{}) string {
+ if v == nil || v.(string) == "" {
+ return "<output class=none>none</output>"
+ }
+ return "<output><q>" + template.HTMLEscapeString(v.(string)) + "</q></output>"
+ },
+ "have": func(v interface{}) bool {
+ return v != nil && v.(string) == "on"
+ },
+ "date": func(v string) string {
+ t, err := time.Parse("2006-01-02", v)
+ if err != nil {
+ panic(err)
+ }
+ return t.Format("January 2, 2006")
+ },
+ }).
+ ParseFiles(filenames...))
+}
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..6c63196
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,2 @@
+.foo {
+} \ No newline at end of file