flounder

A simple gemini site builder
Log | Files | Refs | README | LICENSE

commit 3d84831d6976655373cf07b2c74052df69d2b7cb
parent e7763a0743e1b07d9e9b6925848975597bdfd3f3
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Thu, 22 Oct 2020 00:20:40 -0700

Basic (untested, probably insecure) auth WIP

Diffstat:
MREADME.md | 1+
Aauth.go | 43+++++++++++++++++++++++++++++++++++++++++++
Mconfig.go | 1+
Mflounder.toml | 2+-
Mgemini.go | 1+
Mgo.mod | 3+++
Mgo.sum | 14++++++++++++++
Mhttp.go | 43+++++++++++++++++++++++++++++++++++++++++++
Atemplates/login.html | 28++++++++++++++++++++++++++++
9 files changed, 135 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md @@ -4,6 +4,7 @@ A lightweight server to help users build simple Gemini sites over http(s) Designed to help make the Gemini ecosystem more accessible. + ## Hosting Very simple to host -- a single binary with a gemini server, http server included. diff --git a/auth.go b/auth.go @@ -0,0 +1,43 @@ +package main + +import ( + "bufio" + "fmt" + "golang.org/x/crypto/bcrypt" + "os" + "strings" +) + +func addUser(username string, password string) error { + file, err := os.OpenFile(c.PasswdFile, os.O_APPEND|os.O_CREATE, 0644) + if err != nil { + return err + } + defer file.Close() + hash, err := bcrypt.GenerateFromPassword([]byte(password), -1) + if err != nil { + return err + } + newUser := fmt.Sprintf("%s:%s\n", username, hash) + file.WriteString(newUser) + return nil +} +func checkAuth(username string, password string) error { + file, err := os.OpenFile(c.PasswdFile, os.O_CREATE, 0644) + if err != nil { + return err + } + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, ":") + if len(parts) != 2 { + return fmt.Errorf("malformed line, no colon: %s", line) + } + if username == parts[0] { + return bcrypt.CompareHashAndPassword([]byte(parts[1]), []byte(password)) + } + } + return fmt.Errorf("User not found") +} diff --git a/config.go b/config.go @@ -10,6 +10,7 @@ type Config struct { SiteTitle string Debug bool SecretKey string + PasswdFile string } func getConfig(filename string) (Config, error) { diff --git a/flounder.toml b/flounder.toml @@ -1,4 +1,4 @@ SiteTitle="­čÉčflounder" RootDomain="localhost" FilesDirectory="./files" - +PasswdFile="accounts.htpasswd" diff --git a/gemini.go b/gemini.go @@ -39,6 +39,7 @@ func gmiPage(w *gmi.ResponseWriter, r *gmi.Request) { userName := strings.Split(r.URL.Host, ".")[0] fileName := path.Join(c.FilesDirectory, userName, r.URL.Path) data, err := ioutil.ReadFile(fileName) + // TODO write mimetype if err != nil { // TODO return 404 equivalent log.Fatal(err) diff --git a/go.mod b/go.mod @@ -5,5 +5,8 @@ go 1.15 require ( git.sr.ht/~adnano/gmi v0.1.0-alpha.2 github.com/BurntSushi/toml v0.3.1 + github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c // TODO audit properly github.com/mattn/go-sqlite3 v1.14.4 + github.com/tg123/go-htpasswd v1.0.0 + golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d ) diff --git a/go.sum b/go.sum @@ -4,7 +4,21 @@ git.sr.ht/~adnano/gmi v0.1.0-alpha.2 h1:5/wzImYT3mJmZ27lazJ8YAdpiVN3QNJruLX7PXOI git.sr.ht/~adnano/gmi v0.1.0-alpha.2/go.mod h1:t/m2KtH+7lXIF7jjVN+bNvwPbE0nxHOpvlA/WZ/KeLQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46 h1:rs0kDBt2zF4/CM9rO5/iH+U22jnTygPlqWgX55Ufcxg= +github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= +github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c h1:DBGU7zCwrrPPDsD6+gqKG8UfMxenWg9BOJE/Nmfph+4= +github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c/go.mod h1:SHawtolbB0ZOFoRWgDwakX5WpwuIWAK88bUXVZqK0Ss= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI= github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/tg123/go-htpasswd v1.0.0 h1:Ze/pZsz73JiCwXIyJBPvNs75asKBgfodCf8iTEkgkXs= +github.com/tg123/go-htpasswd v1.0.0/go.mod h1:eQTgl67UrNKQvEPKrDLGBssjVwYQClFZjALVLhIv8C0= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/http.go b/http.go @@ -68,10 +68,49 @@ func mySiteHandler(w http.ResponseWriter, r *http.Request) { } } +func loginHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + // show page + data := struct { + Error error + PageTitle string + }{nil, c.SiteTitle} + err := t.ExecuteTemplate(w, "login.html", data) + if err != nil { + log.Println(err) + renderError(w, InternalServerErrorMsg, 500) + return + } + } else if r.Method == "POST" { + r.ParseForm() + name := r.Form.Get("username") + password := r.Form.Get("password") + err := checkAuth(name, password) + if err == nil { + log.Println("logged in") + // redirect home + } else { + log.Println(err) + } + // create session + // redirect home + // verify login + // check for errors + } +} + +func register(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + } else if r.Method == "POST" { + } +} + // Server a user's file func userFile(w http.ResponseWriter, r *http.Request) { userName := strings.Split(r.Host, ".")[0] fileName := path.Join(c.FilesDirectory, userName, r.URL.Path) + // if gemini -- parse, convert, serve + // else http.ServeFile(w, r, fileName) } @@ -85,8 +124,11 @@ func runHTTPServer() { http.HandleFunc(c.RootDomain+"/", indexHandler) http.HandleFunc(c.RootDomain+"/my_site", mySiteHandler) http.HandleFunc(c.RootDomain+"/edit/", editFileHandler) + http.HandleFunc(c.RootDomain+"/login", loginHandler) // http.HandleFunc("/delete/", deleteFileHandler) // login+register functions + + // handle user files based on subdomain http.HandleFunc("/", userFile) log.Fatal(http.ListenAndServe(":8080", logRequest(http.DefaultServeMux))) } @@ -94,5 +136,6 @@ func runHTTPServer() { func logRequest(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) + handler.ServeHTTP(w, r) }) } diff --git a/templates/login.html b/templates/login.html @@ -0,0 +1,28 @@ +{{template "header" .}} +<h1>{{.PageTitle}}</h1> +<h1>Log in</h1> +<form action="/login" method="post"> + <p> + <label for="username">Username</label> + <input id="username" name="username" size="32" type="text" value="" /> + </p> + <p> + <label for="password">Password</label> + <input id="password" name="password" size="32" type="password" value="" /> + </p> + {{ if .Error }} + <div class="error"> + <p>{{.}}</p> + </div> + {{ end}} + <p> + <input + class="button" + id="submit" + name="submit" + type="submit" + value="Log in" + /> + </p> +</form> +{{template "footer" .}}