828 lines
24 KiB
Go
828 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"
|
|
"net"
|
|
"net/http"
|
|
"net/http/fcgi"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// DB related stuff
|
|
var db *sql.DB
|
|
|
|
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")}
|
|
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)
|
|
|
|
//FIXME prune tokens
|
|
}
|
|
|
|
func main() {
|
|
log.SetFlags(0)
|
|
|
|
// 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("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("mt2fa: 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("mt2fa: started")
|
|
|
|
err = fcgi.Serve(listener, h)
|
|
}
|