akyuu/main.go

313 行
8.5 KiB
Go
Raw Blame 履歴

このファイルには曖昧(ambiguous)なUnicode文字が含まれています

このファイルには、他の文字と見間違える可能性があるUnicode文字が含まれています。 それが意図的なものと考えられる場合は、この警告を無視して構いません。 それらの文字を表示するにはエスケープボタンを使用します。

// htwtxt hosted twtxt server; see README for copyright and license info
package main
import "errors"
import "flag"
import "fmt"
import "golang.org/x/crypto/bcrypt"
import "golang.org/x/crypto/ssh/terminal"
import "gopkg.in/gomail.v2"
import "html/template"
import "io/ioutil"
import "log"
import "net"
import "net/http"
import "os"
import "strconv"
import "strings"
import "syscall"
import "time"
const resetLinkExp = 1800
const resetWaitTime = 3600 * 24
const version = "1.0"
var contact string
var dialer *gomail.Dialer
var mailuser string
var myself string
var signupOpen bool
var templ *template.Template
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 handleTemplate(path, msg string) func(w http.ResponseWriter,
r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
execTemplate(w, path, msg)
}
}
func onlyLegalRunes(str string) bool {
const legalUrlChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"abcdefghijklmnopqrstuvwxyz0123456789_"
for _, ru := range str {
if !(strings.ContainsRune(legalUrlChars, ru)) {
return false
}
}
return true
}
func checkDelay(w http.ResponseWriter, ip string) (int, error) {
var err error
var openTime int
delay := -1
if tokens, e := getFromFileEntryFor(ipDelaysPath, ip, 3); e == nil {
openTime, err = strconv.Atoi(tokens[0])
if err != nil {
log.Fatal("Can't parse IP delays file", err)
}
delay, err = strconv.Atoi(tokens[1])
if err != nil {
log.Fatal("Can't parse IP delays file", err)
}
if int(time.Now().Unix()) < openTime {
execTemplate(w, "error.html",
"This IP must wait a while for its "+
"next login attempt.")
err = errors.New("")
}
}
return delay, err
}
func login(w http.ResponseWriter, r *http.Request) (string, error) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
log.Fatal("Can't parse ip from request", err)
}
delay, err := checkDelay(w, ip)
if err != nil {
return "", err
}
name := r.FormValue("name")
pw := r.FormValue("password")
loginValid := false
tokens, err := getFromFileEntryFor(loginsPath, name, 5)
if err == nil && nil == bcrypt.CompareHashAndPassword([]byte(tokens[0]),
[]byte(pw)) {
loginValid = true
if 0 <= delay {
removeLineStartingWith(ipDelaysPath, ip)
}
}
if !loginValid {
newLine := delay == -1
delay = 2 * delay
if -2 == delay {
delay = 1
}
strOpenTime := strconv.Itoa(int(time.Now().Unix()) + delay)
strDelay := strconv.Itoa(delay)
line := ip + "\t" + strOpenTime + "\t" + strDelay
if newLine {
appendToFile(ipDelaysPath, line)
} else {
replaceLineStartingWith(ipDelaysPath, ip, line)
}
execTemplate(w, "error_login.html", "Bad login.")
return name, errors.New("")
}
return name, nil
}
func nameIsLegal(name string) bool {
return !("" == name || !onlyLegalRunes(name) || len(name) > 140)
}
func passwordIsLegal(password string) bool {
return !("" == password)
}
func hashFromPw(pw string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
if err != nil {
log.Fatal("Can't generate hash", err)
}
return string(hash)
}
func newPassword(w http.ResponseWriter, r *http.Request) (string, error) {
pw := r.FormValue("new_password")
pw2 := r.FormValue("new_password2")
if 0 != strings.Compare(pw, pw2) {
return "", errors.New("Password values did not match")
} else if !passwordIsLegal(pw) {
return "", errors.New("Illegal password.")
}
return hashFromPw(pw), nil
}
func newMailAddress(w http.ResponseWriter, r *http.Request) (string, error) {
mail := r.FormValue("mail")
if len(mail) > 140 || strings.ContainsRune(mail, '\n') ||
strings.ContainsRune(mail, '\t') {
return "", errors.New("Illegal mail address.")
}
return mail, nil
}
func newSecurityQuestion(w http.ResponseWriter, r *http.Request) (string,
string, error) {
secquestion := r.FormValue("secquestion")
secanswer := r.FormValue("secanswer")
if "" == secquestion || len(secquestion) > 140 ||
strings.ContainsRune(secquestion, '\n') ||
strings.ContainsRune(secquestion, '\t') {
return "", "", errors.New("Illegal security question.")
} else if "" == secanswer {
return "", "", errors.New("Illegal security question answer.")
}
return secquestion, hashFromPw(secanswer), nil
}
func changeLoginField(w http.ResponseWriter, r *http.Request,
getter func(w http.ResponseWriter, r *http.Request) (string, error),
position int) {
name, err := login(w, r)
if err != nil {
return
}
input, err := getter(w, r)
if err != nil {
execTemplate(w, "error.html", err.Error())
return
}
tokens, err := getFromFileEntryFor(loginsPath, name, 5)
if err != nil {
log.Fatal("Can't get entry for user", err)
}
tokens[position] = input
replaceLineStartingWith(loginsPath, name,
name+"\t"+strings.Join(tokens, "\t"))
execTemplate(w, "feedset.html", "")
}
func nameMyself(ssl bool, port int) string {
resp, err := http.Get("http://myexternalip.com/raw")
defer resp.Body.Close()
if err != nil {
log.Fatal("Trouble getting IP", err)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal("Trouble reading IP message body", err)
}
ip := strings.Replace(string(body), "\n", "", -1)
s := ""
if ssl {
s = "s"
}
return "http" + s + "://" + ip + ":" + strconv.Itoa(port)
}
func addUser(login string) {
fields := strings.Split(login, ":")
if len(fields) != 2 {
log.Fatal("Malformed adduser string, must be NAME:PASSWORD")
}
name := fields[0]
password := fields[1]
if !nameIsLegal(name) {
log.Fatal("Malformed adduser NAME argument.")
}
if !passwordIsLegal(password) {
log.Fatal("Malformed adduser PASSWORD argument.")
}
if _, err := getFromFileEntryFor(loginsPath, name, 5); err == nil {
log.Fatal("Username already taken.")
}
hash := hashFromPw(password)
appendToFile(loginsPath, name+"\t"+hash+"\t\t\t")
fmt.Println("Added user.")
}
func readOptions() (string, int, string, int, string, bool) {
var mailpw string
var mailport int
var mailserver string
var port int
var newLogin string
var showVersion bool
flag.StringVar(&newLogin, "adduser", "", "instead of starting as "+
"server, add user with login NAME:PASSWORD")
flag.IntVar(&port, "port", 8000, "port to serve")
flag.StringVar(&keyPath, "key", "", "SSL key file")
flag.StringVar(&certPath, "cert", "", "SSL certificate file")
flag.StringVar(&templPath, "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")
flag.StringVar(&contact, "contact",
"[operator passed no contact info to server]",
"operator contact info to display on info page")
flag.BoolVar(&signupOpen, "signup", false,
"enable on-site account creation")
flag.BoolVar(&showVersion, "version", false, "show version number")
flag.StringVar(&mailserver, "mailserver", "",
"SMTP server to send mails through")
flag.IntVar(&mailport, "mailport", 0,
"port of SMTP server to send mails through")
flag.StringVar(&mailuser, "mailuser", "",
"username to login with on SMTP server to send mails through")
flag.Parse()
if "" != mailserver && ("" == mailuser || 0 == mailport) {
log.Fatal("Mail server usage needs username and port number")
}
if ("" == keyPath && "" != certPath) ||
("" != keyPath && "" == certPath) {
log.Fatal("Expect either both key and certificate or none.")
}
if "" != mailserver {
fmt.Print("Enter password for smtp server: ")
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
log.Fatal("Trouble reading password")
}
mailpw = string(bytePassword)
fmt.Println("")
}
return mailserver, mailport, mailpw, port, newLogin, showVersion
}
func main() {
var err error
mailserver, mailport, mailpw, port, newLogin, showVersion :=
readOptions()
if showVersion {
fmt.Println("htwtxt", version)
return
}
initFilesAndDirs()
if "" != newLogin {
addUser(newLogin)
return
}
myself = nameMyself("" != keyPath, port)
templ, err = template.New("main").ParseGlob(templPath + "/*.html")
if err != nil {
log.Fatal("Can't set up new template: ", err)
}
http.Handle("/", handleRoutes())
dialer = gomail.NewPlainDialer(mailserver, mailport, mailuser, mailpw)
log.Println("serving at port", port)
if "" != keyPath {
err = http.ListenAndServeTLS(":"+strconv.Itoa(port),
certPath, keyPath, nil)
} else {
err = http.ListenAndServe(":"+strconv.Itoa(port), nil)
}
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}