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)
+}