diff --git a/README.md b/README.md index 8bcfac5..e0c9daa 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,32 @@ -# htwtxt -hosted twtxt server +Quick and dirty hack of a server for hosted twtxt +================================================= + +This provides a server to host twtxt feeds, if you want to provide users without +their own easily accessed webspace with a simple in-browser solution to twtxt. +(What is twtxt? See ). + +The whole thing is written in Go, building expects a working Go environment, +with $GOPATH set and the go tool installed. + +INSTALLATION/USAGE: + +Copy this directory into your $GOPATH's src directory, i.e. to +$GOPATH/src/htwtxt – then run … + +$ go get htwtxt + +… then. with $key some secret session store key only you know, … + +$ KEY=$key go run $GOPATH/src/htwtxt/main.go + +Optional arguments: + +$ KEY=$key go run $GOPATH/src/htwtxt/main.go [PORT] [CERTIFICATE] [SERVER_KEY] + +PORT may be any desired port number to serve. + +If you provide CERTIFICATE and SERVER KEY (both as file paths) as the second and +third argument, the server will run as a HTTPS server instead of a HTTP server. + +A quick and dirty setup-and-run script can be found in ./bad-setup-and-run.sh – +an even dirtier one is found in ./bad-setup-and-run-https.sh … diff --git a/bad-setup-and-run-https.sh b/bad-setup-and-run-https.sh new file mode 100755 index 0000000..a81112a --- /dev/null +++ b/bad-setup-and-run-https.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +echo WITHOUT FURTHER MODIFICATION +echo THIS IS A REALLY BAD, THOROUGHLY INSECURE WAY TO SETUP HTWTXT. +echo YOU HAVE BEEN WARNED. +echo +echo SETTING UP SERVER KEY AND CERTIFICATE +cd $GOPATH/src/htwtxt +openssl genrsa -out server.key 2048 +openssl req -new -x509 -key server.key -out cert.pem -days 365 +echo INSTALLING GO DEPENDENCIES +go get +KEY=MyTrulyBadDefaultKey +echo RUNNING WITH $KEY AS SESSIONSTORE KEY +KEY=$KEY go run main.go 8000 cert.pem server.key diff --git a/bad-setup-and-run.sh b/bad-setup-and-run.sh new file mode 100755 index 0000000..22a34fd --- /dev/null +++ b/bad-setup-and-run.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +echo WITHOUT FURTHER MODIFICATION +echo THIS IS A REALLY BAD, THOROUGHLY INSECURE WAY TO SETUP HTWTXT. +echo YOU HAVE BEEN WARNED. +echo +cd $GOPATH/src/htwtxt +KEY=MyTrulyBadDefaultKey +echo RUNNING WITH $KEY AS SESSIONSTORE KEY +KEY=$KEY go run main.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..cab0a20 --- /dev/null +++ b/main.go @@ -0,0 +1,253 @@ +package main + +import "bufio" +import "errors" +import "github.com/gorilla/sessions" +import "github.com/gorilla/mux" +import "golang.org/x/crypto/bcrypt" +import "html/template" +import "log" +import "net/http" +import "os" +import "strconv" +import "strings" +import "time" + +const loginsFile = "logins.txt" +const twtsDir = "twtxt" +const portDefault = 8000 + +var useHttps bool +var templ *template.Template +var store *sessions.CookieStore + +func createFileIfNotExists(path string) { + if _, err := os.Stat(path); err != nil { + file, err := os.Create(path) + if err != nil { + log.Fatal("Can't create file: ", err) + } + file.Close() + } +} + +func appendToFile(path string, msg string) { + fileWrite, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0600) + defer fileWrite.Close() + if err != nil { + log.Fatal("Can't open file for appending", err) + } + if _, err = fileWrite.WriteString(msg); err != nil { + log.Fatal("Can't write to file", err) + } +} + +func onlyLegalRunes(str string) bool { + alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + "0123456789_" + for _, ru := range str { + if !(strings.ContainsRune(alphabet, ru)) { + return false + } + } + return true +} + +func getUserFromSession(r *http.Request) (string, error) { + if session, err := store.Get(r, "session"); err == nil { + if str, ok := session.Values["name"].(string); ok { + return str, nil + } + } + return "", errors.New("no valid session") +} + +func execTemplate(w http.ResponseWriter, file string, input string) { + type data struct{ Msg string } + err := templ.ExecuteTemplate(w, file, data{Msg: input}) + if err != nil { + log.Fatal("Trouble executing template", err) + } +} + +func indexHandler(w http.ResponseWriter, r *http.Request) { + user, err := getUserFromSession(r) + if err != nil { + execTemplate(w, "loginform.html", "") + return + } + execTemplate(w, "twtxt.html", user) +} + +func logInPostHandler(w http.ResponseWriter, r *http.Request) { + name := r.FormValue("name") + pw := r.FormValue("password") + file, err := os.Open(loginsFile) + defer file.Close() + if err != nil { + log.Fatal("Can't open file for reading", err) + } + scanner := bufio.NewScanner(bufio.NewReader(file)) + for { + if !scanner.Scan() { + break + } + line := scanner.Text() + tokens := strings.Split(line, " ") + if len(tokens) == 2 { + if 0 == strings.Compare(tokens[0], name) && + nil == bcrypt.CompareHashAndPassword( + []byte(tokens[1]), []byte(pw)) { + // TODO: Handle err here. + session, _ := store.Get(r, "session") + if useHttps { + session.Options.Secure = true + } + session.Options.HttpOnly = true + session.Values["name"] = tokens[0] + if err := session.Save(r, w); err != nil { + log.Fatal("session save trouble:", err) + } + http.Redirect(w, r, "/", 302) + return + } + } + } + execTemplate(w, "bad.html", "Bad login.") +} + +func logOutHandler(w http.ResponseWriter, r *http.Request) { + session, err := store.Get(r, "session") + if err != nil { + log.Fatal("session get trouble", err) + } + session.Options.MaxAge = -1 + if err := session.Save(r, w); err != nil { + log.Fatal("session save trouble", err) + } + http.Redirect(w, r, "/", 302) +} + +func signUpFormHandler(w http.ResponseWriter, r *http.Request) { + execTemplate(w, "signupform.html", "") +} + +func signUpHandler(w http.ResponseWriter, r *http.Request) { + name := r.FormValue("name") + pw := r.FormValue("password") + if 0 == strings.Compare("name", "") || 0 == strings.Compare(pw, "") || + !onlyLegalRunes(name) { + execTemplate(w, "bad.html", "Invalid values.") + return + } + fileRead, err := os.Open(loginsFile) + defer fileRead.Close() + if err != nil { + log.Fatal("Can't open file for reading", err) + } + scanner := bufio.NewScanner(bufio.NewReader(fileRead)) + for { + if !scanner.Scan() { + break + } + line := scanner.Text() + tokens := strings.Split(line, " ") + if 0 == strings.Compare(name, tokens[0]) { + execTemplate(w, "bad.html", "Username taken.") + return + } + } + hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) + if err != nil { + log.Fatal("Can't generate password hash", err) + } + new_line := name + " " + string(hash) + "\n" + appendToFile(loginsFile, new_line) + execTemplate(w, "signup.html", "") +} + +func twtPostHandler(w http.ResponseWriter, r *http.Request) { + text := r.FormValue("twt") + user, err := getUserFromSession(r) + if err != nil { + execTemplate(w, "bad.html", "Invalid session.") + return + } + twtsFile := twtsDir + "/" + user + createFileIfNotExists(twtsFile) + text = strings.Replace(text, "\n", " ", -1) + appendToFile(twtsFile, time.Now().Format(time.RFC3339)+"\t"+text+"\n") + http.Redirect(w, r, "/"+twtsFile, 302) +} + +func twtxtHandler(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + if !onlyLegalRunes(name) { + execTemplate(w, "bad.html", "Bad path.") + return + } + path := twtsDir + "/" + name + if _, err := os.Stat(path); err != nil { + execTemplate(w, "bad.html", "Empty twtxt for user.") + return + } + http.ServeFile(w, r, path) +} + +func main() { + useHttps = false + port := portDefault + var err error + if len(os.Args) > 1 { + port, err = strconv.Atoi(os.Args[1]) + if err != nil { + log.Fatal("Invalid port argument:", err) + } + } + var certificateFile string + var serverKeyFile string + if len(os.Args) > 3 { + useHttps = true + log.Println("using TLS") + certificateFile = os.Args[2] + serverKeyFile = os.Args[3] + if _, err := os.Stat(certificateFile); err != nil { + log.Fatal("No certificate file found.") + } + if _, err := os.Stat(serverKeyFile); err != nil { + log.Fatal("No server key file found.") + } + } + createFileIfNotExists(loginsFile) + // TODO: Handle err here. + _ = os.Mkdir(twtsDir, 0700) + secretKey := os.Getenv("KEY") + if secretKey == "" { + log.Fatal("No secret session key provided.") + } + store = sessions.NewCookieStore([]byte(secretKey)) + templ, err = template.New("main").ParseGlob("./templates/*.html") + if err != nil { + log.Fatal("Can't set up new template: ", err) + } + router := mux.NewRouter() + router.HandleFunc("/", indexHandler) + router.HandleFunc("/login", logInPostHandler).Methods("POST") + router.HandleFunc("/signupform", signUpFormHandler) + router.HandleFunc("/signup", signUpHandler).Methods("POST") + router.HandleFunc("/logout", logOutHandler) + router.HandleFunc("/twt", twtPostHandler).Methods("POST") + router.HandleFunc("/twtxt/{name}", twtxtHandler) + router.HandleFunc("/twtxt/", indexHandler) + http.Handle("/", router) + log.Println("serving at port", port) + if useHttps { + err = http.ListenAndServeTLS(":"+strconv.Itoa(port), + certificateFile, serverKeyFile, nil) + } else { + err = http.ListenAndServe(":"+strconv.Itoa(port), nil) + } + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +} diff --git a/templates/bad.html b/templates/bad.html new file mode 100644 index 0000000..233f773 --- /dev/null +++ b/templates/bad.html @@ -0,0 +1,4 @@ + +

Something went wrong: {{ .Msg }}

+

Try again?

+ diff --git a/templates/loginform.html b/templates/loginform.html new file mode 100644 index 0000000..4589b44 --- /dev/null +++ b/templates/loginform.html @@ -0,0 +1,9 @@ + +

login

+
+ name:
+ password:
+ +
+ No account? Sign up first!
+ diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..c7a341b --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,4 @@ + + Success! + Now try to log in + diff --git a/templates/signupform.html b/templates/signupform.html new file mode 100644 index 0000000..1bbeaa2 --- /dev/null +++ b/templates/signupform.html @@ -0,0 +1,8 @@ + +

sign up

+
+ name: (legal chars: A-Z, a-z, 0-1, _)
+ password:
+ +
+ diff --git a/templates/twtxt.html b/templates/twtxt.html new file mode 100644 index 0000000..a3214a6 --- /dev/null +++ b/templates/twtxt.html @@ -0,0 +1,9 @@ + +

+ Hi there, {{ .Msg }}, wanna log out? +

+
+ + +
+