234 行
5.9 KiB
Go
234 行
5.9 KiB
Go
// htwtxt – hosted twtxt server; see README for copyright and license info
|
||
|
||
package main
|
||
|
||
import "bufio"
|
||
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
|
||
|
||
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 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) {
|
||
execTemplate(w, "index.html", "")
|
||
}
|
||
|
||
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")
|
||
pw2 := r.FormValue("password2")
|
||
if 0 != strings.Compare(pw, pw2) || 0 == strings.Compare("name", "") ||
|
||
0 == strings.Compare(pw, "") || !onlyLegalRunes(name) ||
|
||
len(name) > 140 {
|
||
execTemplate(w, "error.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, "error.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 listHandler(w http.ResponseWriter, r *http.Request) {
|
||
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))
|
||
var dir []string
|
||
for {
|
||
if !scanner.Scan() {
|
||
break
|
||
}
|
||
line := scanner.Text()
|
||
tokens := strings.Split(line, " ")
|
||
if len(tokens) == 2 {
|
||
dir = append(dir, tokens[0])
|
||
}
|
||
}
|
||
type data struct{ Dir []string }
|
||
err = templ.ExecuteTemplate(w, "list.html", data{Dir: dir})
|
||
if err != nil {
|
||
log.Fatal("Trouble executing template", err)
|
||
}
|
||
}
|
||
|
||
func twtxtPostHandler(w http.ResponseWriter, r *http.Request) {
|
||
name := r.FormValue("name")
|
||
pw := r.FormValue("password")
|
||
loginValid := false
|
||
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)) {
|
||
loginValid = true
|
||
|
||
}
|
||
}
|
||
}
|
||
if !loginValid {
|
||
execTemplate(w, "error.html", "Bad login.")
|
||
return
|
||
}
|
||
text := r.FormValue("twt")
|
||
twtsFile := twtsDir + "/" + name
|
||
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, "error.html", "Bad path.")
|
||
return
|
||
}
|
||
path := twtsDir + "/" + name
|
||
if _, err := os.Stat(path); err != nil {
|
||
execTemplate(w, "error.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)
|
||
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("/twtxt", listHandler).Methods("GET")
|
||
router.HandleFunc("/twtxt/", listHandler)
|
||
router.HandleFunc("/signup", signUpFormHandler).Methods("GET")
|
||
router.HandleFunc("/signup", signUpHandler).Methods("POST")
|
||
router.HandleFunc("/twtxt", twtxtPostHandler).Methods("POST")
|
||
router.HandleFunc("/twtxt/{name}", twtxtHandler)
|
||
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)
|
||
}
|
||
}
|