diff options
author | Luke Shumaker <lukeshu@sbcglobal.net> | 2016-11-01 14:46:15 -0400 |
---|---|---|
committer | Luke Shumaker <lukeshu@sbcglobal.net> | 2016-11-01 14:46:15 -0400 |
commit | c7eea383aeaf6748daf994e9e28e4d0c25350736 (patch) | |
tree | a0979cbd4e6be06385fd0340e050147fdacd6e35 |
initial commit
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .gitmodules | 3 | ||||
-rw-r--r-- | Makefile | 3 | ||||
-rw-r--r-- | got/edit.got | 16 | ||||
-rw-r--r-- | got/login.got | 10 | ||||
-rw-r--r-- | got/template.html.got | 31 | ||||
-rw-r--r-- | got/upload.got | 11 | ||||
-rw-r--r-- | src/edit/dir.go | 101 | ||||
-rw-r--r-- | src/edit/git.go | 146 | ||||
-rw-r--r-- | src/edit/main.go | 24 | ||||
-rw-r--r-- | src/edit/util.go | 7 | ||||
-rw-r--r-- | src/edit/views.go | 25 | ||||
m--------- | src/lukeshu.com/git/go/libsystemd | 0 | ||||
-rw-r--r-- | src/util/fd.go | 101 | ||||
-rw-r--r-- | src/util/http.go | 63 | ||||
-rw-r--r-- | src/util/template.go | 56 | ||||
-rw-r--r-- | static/style.css | 2 |
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 |