Every 15 minutes, or whenever a packet is processed and more than 15 minutes have elapsed, we prune tokens that have expired and expired more than 24h earlier. We want to keep expired tokens around for a little bit so we can inform clients that their tokens were expired, instead of replying with less informative error messages.
859 lines
24 KiB
Go
859 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"database/sql"
|
|
"encoding/base32"
|
|
"encoding/json"
|
|
"github.com/badoux/checkmail"
|
|
_ "github.com/mattn/go-sqlite3" // MIT licensed.
|
|
"github.com/spf13/viper"
|
|
"gopkg.in/gomail.v2"
|
|
"io/ioutil"
|
|
"log"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/fcgi"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// Change our logging to be journalctl friendly
|
|
type logWriter struct {
|
|
}
|
|
|
|
func (writer logWriter) Write(bytes []byte) (int, error) {
|
|
return fmt.Print(string(bytes))
|
|
}
|
|
|
|
// DB related stuff
|
|
var db *sql.DB
|
|
|
|
var pruned int64
|
|
type Token struct {
|
|
token string
|
|
cookie string
|
|
created int64
|
|
expiry int64
|
|
request []byte
|
|
confirmed bool
|
|
}
|
|
|
|
type Server struct {
|
|
server_id string
|
|
created int64
|
|
email string
|
|
data string
|
|
ip string
|
|
}
|
|
|
|
type Identity struct {
|
|
email string
|
|
created int64
|
|
data string
|
|
}
|
|
|
|
type Player struct {
|
|
email string
|
|
name string
|
|
server_id string
|
|
created int64
|
|
data string
|
|
}
|
|
|
|
// JSON data blobs
|
|
type Player_data struct {
|
|
Auth_required string `json:"auth_required"`
|
|
}
|
|
|
|
// JSON interface to WWW, and gameserver
|
|
type Serverdata struct {
|
|
Owner string `json:"owner"`
|
|
Name string `json:"name"`
|
|
Address string `json:"address"`
|
|
Url string `json:"url"`
|
|
Announce string `json:"announce"`
|
|
AnnounceUrl string `json:"announce_url"`
|
|
}
|
|
|
|
type tfa_request struct {
|
|
// required
|
|
Request_type string `json:"request_type"`
|
|
Remote_ip string `json:"remote_ip"`
|
|
// optional, need to verify they're present at a later stage
|
|
Email string `json:"email",omitempty`
|
|
Player string `json:"player",omitempty`
|
|
Server_id string `json:"server_id",omitempty`
|
|
Token string `json:"token",omitmempty`
|
|
Cookie string `json:"cookie",omitempty`
|
|
Serverdata Serverdata `json:"server_data"`
|
|
}
|
|
|
|
type tfa_response struct {
|
|
Result string `json:"result"`
|
|
Info string `json:"info"`
|
|
// optional
|
|
Data map[string]string `json:"data"`
|
|
}
|
|
|
|
// misc functions
|
|
func make_token() string {
|
|
b := make([]byte, 24)
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
log.Fatal("Error creating token: ", err)
|
|
}
|
|
|
|
s := base32.StdEncoding.EncodeToString(b)
|
|
|
|
return s[:len(s)-1]
|
|
}
|
|
|
|
func validate_email(email string) bool {
|
|
err := checkmail.ValidateFormat(email)
|
|
if err != nil {
|
|
log.Printf("email: %v: %v\n", email, err)
|
|
return false
|
|
}
|
|
//FIXME enable this when not running behind a NAT
|
|
//err = checkmail.ValidateHost(email)
|
|
//if err != nil {
|
|
// log.Printf("email: %v: %v\n", email, err)
|
|
//}
|
|
//if smtpErr, ok := err.(checkmail.SmtpError); ok && err != nil {
|
|
// log.Printf("email: %v: code: %s, msg: %s", email, smtpErr.Code(), smtpErr)
|
|
// return false
|
|
//}
|
|
return true
|
|
}
|
|
|
|
func do_email(email string, message string) bool {
|
|
m := gomail.NewMessage()
|
|
m.SetHeader("From", viper.GetString("email_sender"))
|
|
m.SetHeader("To", email)
|
|
m.SetHeader("Subject", "Minetest 2-factor confirmation request")
|
|
m.SetBody("text/plain", message)
|
|
|
|
d := gomail.Dialer{Host: viper.GetString("smtp_server"), Port: viper.GetInt("smtp_port")}
|
|
|
|
user := viper.GetString("smtp_user")
|
|
if user != "" {
|
|
d.Username = user
|
|
d.Password = viper.GetString("smtp_pass")
|
|
}
|
|
|
|
if viper.GetBool("smtp_verify_certificate") == false {
|
|
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
|
}
|
|
|
|
if err := d.DialAndSend(m); err != nil {
|
|
log.Println(err)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
type FastCGIServer struct{}
|
|
|
|
func (s FastCGIServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
// track IP of client, we'll need it later for some transactions
|
|
ip, _, err := net.SplitHostPort(req.RemoteAddr)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
log.Print("Request: unable to identify peer\n")
|
|
return
|
|
}
|
|
remoteip := net.ParseIP(ip).String()
|
|
|
|
if req.Method != "POST" {
|
|
w.Header().Set("Access-Control-Allow-Headers", "SHOO")
|
|
http.Error(w, err.Error(), http.StatusMethodNotAllowed)
|
|
log.Printf("Invalid GET from %v\n", remoteip)
|
|
return
|
|
}
|
|
|
|
// parse POST data
|
|
body, err := ioutil.ReadAll(req.Body)
|
|
defer req.Body.Close()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
log.Printf("Request: %v: unable to read body\n", remoteip)
|
|
return
|
|
}
|
|
|
|
var rq tfa_request
|
|
err = json.Unmarshal(body, &rq)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
log.Printf("Request: %v: malformed JSON data\n", remoteip)
|
|
return
|
|
}
|
|
rq.Remote_ip = remoteip
|
|
|
|
// create response
|
|
var rp tfa_response
|
|
|
|
// prefetch server name for requestst that want it
|
|
servername := "(no name)"
|
|
if rq.Request_type == "REG" || rq.Request_type == "AUTH" {
|
|
// get relevant info, ignore errors
|
|
var data []byte
|
|
err = db.QueryRow("SELECT data FROM servers WHERE server_id=?", rq.Server_id).Scan(&data)
|
|
if err == nil {
|
|
var sd Serverdata
|
|
err = json.Unmarshal(data, &sd)
|
|
if err == nil && sd.Name != "" {
|
|
servername = sd.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
// validate request origin is valid
|
|
var orip string
|
|
err = db.QueryRow("SELECT ip FROM servers WHERE server_id=?", rq.Server_id).Scan(&orip)
|
|
if err != nil || orip != remoteip {
|
|
switch rq.Request_type {
|
|
case "CONFIRM":
|
|
case "SERVER":
|
|
case "SERVERSTAT":
|
|
case "SERVERIP":
|
|
case "SERVERIPSTAT":
|
|
default:
|
|
rp = tfa_response{"SERVERIPFAIL", "The server IP address changed. The server owner will need to confirm\nthis change before normal events can be handled again.", nil}
|
|
goto send_response
|
|
}
|
|
}
|
|
|
|
// process request
|
|
switch rq.Request_type {
|
|
case "REG":
|
|
if rq.Email == "" || rq.Player == "" || rq.Server_id == "" {
|
|
rp = tfa_response{"REGFAIL", "Registration failed, insufficient data.", nil}
|
|
break
|
|
}
|
|
if !validate_email(rq.Email) {
|
|
rp = tfa_response{"REGFAIL", "Registration failed, email invalid.", nil}
|
|
break
|
|
}
|
|
|
|
// check identities table for rq.Email
|
|
var created int64
|
|
err := db.QueryRow("SELECT created FROM identities WHERE email=?", rq.Email).Scan(&created)
|
|
if err == nil {
|
|
// already created? could just happen
|
|
var created int64
|
|
err = db.QueryRow("SELECT created FROM players WHERE email=? AND player=?", rq.Email, rq.Player).Scan(&created)
|
|
if err == nil {
|
|
rp = tfa_response{"REGOK", "This email is already registered.", nil}
|
|
break
|
|
}
|
|
// store the server/player combo
|
|
_, err = db.Exec("INSERT INTO players(email, name, server_id, created, data) VALUES (?, ?, ?, ?, ?)",
|
|
rq.Email, rq.Player, rq.Server_id, time.Now().Unix(), "{}")
|
|
if err != nil {
|
|
rp = tfa_response{"REGFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
// looks like this was already registered as an identity!
|
|
rp = tfa_response{"REGOK", "This email is already registered.", nil}
|
|
break
|
|
}
|
|
|
|
// no existing identity
|
|
token := make_token()
|
|
cookie := make_token()
|
|
|
|
// send the confirmation email
|
|
if !do_email(rq.Email,
|
|
"\nHello,\n\n"+
|
|
"You've received this request because you or someone registered this email\n"+
|
|
"address on a minetest server at \""+servername+"\".\n\n"+
|
|
"If this wasn't you, you can safely ignore this email. If it was you, please\n"+
|
|
"click the following link to confirm your registration:\n\n"+
|
|
" " + viper.GetString("base_url") + "confirm?t="+token+"\n\n") {
|
|
rp = tfa_response{"REGFAIL", "Registration failed, unable to send email.", nil}
|
|
break
|
|
}
|
|
|
|
// store the token
|
|
j, err := json.Marshal(rq)
|
|
if err != nil {
|
|
log.Println(err)
|
|
rp = tfa_response{"REGFAIL", "Registration failed, internal server error.", nil}
|
|
break
|
|
}
|
|
t := Token{token, cookie, time.Now().Unix(), time.Now().Unix() + 300, j, false}
|
|
_, err = db.Exec("INSERT INTO tokens(token, cookie, created, expiry, request) VALUES (?, ?, ?, ?, ?)",
|
|
t.token, t.cookie, t.created, t.expiry, t.request)
|
|
if err != nil {
|
|
log.Println(err)
|
|
rp = tfa_response{"REGFAIL", "Registration failed, internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
rp = tfa_response{"REGPEND", "Mail sent. Check your mailbox.", nil}
|
|
rp.Data = make(map[string]string)
|
|
rp.Data["Cookie"] = cookie
|
|
case "REGSTAT":
|
|
var created int64
|
|
err := db.QueryRow("SELECT created FROM identities WHERE email=?", rq.Email).Scan(&created)
|
|
if err == nil {
|
|
rp = tfa_response{"REGOK", "Registration succeeded.", nil}
|
|
break
|
|
}
|
|
// check if a token exists.
|
|
var confirmed bool
|
|
err = db.QueryRow("SELECT confirmed FROM tokens WHERE cookie=?", rq.Cookie).Scan(&confirmed)
|
|
if err == nil {
|
|
if confirmed {
|
|
// badness
|
|
rp = tfa_response{"REGFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
rp = tfa_response{"REGPEND", "Mail sent. Check your mailbox.", nil}
|
|
break
|
|
}
|
|
rp = tfa_response{"REGFAIL", "Registration failed.", nil}
|
|
case "AUTH":
|
|
// check server token + username combo exists
|
|
var email string
|
|
err := db.QueryRow("SELECT email FROM players WHERE name=? AND server_id=?", rq.Player, rq.Server_id).Scan(&email)
|
|
if err != nil {
|
|
rp = tfa_response{"AUTHFAIL", "Authentication failed.", nil}
|
|
break
|
|
}
|
|
|
|
// send an AUTH email
|
|
token := make_token()
|
|
cookie := make_token()
|
|
|
|
// send the confirmation email
|
|
if !do_email(email,
|
|
"\nHello,\n\n"+
|
|
"You've received this request because you or someone wants to authenticate\n"+
|
|
"using this email address on a minetest server at \""+servername+"\".\n\n"+
|
|
"If this wasn't you, you can safely ignore this email. If it was you, please\n"+
|
|
"click the following link to confirm your authentication:\n\n"+
|
|
" " + viper.GetString("base_url") + "confirm?t="+token+"\n\n") {
|
|
rp = tfa_response{"AUTHFAIL", "Authentication failed, unable to send email.", nil}
|
|
break
|
|
}
|
|
|
|
// store the token
|
|
j, err := json.Marshal(rq)
|
|
if err != nil {
|
|
log.Println(err)
|
|
rp = tfa_response{"AUTHFAIL", "Authentication failed, internal server error.", nil}
|
|
break
|
|
}
|
|
t := Token{token, cookie, time.Now().Unix(), time.Now().Unix() + 300, j, false}
|
|
_, err = db.Exec("INSERT INTO tokens(token, cookie, created, expiry, request) VALUES (?, ?, ?, ?, ?)",
|
|
t.token, t.cookie, t.created, t.expiry, t.request)
|
|
if err != nil {
|
|
log.Println(err)
|
|
rp = tfa_response{"AUTHFAIL", "Authentication failed, internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
rp = tfa_response{"AUTHPEND", "Mail sent. Check your mailbox.", nil}
|
|
rp.Data = make(map[string]string)
|
|
rp.Data["Cookie"] = cookie
|
|
case "AUTHSTAT":
|
|
// check tokens
|
|
var confirmed bool
|
|
var expiry int64
|
|
err := db.QueryRow("SELECT confirmed, expiry FROM tokens WHERE cookie=?", rq.Cookie).Scan(&confirmed, &expiry)
|
|
if err != nil {
|
|
// there is no token
|
|
rp = tfa_response{"AUTHFAIL", "Server registration failed.", nil}
|
|
break
|
|
}
|
|
if time.Now().Unix() > expiry {
|
|
rp = tfa_response{"AUTHFAIL", "Authentication failed.", nil}
|
|
break
|
|
}
|
|
if !confirmed {
|
|
rp = tfa_response{"AUTHPEND", "Mail sent. Check your mailbox.", nil}
|
|
break
|
|
}
|
|
rp = tfa_response{"AUTHOK", "Authentication succeeded.", nil}
|
|
case "ACCT":
|
|
// this is sent by a server to see if the account is required to
|
|
// authenticate, and / or to inspect stat data (not implemented yet)
|
|
var data []byte
|
|
var email string
|
|
err := db.QueryRow("SELECT data, email FROM players WHERE name=? AND server_id=?", rq.Player, rq.Server_id).Scan(&data, &email)
|
|
if err != nil {
|
|
rp = tfa_response{"ACCTFAIL", "Request failed.", nil}
|
|
break
|
|
}
|
|
var pd Player_data
|
|
err = json.Unmarshal(data, &pd)
|
|
if pd.Auth_required != "1" {
|
|
err := db.QueryRow("SELECT data FROM identities WHERE email=?", email).Scan(&data)
|
|
if err != nil {
|
|
rp = tfa_response{"ACCTFAIL", "Request failed.", nil}
|
|
break
|
|
}
|
|
err = json.Unmarshal(data, &pd)
|
|
if pd.Auth_required != "1" {
|
|
rp = tfa_response{"ACCTOK", "Account info retrieved.", nil}
|
|
break
|
|
}
|
|
}
|
|
rp = tfa_response{"ACCTOK", "Account info retrieved. Player must authenticate", nil}
|
|
rp.Data = make(map[string]string)
|
|
rp.Data["Auth_required"] = "1"
|
|
// get playerdata struct json
|
|
case "UPDATES":
|
|
// check if server sent server_data changes
|
|
if rq.Serverdata.Owner != "" {
|
|
// refresh server data
|
|
//FIXME make sure all the required fields are present again
|
|
s, err := json.Marshal(rq.Serverdata)
|
|
if err != nil {
|
|
log.Println("Error storing Serverdata")
|
|
} else {
|
|
_, err = db.Exec("UPDATE servers set data=? WHERE server_id=?",
|
|
s, rq.Server_id)
|
|
if err != nil {
|
|
log.Println("Error updating Serverdata")
|
|
} else {
|
|
log.Println(remoteip + ": Updated server info for \"" + servername + "\"")
|
|
}
|
|
}
|
|
}
|
|
//FIXME implement some updates - rp = tfa_response{"UPDATE", "Changes requested.", nil}
|
|
rp = tfa_response{"NOUPDATES", "No changes for server.", nil}
|
|
case "SERVER":
|
|
// server registration request.
|
|
if rq.Email == "" {
|
|
rp = tfa_response{"SERVERFAIL", "Registration failed, insufficient data.", nil}
|
|
break
|
|
}
|
|
if rq.Server_id != "" {
|
|
rp = tfa_response{"SERVERFAIL", "Registration failed, you provided a server id. This server is already registered.", nil}
|
|
break
|
|
}
|
|
if !validate_email(rq.Email) {
|
|
rp = tfa_response{"SERVERFAIL", "Registration failed, email invalid.", nil}
|
|
break
|
|
}
|
|
|
|
// validate serverdata is sufficient
|
|
if rq.Serverdata.Owner == "" {
|
|
rp = tfa_response{"SERVERFAIL", "Registration failed, insufficient data.", nil}
|
|
break
|
|
}
|
|
|
|
token := make_token()
|
|
cookie := make_token()
|
|
|
|
// send the confirmation email
|
|
if !do_email(rq.Email,
|
|
"\nHello,\n\n"+
|
|
"You've received this request because you or someone wants to register\n"+
|
|
"a server using this email address at \""+remoteip+"\".\n\n"+
|
|
"If this wasn't you, you can safely ignore this email. If it was you, please\n"+
|
|
"click the following link to confirm your server registration:\n\n"+
|
|
" " + viper.GetString("base_url") + "confirm?t="+token+"\n\n") {
|
|
rp = tfa_response{"SERVERFAIL", "Server registration failed, unable to send email.", nil}
|
|
break
|
|
}
|
|
|
|
// store the token as "server_id" in the request
|
|
rq.Server_id = token
|
|
|
|
// store the token including original request data
|
|
j, err := json.Marshal(rq)
|
|
if err != nil {
|
|
log.Println(err)
|
|
rp = tfa_response{"SERVERFAIL", "Server registration failed, internal server error.", nil}
|
|
break
|
|
}
|
|
t := Token{token, cookie, time.Now().Unix(), time.Now().Unix() + 300, j, false}
|
|
_, err = db.Exec("INSERT INTO tokens(token, cookie, created, expiry, request) VALUES (?, ?, ?, ?, ?)",
|
|
token, cookie, t.created, t.expiry, t.request)
|
|
if err != nil {
|
|
log.Println(err)
|
|
rp = tfa_response{"SERVERFAIL", "Server registration failed, internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
rp = tfa_response{"SERVERPEND", "Mail sent. Check your mailbox.", nil}
|
|
// send the token to the server, so that the server can use it to validate the registration
|
|
// after confirmation
|
|
rp.Data = make(map[string]string)
|
|
rp.Data["Cookie"] = cookie
|
|
case "SERVERIP":
|
|
// server IP address request.
|
|
if rq.Email == "" || rq.Server_id == "" {
|
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed, insufficient data.", nil}
|
|
break
|
|
}
|
|
|
|
// validate email is actually the one on file for this server
|
|
var serverid, email string
|
|
err := db.QueryRow("SELECT server_id, email FROM servers WHERE server_id=? and email=?", rq.Server_id, rq.Email).Scan(&serverid, &email)
|
|
if err != nil {
|
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed, invalid data.", nil}
|
|
break
|
|
}
|
|
|
|
token := make_token()
|
|
cookie := make_token()
|
|
|
|
// send the confirmation email
|
|
if !do_email(rq.Email,
|
|
"\nHello,\n\n"+
|
|
"You've received this request because you or someone wants to change the IP\n"+
|
|
"address of a server using this email address at \""+remoteip+"\".\n\n"+
|
|
"If this wasn't you, you can safely ignore this email. If it was you, please\n"+
|
|
"click the following link to confirm your server IP change:\n\n"+
|
|
" " + viper.GetString("base_url") + "confirm?t="+token+"\n\n") {
|
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed, unable to send email.", nil}
|
|
break
|
|
}
|
|
|
|
// store the token including original request data
|
|
j, err := json.Marshal(rq)
|
|
if err != nil {
|
|
log.Println(err)
|
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed, internal server error.", nil}
|
|
break
|
|
}
|
|
t := Token{token, cookie, time.Now().Unix(), time.Now().Unix() + 300, j, false}
|
|
_, err = db.Exec("INSERT INTO tokens(token, cookie, created, expiry, request) VALUES (?, ?, ?, ?, ?)",
|
|
token, cookie, t.created, t.expiry, t.request)
|
|
if err != nil {
|
|
log.Println(err)
|
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed, internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
rp = tfa_response{"SERVERIPPEND", "Mail sent. Check your mailbox.", nil}
|
|
// send the token to the server, so that the server can use it to validate the registration
|
|
// after confirmation
|
|
rp.Data = make(map[string]string)
|
|
rp.Data["Cookie"] = cookie
|
|
case "SERVERSTAT":
|
|
// verify that server_id is registered
|
|
var token string
|
|
var expiry int64
|
|
var confirmed bool
|
|
err := db.QueryRow("SELECT token, expiry, confirmed FROM tokens WHERE cookie=?", rq.Cookie).Scan(
|
|
&token, &expiry, &confirmed)
|
|
if err == nil {
|
|
var created int
|
|
err := db.QueryRow("SELECT created FROM servers WHERE server_id=?", token).Scan(&created)
|
|
if err == nil {
|
|
rp = tfa_response{"SERVEROK", "Server registration succeeded.", nil}
|
|
rp.Data = make(map[string]string)
|
|
rp.Data["Server_id"] = token
|
|
break
|
|
}
|
|
|
|
if time.Now().Unix() > expiry {
|
|
rp = tfa_response{"SERVERFAIL", "Server registration failed.", nil}
|
|
break
|
|
}
|
|
if !confirmed {
|
|
rp = tfa_response{"SERVERPEND", "Server registration pending. Check your email.", nil}
|
|
break
|
|
}
|
|
}
|
|
|
|
rp = tfa_response{"SERVERFAIL", "Server registration failed. Try again", nil}
|
|
break
|
|
case "SERVERIPSTAT":
|
|
// verify that server_id is registered
|
|
var expiry int64
|
|
var confirmed bool
|
|
err := db.QueryRow("SELECT expiry, confirmed FROM tokens WHERE cookie=?", rq.Cookie).Scan(
|
|
&expiry, &confirmed)
|
|
if err == nil {
|
|
var ip string
|
|
err := db.QueryRow("SELECT ip FROM servers WHERE server_id=?", rq.Server_id).Scan(&ip)
|
|
if err == nil {
|
|
if rq.Remote_ip == ip {
|
|
rp = tfa_response{"SERVERIPOK", "Server IP change succeeded.", nil}
|
|
break
|
|
}
|
|
}
|
|
|
|
if time.Now().Unix() > expiry {
|
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed.", nil}
|
|
break
|
|
}
|
|
if !confirmed {
|
|
rp = tfa_response{"SERVERIPPEND", "Server IP change pending. Check your email.", nil}
|
|
break
|
|
}
|
|
}
|
|
|
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed. Try again", nil}
|
|
break
|
|
|
|
//
|
|
// www initiated requests
|
|
//
|
|
case "CONFIRM":
|
|
if rq.Token == "" {
|
|
rp = tfa_response{"CONFIRMFAIL", "Invalid confirmation.", nil}
|
|
break
|
|
}
|
|
|
|
// fetch entry from tokens table
|
|
var ot Token
|
|
err := db.QueryRow("SELECT created, expiry, request, confirmed FROM tokens WHERE token=?",
|
|
rq.Token).Scan(&ot.created, &ot.expiry, &ot.request, &ot.confirmed)
|
|
if err != nil {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
// check if we didn't already do this
|
|
if ot.confirmed {
|
|
rp = tfa_response{"CONFIRMOK", "Request already completed before.", nil}
|
|
break
|
|
}
|
|
|
|
// check if token not too old
|
|
if time.Now().Unix() > ot.expiry {
|
|
rp = tfa_response{"CONFIRMFAIL", "Request token expired. Create a new request.", nil}
|
|
break
|
|
}
|
|
|
|
// fetch original request
|
|
var or tfa_request
|
|
err = json.Unmarshal(ot.request, &or)
|
|
if err != nil {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
// complete the transaction!
|
|
if or.Request_type == "REG" {
|
|
_, err = db.Exec("INSERT INTO identities (email, created, data) VALUES (?, ?, ?)",
|
|
or.Email, time.Now().Unix(), "{}")
|
|
if err != nil {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
// Mark as done
|
|
_, err = db.Exec("UPDATE tokens SET confirmed=? WHERE token=?",
|
|
true, rq.Token)
|
|
if err != nil {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
// store the server/player combo
|
|
_, err = db.Exec("INSERT INTO players(email, name, server_id, created, data) VALUES (?, ?, ?, ?, ?)",
|
|
or.Email, or.Player, or.Server_id, time.Now().Unix(), "{}")
|
|
if err != nil {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
// Nothing left to do
|
|
rp = tfa_response{"CONFIRMOK", "Request completed. Your identity is now registered.", nil}
|
|
break
|
|
|
|
} else if or.Request_type == "AUTH" {
|
|
// Mark as done
|
|
_, err = db.Exec("UPDATE tokens SET confirmed=? WHERE token=?",
|
|
true, rq.Token)
|
|
if err != nil {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
// Nothing left to do
|
|
rp = tfa_response{"CONFIRMOK", "Request completed. You are now authenticated.", nil}
|
|
break
|
|
|
|
} else if or.Request_type == "SERVER" {
|
|
s, err := json.Marshal(or.Serverdata)
|
|
if err != nil {
|
|
s = []byte("{}")
|
|
log.Println("Error storing Serverdata")
|
|
}
|
|
_, err = db.Exec("INSERT INTO servers (server_id, email, created, data, ip) VALUES (?, ?, ?, ?, ?)",
|
|
or.Server_id, or.Email, time.Now().Unix(), s, or.Remote_ip)
|
|
if err != nil {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
// Mark as done
|
|
_, err = db.Exec("UPDATE tokens SET confirmed=? WHERE token=?",
|
|
true, rq.Token)
|
|
|
|
if err != nil {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
// Nothing left to do
|
|
rp = tfa_response{"CONFIRMOK", "Request completed. Your server is now registered.", nil}
|
|
break
|
|
|
|
} else if or.Request_type == "SERVERIP" {
|
|
_, err = db.Exec("UPDATE servers SET ip=? WHERE server_id=?",
|
|
or.Remote_ip, or.Server_id)
|
|
if err != nil {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
// Mark as done
|
|
_, err = db.Exec("UPDATE tokens SET confirmed=? WHERE token=?",
|
|
true, rq.Token)
|
|
|
|
if err != nil {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
// Nothing left to do
|
|
rp = tfa_response{"CONFIRMOK", "Request completed. Your server IP address now changed.", nil}
|
|
break
|
|
} else {
|
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
|
break
|
|
}
|
|
|
|
//passwdchange
|
|
//emailchange
|
|
default:
|
|
rp = tfa_response{"UNK", "Unknown request type. Don't do that again.", nil}
|
|
}
|
|
|
|
send_response:
|
|
// and send to the client
|
|
output, err := json.Marshal(rp)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
log.Print("Response: formatting response failed\n")
|
|
return
|
|
}
|
|
|
|
log.Printf("%v: %v->%v (%v) \"%v\"\n",
|
|
remoteip, rq.Request_type, rp.Result,
|
|
rq.Player, rp.Info)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(output)
|
|
|
|
// prune tokens occasionally
|
|
if time.Now().Unix() > pruned + 900 {
|
|
pruned = time.Now().Unix()
|
|
cull := time.Now().Unix() - 86400
|
|
|
|
_, err = db.Exec("DELETE FROM tokens WHERE expired <= ?", cull)
|
|
if err != nil {
|
|
log.Println("Failed to prune tokens.")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func main() {
|
|
// Custom log format
|
|
log.SetFlags(0)
|
|
log.SetOutput(new(logWriter))
|
|
|
|
// config stuffs
|
|
viper.SetConfigName("mt2fa")
|
|
viper.SetConfigType("yaml")
|
|
|
|
viper.AddConfigPath("/usr/share/defaults/etc")
|
|
viper.AddConfigPath("/etc")
|
|
viper.AddConfigPath("$HOME/.config")
|
|
|
|
viper.SetDefault("socket", "/run/mt2fa/sock")
|
|
|
|
viper.SetDefault("email_sender", "nobody@localhost.localdomain")
|
|
viper.SetDefault("smtp_server", "localhost")
|
|
viper.SetDefault("smtp_port", 587)
|
|
viper.SetDefault("smtp_verify_certificate", true)
|
|
viper.SetDefault("smtp_user", "")
|
|
viper.SetDefault("sqlite_db", "mt2fa.sqlite")
|
|
viper.SetDefault("base_url", "https://localhost/")
|
|
|
|
err := viper.ReadInConfig()
|
|
if err != nil {
|
|
log.Fatal("Error in confog file: ", err)
|
|
}
|
|
|
|
// listen on fcgi socket
|
|
s := viper.GetString("socket")
|
|
os.Remove(s)
|
|
|
|
listener, err := net.Listen("unix", s)
|
|
if err != nil {
|
|
log.Fatal("net.Listen: ", err)
|
|
}
|
|
|
|
os.Chmod(s, 0666)
|
|
|
|
defer listener.Close()
|
|
|
|
// open our db
|
|
db, err = sql.Open("sqlite3", viper.GetString("sqlite_db"))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// initialize our db as needed
|
|
createStmt := `
|
|
CREATE TABLE IF NOT EXISTS tokens (
|
|
token TEXT NOT NULL PRIMARY KEY,
|
|
cookie TEXT NOT NULL,
|
|
created INTEGER NOT NULL,
|
|
expiry INTEGER NOT NULL,
|
|
request TEXT NOT NULL,
|
|
confirmed BOOLEAN DEFAULT FALSE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS servers (
|
|
server_id TEXT NOT NULL PRIMARY KEY,
|
|
created INTEGER NOT NULL,
|
|
email TEXT NOT NULL,
|
|
data TEXT NOT NULL,
|
|
ip TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS identities (
|
|
email TEXT NOT NULL PRIMARY KEY,
|
|
created INTEGER NOT NULL,
|
|
data TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS players (
|
|
email TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
server_id TEXT NOT NULL,
|
|
created INTEGER NOT NULL,
|
|
data TEXT NOT NULL
|
|
);
|
|
`
|
|
_, err = db.Exec(createStmt)
|
|
if err != nil {
|
|
log.Fatal("%q: %s\n", err, createStmt)
|
|
}
|
|
|
|
// serve requests.
|
|
h := new(FastCGIServer)
|
|
|
|
log.Print("started")
|
|
|
|
err = fcgi.Serve(listener, h)
|
|
}
|