diff --git a/Companion/exporter/collector_runner.go b/Companion/exporter/collector_runner.go index 048f403..b0454bb 100644 --- a/Companion/exporter/collector_runner.go +++ b/Companion/exporter/collector_runner.go @@ -44,7 +44,7 @@ func NewCollectorRunner(ctx context.Context, frmBaseUrl string, collectors ...Co func (c *CollectorRunner) updateSessionName() { details := SessionInfo{} - err := retrieveData(c.frmBaseUrl+"/getSessionInfo", &details) + err := retrieveData(c.frmBaseUrl, "/getSessionInfo", &details) if err != nil { log.Printf("error reading session name from FRM: %s\n", err) return diff --git a/Companion/exporter/common.go b/Companion/exporter/common.go index 3cf1e59..334fb34 100644 --- a/Companion/exporter/common.go +++ b/Companion/exporter/common.go @@ -1,16 +1,35 @@ package exporter import ( + "bytes" + "crypto/tls" "encoding/json" "fmt" - "github.com/coder/quartz" "log" "net/http" + "net/url" "regexp" "strings" "time" + + "github.com/coder/quartz" ) +// Struct for the JSON body of POST requests +type FrmApiRequest struct { + Function string `json:"function"` + Endpoint string `json:"endpoint"` +} + +// Reusable HTTP client for HTTPS w/ self-signed certs +// InsecureSkipVerify: true isn't technically "safe," but dedicated servers default to self-signed certs +var tlsClient = &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, +} + var timeRegex = regexp.MustCompile(`\d\d:\d\d:\d\d`) var Clock = quartz.NewReal() @@ -35,7 +54,7 @@ func parseBool(b bool) float64 { } } -func retrieveData(frmAddress string, details any) error { +func retrieveDataViaGET(frmAddress string, details any) error { resp, err := http.Get(frmAddress) if err != nil { @@ -53,3 +72,55 @@ func retrieveData(frmAddress string, details any) error { err = decoder.Decode(&details) return err } + +func retrieveDataViaPOST(frmApiUrl string, endpointName string, details any) error { + reqBody := FrmApiRequest{ + Function: "frm", + Endpoint: endpointName, + } + jsonData, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("error marshalling request body: %s\n", err.Error()) + } + + req, err := http.NewRequest("POST", frmApiUrl, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("error creating POST request: %s\n", err.Error()) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := tlsClient.Do(req) + if err != nil { + return fmt.Errorf("error fetching statistics from FRM: %s\n", err.Error()) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("non-200 returned when retrieving data: %d", resp.StatusCode) + } + + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&details) + return err +} + +func retrieveData(frmAddress string, endpoint string, details any) error { + u, err := url.Parse(frmAddress) + if err != nil { + return fmt.Errorf("invalid FRM address URL: %s", err) + } + + // Check if we're using the Dedicated Server API (1.1) + if strings.HasSuffix(u.Path, "/api/v1") { + // Dedicated server mode. + // frmAddress is "https://host:7777/api/v1" + // endpoint is "/getPower", so we strip the slash + endpointName := strings.TrimPrefix(endpoint, "/") + return retrieveDataViaPOST(frmAddress, endpointName, details) + } else { + // Web server mode. + // frmAddress is "http://host:8080" + // endpoint is "/getPower" + return retrieveDataViaGET(frmAddress+endpoint, details) + } +} diff --git a/Companion/exporter/drone_station_collector.go b/Companion/exporter/drone_station_collector.go index 918f4a6..ee776a4 100644 --- a/Companion/exporter/drone_station_collector.go +++ b/Companion/exporter/drone_station_collector.go @@ -67,7 +67,7 @@ func NewDroneStationCollector(endpoint string) *DroneStationCollector { func (c *DroneStationCollector) Collect(frmAddress string, sessionName string) { details := []DroneStationDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { c.metricsDropper.DropStaleMetricLabels() log.Printf("error reading drone station statistics from FRM: %s\n", err) diff --git a/Companion/exporter/extractor_collector.go b/Companion/exporter/extractor_collector.go index 7b1fc28..7a968c9 100644 --- a/Companion/exporter/extractor_collector.go +++ b/Companion/exporter/extractor_collector.go @@ -40,7 +40,7 @@ func NewExtractorCollector(endpoint string) *ExtractorCollector { func (c *ExtractorCollector) Collect(frmAddress string, sessionName string) { details := []ExtractorDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { log.Printf("error reading extractor statistics from FRM: %s\n", err) return diff --git a/Companion/exporter/factory_building_collector.go b/Companion/exporter/factory_building_collector.go index 517b9a7..1222dc9 100644 --- a/Companion/exporter/factory_building_collector.go +++ b/Companion/exporter/factory_building_collector.go @@ -24,7 +24,7 @@ func NewFactoryBuildingCollector(endpoint string) *FactoryBuildingCollector { func (c *FactoryBuildingCollector) Collect(frmAddress string, sessionName string) { details := []BuildingDetail{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { c.metricsDropper.DropStaleMetricLabels() log.Printf("error reading factory buildings from FRM: %s\n", err) diff --git a/Companion/exporter/factory_building_collector_test.go b/Companion/exporter/factory_building_collector_test.go index 81bed72..88d910f 100644 --- a/Companion/exporter/factory_building_collector_test.go +++ b/Companion/exporter/factory_building_collector_test.go @@ -270,7 +270,7 @@ var _ = Describe("FactoryBuildingCollector", func() { Recipe: "Power Shard", ManuSpeed: 100.0, CircuitGroupId: 0, - Somersloops: 4, + Somersloops: 4, PowerInfo: exporter.PowerInfo{ CircuitGroupId: 1, PowerConsumed: 23, diff --git a/Companion/exporter/fracking_collector.go b/Companion/exporter/fracking_collector.go index e10394c..247eb38 100644 --- a/Companion/exporter/fracking_collector.go +++ b/Companion/exporter/fracking_collector.go @@ -40,7 +40,7 @@ func NewFrackingCollector(endpoint string) *FrackingCollector { func (c *FrackingCollector) Collect(frmAddress string, sessionName string) { details := []FrackingDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { log.Printf("error reading fracking statistics from FRM: %s\n", err) return diff --git a/Companion/exporter/hypertube_collector.go b/Companion/exporter/hypertube_collector.go index ed9769b..6352c3d 100644 --- a/Companion/exporter/hypertube_collector.go +++ b/Companion/exporter/hypertube_collector.go @@ -40,7 +40,7 @@ func NewHypertubeCollector(endpoint string) *HypertubeCollector { func (c *HypertubeCollector) Collect(frmAddress string, sessionName string) { details := []HypertubeDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { log.Printf("error reading hypertube statistics from FRM: %s\n", err) return diff --git a/Companion/exporter/portal_collector.go b/Companion/exporter/portal_collector.go index edb01b5..34a6da6 100644 --- a/Companion/exporter/portal_collector.go +++ b/Companion/exporter/portal_collector.go @@ -40,7 +40,7 @@ func NewPortalCollector(endpoint string) *PortalCollector { func (c *PortalCollector) Collect(frmAddress string, sessionName string) { details := []PortalDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { log.Printf("error reading portal statistics from FRM: %s\n", err) return diff --git a/Companion/exporter/power_collector.go b/Companion/exporter/power_collector.go index 4cb4272..886ae6d 100644 --- a/Companion/exporter/power_collector.go +++ b/Companion/exporter/power_collector.go @@ -114,7 +114,7 @@ func NewPowerCollector(endpoint string) *PowerCollector { func (c *PowerCollector) Collect(frmAddress string, sessionName string) { details := []PowerDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { c.metricsDropper.DropStaleMetricLabels() log.Printf("error reading power statistics from FRM: %s\n", err) diff --git a/Companion/exporter/power_info.go b/Companion/exporter/power_info.go index 44287fd..cde1f8c 100644 --- a/Companion/exporter/power_info.go +++ b/Companion/exporter/power_info.go @@ -37,11 +37,11 @@ func powerMultiplier(clockspeed float64, sloops float64, slots float64) float64 func MaxParticleAcceleratorPower(recipe string) float64 { recipes := map[string]float64{ - "Dark Matter Crystal": 1500.0, - "Diamonds": 750.0, - "Ficsonium": 1500.0, - "Nuclear Pasta": 1500.0, - "Plutonium Pellet": 750.0, + "Dark Matter Crystal": 1500.0, + "Diamonds": 750.0, + "Ficsonium": 1500.0, + "Nuclear Pasta": 1500.0, + "Plutonium Pellet": 750.0, "Alternate: Cloudy Diamonds": 750.0, "Alternate: Dark Matter Crystallization": 1500.0, "Alternate: Dark Matter Trap": 1500.0, diff --git a/Companion/exporter/production_collector.go b/Companion/exporter/production_collector.go index e285c6e..2468cfa 100644 --- a/Companion/exporter/production_collector.go +++ b/Companion/exporter/production_collector.go @@ -37,7 +37,7 @@ func NewProductionCollector(endpoint string) *ProductionCollector { func (c *ProductionCollector) Collect(frmAddress string, sessionName string) { details := []ProductionDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { c.metricsDropper.DropStaleMetricLabels() log.Printf("error reading production statistics from FRM: %s\n", err) diff --git a/Companion/exporter/pump_collector.go b/Companion/exporter/pump_collector.go index 05bb13d..e2629f9 100644 --- a/Companion/exporter/pump_collector.go +++ b/Companion/exporter/pump_collector.go @@ -40,7 +40,7 @@ func NewPumpCollector(endpoint string) *PumpCollector { func (c *PumpCollector) Collect(frmAddress string, sessionName string) { details := []PumpDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { log.Printf("error reading pump statistics from FRM: %s\n", err) return diff --git a/Companion/exporter/resource_sink_collector.go b/Companion/exporter/resource_sink_collector.go index 7c991f7..a93ae32 100644 --- a/Companion/exporter/resource_sink_collector.go +++ b/Companion/exporter/resource_sink_collector.go @@ -34,14 +34,14 @@ func NewResourceSinkCollector(buildingEndpoint, globalResourceEndpoint, globalEx func (c *ResourceSinkCollector) Collect(frmAddress string, sessionName string) { buildingDetails := []ResourceSinkDetails{} - err := retrieveData(frmAddress+c.buildingEndpoint, &buildingDetails) + err := retrieveData(frmAddress, c.buildingEndpoint, &buildingDetails) if err != nil { log.Printf("error reading resource sink details statistics from FRM: %s\n", err) return } globalResourceDetails := []GlobalSinkDetails{} - err = retrieveData(frmAddress+c.globalResourceEndpoint, &globalResourceDetails) + err = retrieveData(frmAddress, c.globalResourceEndpoint, &globalResourceDetails) if err != nil { log.Printf("error reading global resource sink statistics from FRM: %s\n", err) return @@ -55,7 +55,7 @@ func (c *ResourceSinkCollector) Collect(frmAddress string, sessionName string) { } globalExplorationDetails := []GlobalSinkDetails{} - err = retrieveData(frmAddress+c.globalExplorationEndpoint, &globalExplorationDetails) + err = retrieveData(frmAddress, c.globalExplorationEndpoint, &globalExplorationDetails) if err != nil { log.Printf("error reading global resource sink statistics from FRM: %s\n", err) return diff --git a/Companion/exporter/train_collector.go b/Companion/exporter/train_collector.go index bba76b3..130cdd3 100644 --- a/Companion/exporter/train_collector.go +++ b/Companion/exporter/train_collector.go @@ -127,7 +127,7 @@ func (d *TrainDetails) handleTimingUpdates(trackedTrains map[string]*TrainDetail func (c *TrainCollector) Collect(frmAddress string, sessionName string) { details := []TrainDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { c.metricsDropper.DropStaleMetricLabels() log.Printf("error reading train statistics from FRM: %s\n", err) diff --git a/Companion/exporter/train_station_collector.go b/Companion/exporter/train_station_collector.go index 2820a04..2b043ce 100644 --- a/Companion/exporter/train_station_collector.go +++ b/Companion/exporter/train_station_collector.go @@ -32,7 +32,7 @@ func NewTrainStationCollector(endpoint string) *TrainStationCollector { func (c *TrainStationCollector) Collect(frmAddress string, sessionName string) { details := []TrainStationDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { log.Printf("error reading train station statistics from FRM: %s\n", err) return diff --git a/Companion/exporter/vehicle_collector.go b/Companion/exporter/vehicle_collector.go index f7432c1..2c614b5 100644 --- a/Companion/exporter/vehicle_collector.go +++ b/Companion/exporter/vehicle_collector.go @@ -102,7 +102,7 @@ func NewVehicleCollector(endpoint string) *VehicleCollector { func (c *VehicleCollector) Collect(frmAddress string, sessionName string) { details := []VehicleDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { c.metricsDropper.DropStaleMetricLabels() log.Printf("error reading vehicle statistics from FRM: %s\n", err) diff --git a/Companion/exporter/vehicle_station_collector.go b/Companion/exporter/vehicle_station_collector.go index 702fdee..9cbfbad 100644 --- a/Companion/exporter/vehicle_station_collector.go +++ b/Companion/exporter/vehicle_station_collector.go @@ -23,7 +23,7 @@ func NewVehicleStationCollector(endpoint string) *VehicleStationCollector { func (c *VehicleStationCollector) Collect(frmAddress string, sessionName string) { details := []VehicleStationDetails{} - err := retrieveData(frmAddress+c.endpoint, &details) + err := retrieveData(frmAddress, c.endpoint, &details) if err != nil { log.Printf("error reading vehicle station statistics from FRM: %s\n", err) return diff --git a/README.md b/README.md index 1d54f5e..18b25d1 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,11 @@ The [Prometheus metrics server](https://prometheus.io/) allows you to [explore t `FRM_PORT`: The port of the Ficist Remote Monitoring server. EG: `8080`. -`FRM_HOSTS`: A comma separated list of Ficsit Remote Monitoring servers. If protocol is unspecified, it defaults to http. EG: `http://myserver1.frm.example:8080,myserver2.frm.example:8080,https://myserver3.frm.example:8081` +`FRM_HOSTS`: A comma separated list of Ficsit Remote Monitoring servers. This variable supports two connection modes: +* **Standard FRM Web Server:** Provide the base URL. If protocol is unspecified, it defaults to `http`. + * *Example:* `http://myserver1.frm.example:8080,http://myserver2.frm.example:8080` +* **Dedicated Server API:** Provide the full path to the server's built-in API. This will automatically switch to using the secure `POST` method. + * *Example:* `https://my-dedicated-server.example:7777/api/v1` `FRM_LOG_STDOUT`: If FRMC should print to stdout rather than a separate logfile. Useful for docker/containerization. default false. diff --git a/readme/readme.tpl.md b/readme/readme.tpl.md index c62fb63..b9782c7 100644 --- a/readme/readme.tpl.md +++ b/readme/readme.tpl.md @@ -50,7 +50,11 @@ The [Prometheus metrics server](https://prometheus.io/) allows you to [explore t `FRM_PORT`: The port of the Ficist Remote Monitoring server. EG: `8080`. -`FRM_HOSTS`: A comma separated list of Ficsit Remote Monitoring servers. If protocol is unspecified, it defaults to http. EG: `http://myserver1.frm.example:8080,myserver2.frm.example:8080,https://myserver3.frm.example:8081` +`FRM_HOSTS`: A comma separated list of Ficsit Remote Monitoring servers. This variable supports two connection modes: +* **Standard FRM Web Server:** Provide the base URL. If protocol is unspecified, it defaults to `http`. + * *Example:* `http://myserver1.frm.example:8080,http://myserver2.frm.example:8080` +* **Dedicated Server API:** Provide the full path to the server's built-in API. This will automatically switch to using the secure `POST` method. + * *Example:* `https://my-dedicated-server.example:7777/api/v1` `FRM_LOG_STDOUT`: If FRMC should print to stdout rather than a separate logfile. Useful for docker/containerization. default false.