630 lines
18 KiB
Go
630 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type (
|
|
THttpHandler = func(Context *THttpRequestContext)
|
|
|
|
THttpRoute struct {
|
|
Method string
|
|
Prefix string
|
|
AllowParams bool
|
|
Handler THttpHandler
|
|
}
|
|
|
|
THttpRouter struct {
|
|
Routes []THttpRoute
|
|
NotFound THttpHandler
|
|
}
|
|
|
|
THttpRequestContext struct {
|
|
Request *http.Request
|
|
Writer http.ResponseWriter
|
|
Prefix string
|
|
Params []string
|
|
IPAddress string
|
|
SessionID []byte
|
|
AccountID int
|
|
}
|
|
)
|
|
|
|
var (
|
|
// HTTP/HTTPS Config
|
|
g_HttpPort int = 80
|
|
g_HttpsPort int = 443
|
|
g_HttpsCertFile string = ""
|
|
g_HttpsKeyFile string = ""
|
|
|
|
// SMTP Config
|
|
g_SmtpHost string = "smtp.domain.com"
|
|
g_SmtpPort int = 587
|
|
g_SmtpUser string = "username"
|
|
g_SmtpPassword string = ""
|
|
g_SmtpSender string = "support@domain.com"
|
|
|
|
// Query Manager Config
|
|
g_QueryManagerHost string = "localhost"
|
|
g_QueryManagerPort int = 7174
|
|
g_QueryManagerPassword string = ""
|
|
|
|
// Query Manager Cache Config
|
|
g_MaxCachedAccounts = 4096
|
|
g_MaxCachedCharacters = 4096
|
|
g_CharacterRefreshInterval = 15 * time.Minute
|
|
g_WorldsRefreshInterval = 15 * time.Minute
|
|
g_OnlineCharactersRefreshInterval = 15 * time.Minute
|
|
g_KillStatisticsRefreshInterval = 30 * time.Minute
|
|
|
|
// Loggers
|
|
g_Log = log.New(os.Stderr, "INFO ", log.Ldate|log.Ltime|log.Lmsgprefix)
|
|
g_LogWarn = log.New(os.Stderr, "WARN ", log.Ldate|log.Ltime|log.Lshortfile|log.Lmsgprefix)
|
|
g_LogErr = log.New(os.Stderr, "ERR ", log.Ldate|log.Ltime|log.Lshortfile|log.Lmsgprefix)
|
|
)
|
|
|
|
func WebKVCallback(Key string, Value string) {
|
|
if strings.EqualFold(Key, "HttpPort") {
|
|
g_HttpPort = ParseInteger(Value)
|
|
} else if strings.EqualFold(Key, "HttpsPort") {
|
|
g_HttpsPort = ParseInteger(Value)
|
|
} else if strings.EqualFold(Key, "HttpsCertFile") {
|
|
g_HttpsCertFile = ParseString(Value)
|
|
} else if strings.EqualFold(Key, "HttpsKeyFile") {
|
|
g_HttpsKeyFile = ParseString(Value)
|
|
} else if strings.EqualFold(Key, "SmtpHost") {
|
|
g_SmtpHost = ParseString(Value)
|
|
} else if strings.EqualFold(Key, "SmtpPort") {
|
|
g_SmtpPort = ParseInteger(Value)
|
|
} else if strings.EqualFold(Key, "SmtpUser") {
|
|
g_SmtpUser = ParseString(Value)
|
|
} else if strings.EqualFold(Key, "SmtpPassword") {
|
|
g_SmtpPassword = ParseString(Value)
|
|
} else if strings.EqualFold(Key, "SmtpSender") {
|
|
g_SmtpSender = ParseString(Value)
|
|
} else if strings.EqualFold(Key, "QueryManagerHost") {
|
|
g_QueryManagerHost = ParseString(Value)
|
|
} else if strings.EqualFold(Key, "QueryManagerPort") {
|
|
g_QueryManagerPort = ParseInteger(Value)
|
|
} else if strings.EqualFold(Key, "QueryManagerPassword") {
|
|
g_QueryManagerPassword = ParseString(Value)
|
|
} else if strings.EqualFold(Key, "CharacterRefreshInterval") {
|
|
g_CharacterRefreshInterval = ParseDuration(Value)
|
|
} else if strings.EqualFold(Key, "WorldsRefreshInterval") {
|
|
g_WorldsRefreshInterval = ParseDuration(Value)
|
|
} else if strings.EqualFold(Key, "OnlineCharactersRefreshInterval") {
|
|
g_OnlineCharactersRefreshInterval = ParseDuration(Value)
|
|
} else if strings.EqualFold(Key, "KillStatisticsRefreshInterval") {
|
|
g_KillStatisticsRefreshInterval = ParseDuration(Value)
|
|
} else if strings.EqualFold(Key, "MaxCachedAccounts") {
|
|
g_MaxCachedAccounts = ParseInteger(Value)
|
|
} else if strings.EqualFold(Key, "MaxCachedCharacters") {
|
|
g_MaxCachedCharacters = ParseInteger(Value)
|
|
} else {
|
|
g_LogWarn.Printf("Unknown config \"%v\"", Key)
|
|
}
|
|
}
|
|
|
|
func (Route *THttpRoute) LessThan(Method string, Prefix string, AllowParams bool) bool {
|
|
if Route.Method != Method {
|
|
return Route.Method < Method
|
|
} else if Route.Prefix != Prefix {
|
|
return Route.Prefix < Prefix
|
|
} else {
|
|
// NOTE(fusion): Use `false < true` convention.
|
|
return !Route.AllowParams && AllowParams
|
|
}
|
|
}
|
|
|
|
func (Router *THttpRouter) Add(Method string, Prefix string, Handler THttpHandler) {
|
|
AllowParams := false
|
|
if Prefix != "/" && Prefix[len(Prefix)-1] == '/' {
|
|
AllowParams = true
|
|
Prefix = Prefix[:len(Prefix)-1]
|
|
}
|
|
|
|
Index := 0
|
|
for ; Index < len(Router.Routes); Index += 1 {
|
|
if !Router.Routes[Index].LessThan(Method, Prefix, AllowParams) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if Index < len(Router.Routes) &&
|
|
Router.Routes[Index].Method == Method &&
|
|
Router.Routes[Index].Prefix == Prefix &&
|
|
Router.Routes[Index].AllowParams == AllowParams {
|
|
if AllowParams {
|
|
g_LogErr.Printf("Discarding duplicate route \"%v %v[/params]\"",
|
|
Method, Prefix)
|
|
} else {
|
|
g_LogErr.Printf("Discarding duplicate route \"%v %v\"",
|
|
Method, Prefix)
|
|
}
|
|
return
|
|
}
|
|
|
|
Router.Routes = slices.Insert(Router.Routes, Index,
|
|
THttpRoute{
|
|
Method: Method,
|
|
Prefix: Prefix,
|
|
AllowParams: AllowParams,
|
|
Handler: Handler,
|
|
})
|
|
}
|
|
|
|
func GetRequestIPAddress(Request *http.Request) string {
|
|
// NOTE(fusion): `Request.RemoteAddr` should to be in the exact format
|
|
// expected by `net.SplitHostPort` so I expect this to NEVER fail.
|
|
IPAddress, _, Err := net.SplitHostPort(Request.RemoteAddr)
|
|
if Err != nil {
|
|
g_LogErr.Printf("net.SplitHostPort(\"%v\") failed: %v", Request.RemoteAddr, Err)
|
|
return ""
|
|
}
|
|
|
|
return IPAddress
|
|
}
|
|
|
|
func (Router *THttpRouter) ServeHTTP(Writer http.ResponseWriter, Request *http.Request) {
|
|
Path := Request.URL.Path
|
|
if Path == "" {
|
|
Path = "/"
|
|
}
|
|
|
|
IPAddress := GetRequestIPAddress(Request)
|
|
if IPAddress == "" {
|
|
http.Error(Writer, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
SessionID := GetRequestSessionID(Request)
|
|
Context := THttpRequestContext{
|
|
Request: Request,
|
|
Writer: Writer,
|
|
Prefix: Path,
|
|
Params: nil,
|
|
IPAddress: IPAddress,
|
|
SessionID: SessionID,
|
|
AccountID: SessionLookup(SessionID, IPAddress),
|
|
}
|
|
|
|
for Index := len(Router.Routes) - 1; Index >= 0; Index -= 1 {
|
|
Route := &Router.Routes[Index]
|
|
if Route.Method != "" && Route.Method != Request.Method {
|
|
continue
|
|
}
|
|
|
|
Suffix, Found := strings.CutPrefix(Path, Route.Prefix)
|
|
if Found && (Suffix == "" || Suffix[0] == '/') {
|
|
Params := SplitDiscardEmpty(Suffix, "/")
|
|
if Route.AllowParams || len(Params) == 0 {
|
|
Context.Prefix = Route.Prefix
|
|
Context.Params = Params
|
|
Route.Handler(&Context)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
Router.NotFound(&Context)
|
|
}
|
|
|
|
func Redirect(Context *THttpRequestContext, Path string) {
|
|
Context.Writer.Header().Set("Location", Path)
|
|
Context.Writer.WriteHeader(http.StatusTemporaryRedirect)
|
|
}
|
|
|
|
func RequestError(Context *THttpRequestContext, Status int) {
|
|
g_LogErr.Printf("Failed to serve request \"%v %v\" to \"%v\": (%v) %v",
|
|
Context.Request.Method, Context.Request.URL.Path, Context.Request.RemoteAddr,
|
|
Status, http.StatusText(Status))
|
|
RenderRequestError(Context, Status)
|
|
}
|
|
|
|
func BadRequest(Context *THttpRequestContext) {
|
|
RequestError(Context, http.StatusBadRequest)
|
|
}
|
|
|
|
func Forbidden(Context *THttpRequestContext) {
|
|
RequestError(Context, http.StatusForbidden)
|
|
}
|
|
|
|
func NotFound(Context *THttpRequestContext) {
|
|
RequestError(Context, http.StatusNotFound)
|
|
}
|
|
|
|
func InternalError(Context *THttpRequestContext) {
|
|
RequestError(Context, http.StatusInternalServerError)
|
|
}
|
|
|
|
func ResourceError(Context *THttpRequestContext, Status int) {
|
|
// IMPORTANT(fusion): This is used for resource errors in which case we
|
|
// don't want to render any HTML to avoid pointless traffic. `http.Error`
|
|
// should send a minimal response with the appropriate status code.
|
|
g_LogErr.Printf("Failed to fetch resource \"%v %v\" to \"%v\": (%v) %v",
|
|
Context.Request.Method, Context.Request.URL.Path, Context.Request.RemoteAddr,
|
|
Status, http.StatusText(Status))
|
|
http.Error(Context.Writer, "", Status)
|
|
}
|
|
|
|
func HandleResource(Context *THttpRequestContext) {
|
|
if len(Context.Params) == 0 {
|
|
ResourceError(Context, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
FileName := path.Join(Context.Params...)
|
|
File, Err := os.OpenInRoot("./res", FileName)
|
|
if Err != nil {
|
|
g_LogErr.Printf("Failed to open file (%v): %v", FileName, Err)
|
|
ResourceError(Context, http.StatusNotFound)
|
|
return
|
|
}
|
|
defer File.Close()
|
|
|
|
Stat, Err := File.Stat()
|
|
if Err != nil {
|
|
g_LogErr.Printf("Failed to retrieve file description (%v): %v", FileName, Err)
|
|
ResourceError(Context, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// NOTE(fusion): File headers.
|
|
switch path.Ext(FileName) {
|
|
case ".css":
|
|
Context.Writer.Header().Set("Content-Type", "text/css")
|
|
case ".jpg", ".jpeg":
|
|
Context.Writer.Header().Set("Content-Type", "image/jpeg")
|
|
case ".js":
|
|
Context.Writer.Header().Set("Content-Type", "text/javascript")
|
|
case ".png":
|
|
Context.Writer.Header().Set("Content-Type", "image/png")
|
|
default:
|
|
Context.Writer.Header().Set("Content-Disposition",
|
|
fmt.Sprintf("attachment; filename=\"%v\"", FileName))
|
|
Context.Writer.Header().Set("Content-Type", "application/octet-stream")
|
|
}
|
|
Context.Writer.Header().Set("Content-Length", strconv.FormatInt(Stat.Size(), 10))
|
|
Context.Writer.Header().Set("Date", time.Now().UTC().Format(http.TimeFormat))
|
|
Context.Writer.Header().Set("Last-Modified", Stat.ModTime().UTC().Format(http.TimeFormat))
|
|
|
|
// NOTE(fusion): File contents.
|
|
TotalRead := 0
|
|
TotalWritten := 0
|
|
for {
|
|
var Buffer [1024 * 1024]byte
|
|
BytesRead, Err := File.Read(Buffer[:])
|
|
if Err != nil && Err != io.EOF {
|
|
g_LogErr.Printf("Failed to read resource (%v:%v): %v", FileName, TotalRead, Err)
|
|
return
|
|
}
|
|
|
|
if BytesRead == 0 {
|
|
return
|
|
}
|
|
|
|
BytesWritten, Err := Context.Writer.Write(Buffer[:BytesRead])
|
|
if Err != nil || BytesWritten != BytesRead {
|
|
g_LogErr.Printf("Failed to write resource (%v:%v): %v", FileName, TotalWritten, Err)
|
|
return
|
|
}
|
|
|
|
TotalRead += BytesRead
|
|
TotalWritten += BytesWritten
|
|
}
|
|
}
|
|
|
|
func HandleFavicon(Context *THttpRequestContext) {
|
|
if len(Context.Params) != 0 {
|
|
ResourceError(Context, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
Context.Params = []string{"favicon.ico"}
|
|
HandleResource(Context)
|
|
}
|
|
|
|
func HandleIndex(Context *THttpRequestContext) {
|
|
Redirect(Context, "/account")
|
|
}
|
|
|
|
func HandleAccount(Context *THttpRequestContext) {
|
|
if Context.AccountID > 0 {
|
|
RenderAccountSummary(Context)
|
|
return
|
|
}
|
|
|
|
switch Context.Request.Method {
|
|
case http.MethodGet:
|
|
RenderAccountLogin(Context)
|
|
case http.MethodPost:
|
|
Account := Context.Request.FormValue("account")
|
|
Password := Context.Request.FormValue("password")
|
|
|
|
// TODO(fusion): Other input checks?
|
|
if Account == "" || Password == "" {
|
|
RenderMessage(Context, "Login Error", "Account or password is not correct.")
|
|
return
|
|
}
|
|
|
|
AccountID, Err := strconv.Atoi(Account)
|
|
if Err != nil {
|
|
g_LogErr.Printf("Failed to parse account id: %d", Err)
|
|
RenderMessage(Context, "Login Error", "Account or password is not correct.")
|
|
return
|
|
}
|
|
|
|
Result := CheckAccountPassword(AccountID, Password, Context.IPAddress)
|
|
switch Result {
|
|
case 0:
|
|
// NOTE(fusion): Invalidate account's cached data just in case.
|
|
InvalidateAccountCachedData(AccountID)
|
|
SessionStart(Context, AccountID)
|
|
RenderAccountSummary(Context)
|
|
case 1, 2:
|
|
RenderMessage(Context, "Login Error", "Account or password is not correct.")
|
|
case 3:
|
|
RenderMessage(Context, "Login Error", "Account disabled for five minutes.")
|
|
case 4:
|
|
RenderMessage(Context, "Login Error", "IP address blocked for 30 minutes.")
|
|
case 5:
|
|
RenderMessage(Context, "Login Error", "Your account is banished.")
|
|
case 6:
|
|
RenderMessage(Context, "Login Error", "Your IP address is banished.")
|
|
default:
|
|
RenderMessage(Context, "Login Error", "Internal error.")
|
|
}
|
|
default:
|
|
NotFound(Context)
|
|
}
|
|
}
|
|
|
|
func HandleAccountLogout(Context *THttpRequestContext) {
|
|
SessionEnd(Context)
|
|
Redirect(Context, "/account")
|
|
}
|
|
|
|
func HandleAccountCreate(Context *THttpRequestContext) {
|
|
if Context.AccountID > 0 {
|
|
Redirect(Context, "/account")
|
|
return
|
|
}
|
|
|
|
switch Context.Request.Method {
|
|
case http.MethodGet:
|
|
RenderAccountCreate(Context)
|
|
case http.MethodPost:
|
|
Account := Context.Request.FormValue("account")
|
|
Email := Context.Request.FormValue("email")
|
|
Password := Context.Request.FormValue("password")
|
|
|
|
if Account == "" || Email == "" || Password == "" {
|
|
RenderMessage(Context, "Create Account Error", "All inputs are REQUIRED.")
|
|
return
|
|
}
|
|
|
|
AccountID, Err := strconv.Atoi(Account)
|
|
if Err != nil {
|
|
g_LogErr.Printf("Failed to parse account id: %d", Err)
|
|
RenderMessage(Context, "Create Account Error", "Invalid account number.")
|
|
return
|
|
}
|
|
|
|
if Email != Context.Request.FormValue("email_confirm") {
|
|
RenderMessage(Context, "Create Account Error", "Emails don't match.")
|
|
return
|
|
}
|
|
|
|
if Password != Context.Request.FormValue("password_confirm") {
|
|
RenderMessage(Context, "Create Account Error", "Passwords don't match.")
|
|
return
|
|
}
|
|
|
|
if AccountID < 100000 || AccountID > 999999999 {
|
|
RenderMessage(Context, "Create Account Error", "Account number must contain 6-9 digits.")
|
|
return
|
|
}
|
|
|
|
// TODO(fusion): Proper email and password checking.
|
|
if len(Password) < 8 {
|
|
RenderMessage(Context, "Create Account Error", "Password must contain at least 8 characters.")
|
|
return
|
|
}
|
|
|
|
Result := CreateAccount(AccountID, Email, Password)
|
|
switch Result {
|
|
case 0:
|
|
RenderMessage(Context, "Account Created",
|
|
"Your account has been created. Head back to the login page to access it.")
|
|
case 1:
|
|
RenderMessage(Context, "Create Account Error", "An account with that number already exists.")
|
|
case 2:
|
|
RenderMessage(Context, "Create Account Error", "An account with that email already exists.")
|
|
default:
|
|
RenderMessage(Context, "Create Account Error", "Internal error.")
|
|
}
|
|
default:
|
|
NotFound(Context)
|
|
}
|
|
}
|
|
|
|
func HandleAccountRecover(Context *THttpRequestContext) {
|
|
if Context.AccountID > 0 {
|
|
Redirect(Context, "/account")
|
|
return
|
|
}
|
|
|
|
switch Context.Request.Method {
|
|
case http.MethodGet:
|
|
RenderAccountRecover(Context)
|
|
default:
|
|
NotFound(Context)
|
|
}
|
|
}
|
|
|
|
func HandleCharacterCreate(Context *THttpRequestContext) {
|
|
if Context.AccountID <= 0 {
|
|
Redirect(Context, "/account")
|
|
return
|
|
}
|
|
|
|
switch Context.Request.Method {
|
|
case http.MethodGet:
|
|
RenderCharacterCreate(Context)
|
|
case http.MethodPost:
|
|
World := strings.TrimSpace(Context.Request.FormValue("world"))
|
|
if World == "" || GetWorld(World) == nil {
|
|
RenderMessage(Context, "Create Character Error", "Invalid world.")
|
|
return
|
|
}
|
|
|
|
// TODO(fusion): Proper name checking.
|
|
Name := strings.TrimSpace(Context.Request.FormValue("name"))
|
|
if len(Name) < 4 || len(Name) > 25 {
|
|
RenderMessage(Context, "Create Character Error", "Name must contain between 8 and 25 characters.")
|
|
return
|
|
}
|
|
|
|
Sex, Err := strconv.Atoi(Context.Request.FormValue("sex"))
|
|
if Err != nil || (Sex != 1 && Sex != 2) {
|
|
if Err != nil {
|
|
g_LogErr.Printf("Failed to parse character sex: %v", Err)
|
|
}
|
|
RenderMessage(Context, "Create Character Error", "Invalid sex.")
|
|
return
|
|
}
|
|
|
|
Result := CreateCharacter(World, Context.AccountID, Name, Sex)
|
|
switch Result {
|
|
case 0:
|
|
// NOTE(fusion): Invalidate account's cached data so the new character
|
|
// is displayed in the account's summary.
|
|
InvalidateAccountCachedData(Context.AccountID)
|
|
RenderMessage(Context, "Character Created",
|
|
fmt.Sprintf("And so, %v was brought into this world. May fortune"+
|
|
" favor your blade and guide your steps through the trials ahead.",
|
|
Name))
|
|
case 1:
|
|
RenderMessage(Context, "Create Character Error",
|
|
"Weirdly enough, the selected world doesn't exist. What have you been up to?")
|
|
case 2:
|
|
RenderMessage(Context, "Create Character Error",
|
|
"Weirdly enough, your account doesn't exist. What have you been up to?")
|
|
case 3:
|
|
RenderMessage(Context, "Create Character Error", "A character with that name already exists.")
|
|
default:
|
|
RenderMessage(Context, "Create Character Error", "Internal error.")
|
|
}
|
|
default:
|
|
NotFound(Context)
|
|
}
|
|
}
|
|
|
|
func HandleCharacterProfile(Context *THttpRequestContext) {
|
|
QueryValues := Context.Request.URL.Query()
|
|
CharacterName := QueryValues.Get("name")
|
|
if CharacterName == "" {
|
|
RenderCharacterProfile(Context, nil)
|
|
} else {
|
|
Result, Character := GetCharacterProfile(CharacterName)
|
|
switch Result {
|
|
case 0:
|
|
RenderCharacterProfile(Context, &Character)
|
|
case 1:
|
|
RenderMessage(Context, "Search Error", "A character with that name doesn't exist.")
|
|
default:
|
|
RenderMessage(Context, "Search Error", "Internal error.")
|
|
}
|
|
}
|
|
}
|
|
|
|
func HandleKillStatistics(Context *THttpRequestContext) {
|
|
QueryValues := Context.Request.URL.Query()
|
|
WorldName := QueryValues.Get("world")
|
|
if WorldName == "" || GetWorld(WorldName) == nil {
|
|
Redirect(Context, "/world")
|
|
} else {
|
|
RenderKillStatistics(Context, WorldName)
|
|
}
|
|
}
|
|
|
|
func HandleWorld(Context *THttpRequestContext) {
|
|
QueryValues := Context.Request.URL.Query()
|
|
WorldName := QueryValues.Get("name")
|
|
if WorldName == "" || GetWorld(WorldName) == nil {
|
|
RenderWorldList(Context)
|
|
} else {
|
|
RenderWorldInfo(Context, WorldName)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
g_Log.Print("Tibia Web Server v0.1")
|
|
if !ReadConfig("config.cfg", WebKVCallback) {
|
|
return
|
|
}
|
|
|
|
defer ExitQuery()
|
|
defer ExitMail()
|
|
defer ExitTemplates()
|
|
if !InitQuery() || !InitMail() || !InitTemplates() {
|
|
return
|
|
}
|
|
|
|
Router := THttpRouter{}
|
|
Router.Add("GET", "/res/", HandleResource)
|
|
Router.Add("GET", "/favicon.ico", HandleFavicon)
|
|
Router.Add("GET", "/", HandleIndex)
|
|
Router.Add("GET", "/index", HandleIndex)
|
|
Router.Add("GET", "/account", HandleAccount)
|
|
Router.Add("POST", "/account", HandleAccount)
|
|
Router.Add("GET", "/account/logout", HandleAccountLogout)
|
|
Router.Add("GET", "/account/create", HandleAccountCreate)
|
|
Router.Add("POST", "/account/create", HandleAccountCreate)
|
|
Router.Add("GET", "/account/recover", HandleAccountRecover)
|
|
Router.Add("POST", "/account/recover", HandleAccountRecover)
|
|
Router.Add("GET", "/character/create", HandleCharacterCreate)
|
|
Router.Add("POST", "/character/create", HandleCharacterCreate)
|
|
Router.Add("GET", "/character", HandleCharacterProfile)
|
|
Router.Add("GET", "/killstatistics", HandleKillStatistics)
|
|
Router.Add("GET", "/world", HandleWorld)
|
|
Router.NotFound = NotFound
|
|
|
|
// NOTE(fusion): Force the server to run on IPv4 because that is the only
|
|
// format the query manager currently handles. Trying to use IPv6 will cause
|
|
// queries to fail.
|
|
if FileExists(g_HttpsCertFile) && FileExists(g_HttpsKeyFile) {
|
|
Listener, Err := net.Listen("tcp4", JoinHostPort("", g_HttpsPort))
|
|
if Err != nil {
|
|
g_LogErr.Printf("Failed to listen to HTTPS port %v: %v", g_HttpsPort, Err)
|
|
return
|
|
}
|
|
|
|
g_Log.Printf("Running over HTTPS on port %v", g_HttpsPort)
|
|
g_Log.Print(http.ServeTLS(Listener, &Router, g_HttpsCertFile, g_HttpsKeyFile))
|
|
} else {
|
|
g_LogWarn.Print("The server is setup to run over HTTP which is NOT SECURE" +
|
|
" and prone to a man-in-the-middle or eavesdropping attack. This setup" +
|
|
" may only be used for TESTING.")
|
|
|
|
Listener, Err := net.Listen("tcp4", JoinHostPort("", g_HttpPort))
|
|
if Err != nil {
|
|
g_LogErr.Printf("Failed to listen to HTTP port %v: %v", g_HttpPort, Err)
|
|
return
|
|
}
|
|
|
|
g_Log.Printf("Running over HTTP on port %v", g_HttpPort)
|
|
g_Log.Print(http.Serve(Listener, &Router))
|
|
}
|
|
}
|