mt2fa-server/main.go

848 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
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)
//FIXME 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)
}