diff --git a/README.md b/README.md index e861917..0a538e5 100644 --- a/README.md +++ b/README.md @@ -37,19 +37,21 @@ Persistent storage utilizes MySQL. You will need a MySQL server to point openfsd Use the following environment variables to configure the server: -| Variable Name | Default Value | Description | -|-----------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `FSD_ADDR` | 0.0.0.0:6809 | FSD listen address | -| `HTTP_ADDR` | 0.0.0.0:8080 | HTTP listen address | -| `TLS_CERT_FILE` | | TLS certificate file path (setting this enables HTTPS. otherwise, plaintext HTTP will be used.) | -| `TLS_KEY_FILE` | | TLS key file path | -| `IN_MEMORY_DB` | false | Enables an ephemeral in-memory database in place of a real MySQL server.
This should only be used for testing. | -| `MYSQL_USER` | | MySQL username | -| `MYSQL_PASS` | | MySQL password | -| `MYSQL_NET` | | MySQL network protocol e.g. `tcp` | -| `MYSQL_ADDR` | | MySQL network address e.g. `127.0.0.1:3306` | -| `MYSQL_DBNAME` | | MySQL database name | -| `MOTD` | openfsd | "Message of the Day." This text is sent as a chat message to each client upon successful login to FSD. | +| Variable Name | Default Value | Description | +|-----------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `FSD_ADDR` | 0.0.0.0:6809 | FSD listen address | +| `HTTP_ADDR` | 0.0.0.0:8080 | HTTP listen address | +| `DOMAIN_NAME` | | Server domain name. This is required to properly set status.txt content.
e.g. `myopenfsdserver.com`
Optionally use `HTTP_DOMAIN_NAME` and `FSD_DOMAIN_NAME` for more granularity if required. | +| `TLS_ENABLED` | false | Whether to **flag** that TLS is enabled somewhere between openfsd and the client, so the status.txt API will format properly. This will **not** enable TLS for the internal HTTP server. Use TLS_CERT_FILE and TLS_KEY_FILE for that. | +| `TLS_CERT_FILE` | | TLS certificate file path (setting this enables HTTPS. otherwise, plaintext HTTP will be used.) | +| `TLS_KEY_FILE` | | TLS key file path | +| `IN_MEMORY_DB` | false | Enables an ephemeral in-memory database in place of a real MySQL server.
This should only be used for testing. | +| `MYSQL_USER` | | MySQL username | +| `MYSQL_PASS` | | MySQL password | +| `MYSQL_NET` | | MySQL network protocol e.g. `tcp` | +| `MYSQL_ADDR` | | MySQL network address e.g. `127.0.0.1:3306` | +| `MYSQL_DBNAME` | | MySQL database name | +| `MOTD` | openfsd | "Message of the Day." This text is sent as a chat message to each client upon successful login to FSD. | For 99.9% of use cases, it is also recommended to set: ``` @@ -97,6 +99,9 @@ Administrators and supervisors can create/mutate user records via the administra - `/api/v1/users` (See [documentation](https://github.com/renorris/openfsd/tree/main/web)) - `/api/v1/data/openfsd-data.json` VATSIM-esque [data feed](https://github.com/renorris/openfsd/blob/main/web/DATAFEED.md) +- `/api/v1/data/status.txt` VATSIM-esque [status.txt](https://status.vatsim.net) +- `/api/v1/data/servers.txt` VATSIM-esque [servers.txt](https://data.vatsim.net/vatsim-servers.txt) +- `/api/v1/data/servers.json` VATSIM-esque [servers.json](https://data.vatsim.net/v3/vatsim-servers.json) - `/login ... etc` front-end interface ## Connecting diff --git a/bootstrap/service/http.go b/bootstrap/service/http.go index d4ecbee..a018d43 100644 --- a/bootstrap/service/http.go +++ b/bootstrap/service/http.go @@ -51,6 +51,11 @@ func (s *HTTPService) boot(ctx context.Context, listener net.Listener) (err erro // data feed mux.HandleFunc("GET /api/v1/data/openfsd-data.json", web.DataFeedHandler) + // status.txt, servers.txt, servers.json + mux.HandleFunc("GET /api/v1/data/status.txt", web.StatusTxtHandler) + mux.HandleFunc("GET /api/v1/data/servers.txt", web.ServerListTxtHandler) + mux.HandleFunc("GET /api/v1/data/servers.json", web.ServerListJsonHandler) + // favicon mux.HandleFunc("/favicon.ico", web.FaviconHandler) diff --git a/servercontext/servercontext.go b/servercontext/servercontext.go index 70d25b0..f28f992 100644 --- a/servercontext/servercontext.go +++ b/servercontext/servercontext.go @@ -54,17 +54,21 @@ func DataFeed() *datafeed.DataFeed { } type ServerConfig struct { - FSDListenAddress string `env:"FSD_ADDR, default=0.0.0.0:6809"` // FSD network frontend/port - HTTPListenAddress string `env:"HTTP_ADDR, default=0.0.0.0:8080"` // HTTP network frontend/port - TLSCertFile string `env:"TLS_CERT_FILE"` // TLS certificate file path - TLSKeyFile string `env:"TLS_KEY_FILE"` // TLS key file path - MySQLUser string `env:"MYSQL_USER"` // MySQL username - MySQLPass string `env:"MYSQL_PASS"` // MySQL password - MySQLNet string `env:"MYSQL_NET"` // MySQL network protocol e.g. tcp, unix, etc - MySQLAddr string `env:"MYSQL_ADDR"` // MySQL network address e.g. 127.0.0.1:3306 - MySQLDBName string `env:"MYSQL_DBNAME"` // MySQL database name - InMemoryDB bool `env:"IN_MEMORY_DB, default=false"` // Whether to use an ephemeral in-memory DB instead of a real MySQL server - MOTD string `env:"MOTD, default=openfsd"` // Server "Message of the Day" + FSDListenAddress string `env:"FSD_ADDR, default=0.0.0.0:6809"` // FSD network frontend/port + HTTPListenAddress string `env:"HTTP_ADDR, default=0.0.0.0:8080"` // HTTP network frontend/port + TLSEnabled bool `env:"TLS_ENABLED"` // Whether to **flag** that TLS is enabled somewhere between openfsd and the client + TLSCertFile string `env:"TLS_CERT_FILE"` // TLS certificate file path + TLSKeyFile string `env:"TLS_KEY_FILE"` // TLS key file path + DomainName string `env:"DOMAIN_NAME, default=INCORRECT_DOMAIN_NAME_CONFIGURATION"` // Server domain name e.g. myserver.com + HTTPDomainName string `env:"HTTP_DOMAIN_NAME"` // HTTP domain name e.g. web.myserver.com (overrides DOMAIN_NAME for HTTP services if set) + FSDDomainName string `env:"FSD_DOMAIN_NAME"` // FSD domain name e.g. fsd.myserver.com (overrides DOMAIN_NAME for FSD services if set) + MySQLUser string `env:"MYSQL_USER"` // MySQL username + MySQLPass string `env:"MYSQL_PASS"` // MySQL password + MySQLNet string `env:"MYSQL_NET"` // MySQL network protocol e.g. tcp, unix, etc + MySQLAddr string `env:"MYSQL_ADDR"` // MySQL network address e.g. 127.0.0.1:3306 + MySQLDBName string `env:"MYSQL_DBNAME"` // MySQL database name + InMemoryDB bool `env:"IN_MEMORY_DB, default=false"` // Whether to use an ephemeral in-memory DB instead of a real MySQL server + MOTD string `env:"MOTD, default=openfsd"` // Server "Message of the Day" } type ServerContext struct { @@ -101,6 +105,11 @@ func New() *ServerContext { server.config.MySQLPass = "" } + // Ensure TLSEnabled is set if TLS for the internal HTTP server is enabled + if server.config.TLSCertFile != "" { + server.config.TLSEnabled = true + } + // Load the JWT private key server.jwtKey = loadOrCreateJWTKey(privateKeyFile) diff --git a/web/serverlist_handler.go b/web/serverlist_handler.go new file mode 100644 index 0000000..b59488c --- /dev/null +++ b/web/serverlist_handler.go @@ -0,0 +1,128 @@ +package web + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/renorris/openfsd/servercontext" + "io" + "net/http" + "strings" +) + +// TXT + +const serverlistTextFormat = `!GENERAL: +VERSION = 8 +RELOAD = 2 +UPDATE = 20220401021210 +ATIS ALLOW MIN = 5 +CONNECTED CLIENTS = 1 +; +; +!SERVERS: +{SERVERS_LIST} +; +; END` + +var formattedServerListTxt string + +func formatServerListTxt() string { + + var domainName string + if servercontext.Config().FSDDomainName != "" { + domainName = servercontext.Config().FSDDomainName + } else { + domainName = servercontext.Config().DomainName + } + + serversList := fmt.Sprintf("OPENFSD:%s:Everywhere:OPENFSD:1:", domainName) + return strings.Replace(serverlistTextFormat, "{SERVERS_LIST}", serversList, -1) +} + +var formattedServerListTxtEtag string + +// JSON + +type serverListEntry struct { + Ident string `json:"ident"` + HostnameOrIP string `json:"hostname_or_ip"` + Location string `json:"location"` + Name string `json:"name"` + ClientsConnectionAllowed int `json:"clients_connection_allowed"` + ClientConnectionAllowed bool `json:"client_connections_allowed"` + IsSweatbox bool `json:"is_sweatbox"` +} + +var formattedServerListJson = "" + +func formatServerListJson() (str string, err error) { + var domainName string + if servercontext.Config().FSDDomainName != "" { + domainName = servercontext.Config().FSDDomainName + } else { + domainName = servercontext.Config().DomainName + } + + serverList := []serverListEntry{{ + Ident: "OPENFSD", + HostnameOrIP: domainName, + Location: "Everywhere", + Name: "OPENFSD", + ClientsConnectionAllowed: 1, + ClientConnectionAllowed: true, + IsSweatbox: false, + }} + + var serverListBytes []byte + if serverListBytes, err = json.Marshal(&serverList); err != nil { + return + } + + str = string(serverListBytes) + return +} + +var formattedServerListJSONEtag = "" + +// ServerListJsonHandler handles json server list calls +func ServerListJsonHandler(w http.ResponseWriter, r *http.Request) { + if formattedServerListJson == "" { + var err error + if formattedServerListJson, err = formatServerListJson(); err != nil { + http.Error(w, "internal server error: error marshalling server list JSON", http.StatusInternalServerError) + return + } + } + + if formattedServerListJSONEtag == "" { + formattedServerListJSONEtag = getEtag(formattedServerListJson) + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("ETag", formattedServerListJSONEtag) + w.Header().Set("Cache-Control", "max-age=60") + + w.WriteHeader(200) + + io.Copy(w, bytes.NewReader([]byte(formattedServerListJson))) +} + +// ServerListTxtHandler handles text/plain server list calls +func ServerListTxtHandler(w http.ResponseWriter, r *http.Request) { + if formattedServerListTxt == "" { + formattedServerListTxt = formatServerListTxt() + } + + if formattedServerListTxtEtag == "" { + formattedServerListTxtEtag = getEtag(formattedServerListTxt) + } + + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("ETag", formattedServerListTxtEtag) + w.Header().Set("Cache-Control", "max-age=60") + + w.WriteHeader(200) + + io.Copy(w, bytes.NewReader([]byte(formattedServerListTxt))) +} diff --git a/web/statustxt_handler.go b/web/statustxt_handler.go new file mode 100644 index 0000000..1eb7f01 --- /dev/null +++ b/web/statustxt_handler.go @@ -0,0 +1,61 @@ +package web + +import ( + "bytes" + "github.com/renorris/openfsd/servercontext" + "io" + "net/http" + "strings" +) + +const statusFormat = `; IMPORTANT NOTE: This file can change as data sources change. Please check at regular intervals. +120218:NOTCP +; +json3={OPENFSD_ADDRESS}/api/v1/data/openfsd-data.json +; +url1={OPENFSD_ADDRESS}/api/v1/data/servers.txt +; +servers.live={OPENFSD_ADDRESS}/api/v1/data/servers.txt +; +voice0=afv +; +; END` + +var formattedStatusTxt string + +func formatStatusTxt() string { + openfsdAddress := "" + if servercontext.Config().TLSEnabled { + openfsdAddress += "https://" + } else { + openfsdAddress += "http://" + } + + if servercontext.Config().HTTPDomainName != "" { + openfsdAddress += servercontext.Config().HTTPDomainName + } else { + openfsdAddress += servercontext.Config().DomainName + } + + return strings.Replace(statusFormat, "{OPENFSD_ADDRESS}", openfsdAddress, -1) +} + +var formattedStatusTxtEtag string + +func StatusTxtHandler(w http.ResponseWriter, r *http.Request) { + if formattedStatusTxt == "" { + formattedStatusTxt = formatStatusTxt() + } + + if formattedStatusTxtEtag == "" { + formattedStatusTxtEtag = getEtag(formattedStatusTxt) + } + + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("ETag", formattedStatusTxtEtag) + w.Header().Set("Cache-Control", "max-age=60") + + w.WriteHeader(200) + + io.Copy(w, bytes.NewReader([]byte(formattedStatusTxt))) +} diff --git a/web/util.go b/web/util.go index cc19a59..fc4fdc6 100644 --- a/web/util.go +++ b/web/util.go @@ -3,6 +3,7 @@ package web import ( "bytes" "crypto/rand" + "crypto/sha1" "encoding/hex" "encoding/json" "io" @@ -24,3 +25,9 @@ func generateRandomPassword() (string, error) { return hex.EncodeToString(randBytes), nil } + +func getEtag(str string) string { + sum := sha1.Sum([]byte(str)) + sumSlice := sum[:] + return hex.EncodeToString(sumSlice) +}