add data API calls

- /api/v1/data/status.txt
- /api/v1/data/servers.json
- /api/v1/data/servers.txt
This commit is contained in:
Reese Norris
2024-10-13 10:53:59 -07:00
parent 5b10de9df9
commit 95a30294f4
6 changed files with 239 additions and 24 deletions

View File

@@ -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.<br>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.<br>e.g. `myopenfsdserver.com`<br>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.<br>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

View File

@@ -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)

View File

@@ -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)

128
web/serverlist_handler.go Normal file
View File

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

61
web/statustxt_handler.go Normal file
View File

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

View File

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