2016-02-08 00:28:08 +09:00
|
|
|
|
// htwtxt – hosted twtxt server; see README for copyright and license info
|
|
|
|
|
|
2016-02-07 01:05:26 +09:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import "bufio"
|
2016-02-09 07:36:24 +09:00
|
|
|
|
import "errors"
|
2016-02-10 06:50:00 +09:00
|
|
|
|
import "flag"
|
2016-02-07 01:05:26 +09:00
|
|
|
|
import "github.com/gorilla/mux"
|
|
|
|
|
import "golang.org/x/crypto/bcrypt"
|
|
|
|
|
import "html/template"
|
2016-02-09 07:36:24 +09:00
|
|
|
|
import "io/ioutil"
|
2016-02-07 01:05:26 +09:00
|
|
|
|
import "log"
|
|
|
|
|
import "net/http"
|
|
|
|
|
import "os"
|
|
|
|
|
import "strconv"
|
|
|
|
|
import "strings"
|
|
|
|
|
import "time"
|
|
|
|
|
|
|
|
|
|
const loginsFile = "logins.txt"
|
2016-02-10 09:23:39 +09:00
|
|
|
|
const feedsDir = "feeds"
|
2016-02-07 01:05:26 +09:00
|
|
|
|
|
2016-02-10 07:18:05 +09:00
|
|
|
|
var dataDir string
|
|
|
|
|
var loginsPath string
|
2016-02-10 09:23:39 +09:00
|
|
|
|
var feedsPath string
|
2016-02-07 01:05:26 +09:00
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-09 07:36:24 +09:00
|
|
|
|
func login(w http.ResponseWriter, r *http.Request) (string, error) {
|
|
|
|
|
name := r.FormValue("name")
|
|
|
|
|
pw := r.FormValue("password")
|
|
|
|
|
loginValid := false
|
2016-02-10 07:18:05 +09:00
|
|
|
|
file, err := os.Open(loginsPath)
|
2016-02-09 07:36:24 +09:00
|
|
|
|
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) == 3 {
|
|
|
|
|
if 0 == strings.Compare(tokens[0], name) &&
|
|
|
|
|
nil == bcrypt.CompareHashAndPassword(
|
|
|
|
|
[]byte(tokens[1]), []byte(pw)) {
|
|
|
|
|
loginValid = true
|
2016-02-07 01:05:26 +09:00
|
|
|
|
|
2016-02-09 07:36:24 +09:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !loginValid {
|
|
|
|
|
execTemplate(w, "error.html", "Bad login.")
|
|
|
|
|
return name, errors.New("")
|
|
|
|
|
}
|
|
|
|
|
return name, nil
|
2016-02-07 01:05:26 +09:00
|
|
|
|
}
|
|
|
|
|
|
2016-02-09 07:36:24 +09:00
|
|
|
|
func accountLine(w http.ResponseWriter, r *http.Request,
|
|
|
|
|
checkDupl bool) (string, error) {
|
2016-02-07 01:05:26 +09:00
|
|
|
|
name := r.FormValue("name")
|
2016-02-09 07:36:24 +09:00
|
|
|
|
pw := r.FormValue("new_password")
|
|
|
|
|
pw2 := r.FormValue("new_password2")
|
2016-02-09 06:20:04 +09:00
|
|
|
|
mail := r.FormValue("mail")
|
2016-02-09 04:40:32 +09:00
|
|
|
|
if 0 != strings.Compare(pw, pw2) || 0 == strings.Compare("name", "") ||
|
|
|
|
|
0 == strings.Compare(pw, "") || !onlyLegalRunes(name) ||
|
|
|
|
|
len(name) > 140 {
|
2016-02-08 00:28:08 +09:00
|
|
|
|
execTemplate(w, "error.html", "Invalid values.")
|
2016-02-09 07:36:24 +09:00
|
|
|
|
return "", errors.New("")
|
|
|
|
|
}
|
|
|
|
|
if checkDupl {
|
2016-02-10 07:18:05 +09:00
|
|
|
|
fileRead, err := os.Open(loginsPath)
|
2016-02-09 07:36:24 +09:00
|
|
|
|
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 "", errors.New("")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal("Can't generate password hash", err)
|
|
|
|
|
}
|
2016-02-09 08:05:59 +09:00
|
|
|
|
return name + " " + string(hash) + " " + mail, nil
|
2016-02-09 07:36:24 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
newLine, err := accountLine(w, r, true)
|
|
|
|
|
if err != nil {
|
2016-02-07 01:05:26 +09:00
|
|
|
|
return
|
|
|
|
|
}
|
2016-02-10 07:18:05 +09:00
|
|
|
|
appendToFile(loginsPath, newLine+"\n")
|
2016-02-09 07:36:24 +09:00
|
|
|
|
execTemplate(w, "feedset.html", "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func accountFormHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
execTemplate(w, "accountform.html", "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func accountPostHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
name, err := login(w, r)
|
2016-02-07 01:05:26 +09:00
|
|
|
|
if err != nil {
|
2016-02-09 07:36:24 +09:00
|
|
|
|
return
|
2016-02-07 01:05:26 +09:00
|
|
|
|
}
|
2016-02-09 07:36:24 +09:00
|
|
|
|
newLine, err := accountLine(w, r, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2016-02-10 07:18:05 +09:00
|
|
|
|
text, err := ioutil.ReadFile(loginsPath)
|
2016-02-09 07:36:24 +09:00
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal("Can't read file", err)
|
|
|
|
|
}
|
|
|
|
|
lines := strings.Split(string(text), "\n")
|
|
|
|
|
for i, line := range lines {
|
2016-02-07 01:05:26 +09:00
|
|
|
|
tokens := strings.Split(line, " ")
|
|
|
|
|
if 0 == strings.Compare(name, tokens[0]) {
|
2016-02-09 07:36:24 +09:00
|
|
|
|
lines[i] = newLine
|
|
|
|
|
break
|
2016-02-07 01:05:26 +09:00
|
|
|
|
}
|
|
|
|
|
}
|
2016-02-09 08:05:59 +09:00
|
|
|
|
text = []byte(strings.Join(lines, "\n"))
|
2016-02-10 07:18:05 +09:00
|
|
|
|
tmpFile := "tmp_" + loginsPath
|
2016-02-09 07:36:24 +09:00
|
|
|
|
if err := ioutil.WriteFile(tmpFile, []byte(text), 0600); err != nil {
|
|
|
|
|
log.Fatal("Trouble writing file", err)
|
|
|
|
|
}
|
2016-02-10 07:18:05 +09:00
|
|
|
|
if err := os.Rename(loginsPath, "_"+loginsFile); err != nil {
|
2016-02-09 07:36:24 +09:00
|
|
|
|
log.Fatal("Trouble moving file", err)
|
|
|
|
|
}
|
2016-02-10 07:18:05 +09:00
|
|
|
|
if err := os.Rename(tmpFile, loginsPath); err != nil {
|
2016-02-09 07:36:24 +09:00
|
|
|
|
log.Fatal("Trouble moving file", err)
|
|
|
|
|
}
|
2016-02-10 07:18:05 +09:00
|
|
|
|
if err := os.Remove("_" + loginsPath); err != nil {
|
2016-02-09 07:36:24 +09:00
|
|
|
|
log.Fatal("Trouble removing file", err)
|
2016-02-07 01:05:26 +09:00
|
|
|
|
}
|
2016-02-09 07:42:01 +09:00
|
|
|
|
execTemplate(w, "feedset.html", "")
|
2016-02-07 01:05:26 +09:00
|
|
|
|
}
|
|
|
|
|
|
2016-02-08 11:32:59 +09:00
|
|
|
|
func listHandler(w http.ResponseWriter, r *http.Request) {
|
2016-02-10 07:18:05 +09:00
|
|
|
|
file, err := os.Open(loginsPath)
|
2016-02-08 11:32:59 +09:00
|
|
|
|
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, " ")
|
2016-02-09 06:20:04 +09:00
|
|
|
|
if len(tokens) == 3 {
|
2016-02-08 11:32:59 +09:00
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-08 00:28:08 +09:00
|
|
|
|
func twtxtPostHandler(w http.ResponseWriter, r *http.Request) {
|
2016-02-09 07:36:24 +09:00
|
|
|
|
name, err := login(w, r)
|
2016-02-07 01:05:26 +09:00
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2016-02-08 00:28:08 +09:00
|
|
|
|
text := r.FormValue("twt")
|
2016-02-10 09:23:39 +09:00
|
|
|
|
twtsFile := feedsPath + "/" + name
|
2016-02-07 01:05:26 +09:00
|
|
|
|
createFileIfNotExists(twtsFile)
|
|
|
|
|
text = strings.Replace(text, "\n", " ", -1)
|
|
|
|
|
appendToFile(twtsFile, time.Now().Format(time.RFC3339)+"\t"+text+"\n")
|
2016-02-10 09:23:39 +09:00
|
|
|
|
http.Redirect(w, r, "/"+feedsDir+"/"+name, 302)
|
2016-02-07 01:05:26 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func twtxtHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
name := mux.Vars(r)["name"]
|
|
|
|
|
if !onlyLegalRunes(name) {
|
2016-02-08 00:28:08 +09:00
|
|
|
|
execTemplate(w, "error.html", "Bad path.")
|
2016-02-07 01:05:26 +09:00
|
|
|
|
return
|
|
|
|
|
}
|
2016-02-10 09:23:39 +09:00
|
|
|
|
path := feedsPath + "/" + name
|
2016-02-07 01:05:26 +09:00
|
|
|
|
if _, err := os.Stat(path); err != nil {
|
2016-02-08 00:28:08 +09:00
|
|
|
|
execTemplate(w, "error.html", "Empty twtxt for user.")
|
2016-02-07 01:05:26 +09:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
http.ServeFile(w, r, path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
var err error
|
2016-02-10 06:50:00 +09:00
|
|
|
|
portPtr := flag.Int("port", 8000, "port to serve")
|
|
|
|
|
keyPtr := flag.String("key", "", "SSL key file")
|
|
|
|
|
certPtr := flag.String("cert", "", "SSL certificate file")
|
2016-02-10 07:18:05 +09:00
|
|
|
|
templDirPtr := flag.String("templates",
|
|
|
|
|
os.Getenv("GOPATH")+"/src/htwtxt/templates",
|
|
|
|
|
"directory where to expect HTML templates")
|
|
|
|
|
flag.StringVar(&dataDir, "dir", os.Getenv("HOME")+"/htwtxt",
|
|
|
|
|
"directory to store feeds and login data")
|
2016-02-10 06:50:00 +09:00
|
|
|
|
flag.Parse()
|
2016-02-10 07:18:05 +09:00
|
|
|
|
log.Println("Using as templates dir:", *templDirPtr)
|
|
|
|
|
log.Println("Using as data dir:", dataDir)
|
|
|
|
|
loginsPath = dataDir + "/" + loginsFile
|
2016-02-10 09:23:39 +09:00
|
|
|
|
feedsPath = dataDir + "/" + feedsDir
|
2016-02-10 06:50:00 +09:00
|
|
|
|
if ("" == *keyPtr && "" != *certPtr) ||
|
|
|
|
|
("" != *keyPtr && "" == *certPtr) {
|
|
|
|
|
log.Fatal("Expect either both key and certificate or none.")
|
2016-02-07 01:05:26 +09:00
|
|
|
|
}
|
2016-02-10 06:50:00 +09:00
|
|
|
|
if "" != *keyPtr {
|
2016-02-10 07:18:05 +09:00
|
|
|
|
log.Println("Using TLS.")
|
2016-02-10 06:50:00 +09:00
|
|
|
|
if _, err := os.Stat(*certPtr); err != nil {
|
2016-02-07 01:05:26 +09:00
|
|
|
|
log.Fatal("No certificate file found.")
|
|
|
|
|
}
|
2016-02-10 06:50:00 +09:00
|
|
|
|
if _, err := os.Stat(*keyPtr); err != nil {
|
2016-02-07 01:05:26 +09:00
|
|
|
|
log.Fatal("No server key file found.")
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-02-10 07:18:05 +09:00
|
|
|
|
createFileIfNotExists(loginsPath)
|
2016-02-07 01:05:26 +09:00
|
|
|
|
// TODO: Handle err here.
|
2016-02-10 09:23:39 +09:00
|
|
|
|
_ = os.Mkdir(feedsPath, 0700)
|
2016-02-10 07:18:05 +09:00
|
|
|
|
templ, err = template.New("main").ParseGlob(*templDirPtr + "/*.html")
|
2016-02-07 01:05:26 +09:00
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal("Can't set up new template: ", err)
|
|
|
|
|
}
|
|
|
|
|
router := mux.NewRouter()
|
|
|
|
|
router.HandleFunc("/", indexHandler)
|
2016-02-09 10:37:55 +09:00
|
|
|
|
router.HandleFunc("/feeds", listHandler).Methods("GET")
|
|
|
|
|
router.HandleFunc("/feeds/", listHandler)
|
2016-02-09 07:36:24 +09:00
|
|
|
|
router.HandleFunc("/account", accountFormHandler).Methods("GET")
|
|
|
|
|
router.HandleFunc("/account", accountPostHandler).Methods("POST")
|
2016-02-08 00:28:08 +09:00
|
|
|
|
router.HandleFunc("/signup", signUpFormHandler).Methods("GET")
|
2016-02-07 01:05:26 +09:00
|
|
|
|
router.HandleFunc("/signup", signUpHandler).Methods("POST")
|
2016-02-09 10:37:55 +09:00
|
|
|
|
router.HandleFunc("/feeds", twtxtPostHandler).Methods("POST")
|
|
|
|
|
router.HandleFunc("/feeds/{name}", twtxtHandler)
|
2016-02-07 01:05:26 +09:00
|
|
|
|
http.Handle("/", router)
|
2016-02-10 06:50:00 +09:00
|
|
|
|
log.Println("serving at port", *portPtr)
|
|
|
|
|
if "" != *keyPtr {
|
|
|
|
|
err = http.ListenAndServeTLS(":"+strconv.Itoa(*portPtr),
|
|
|
|
|
*certPtr, *keyPtr, nil)
|
2016-02-07 01:05:26 +09:00
|
|
|
|
} else {
|
2016-02-10 06:50:00 +09:00
|
|
|
|
err = http.ListenAndServe(":"+strconv.Itoa(*portPtr), nil)
|
2016-02-07 01:05:26 +09:00
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal("ListenAndServe: ", err)
|
|
|
|
|
}
|
|
|
|
|
}
|