diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 27d9dc832..320ae5326 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,7 +22,9 @@ jobs:
with:
esp_idf_version: v5.4.2
target: esp32s3
- command: GITHUB_ACTIONS="true" idf.py build
+ command: |
+ GITHUB_ACTIONS="true" idf.py add-dependency "espressif/mdns^1.8.2"
+ GITHUB_ACTIONS="true" idf.py build
path: '.'
- uses: actions/setup-python@v5
with:
diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml
index 5d3bfda9c..0213a2b1d 100644
--- a/.github/workflows/unittest.yml
+++ b/.github/workflows/unittest.yml
@@ -2,48 +2,35 @@ name: Unit Test
on: [push, pull_request]
permissions:
+ checks: write
+ pull-requests: write
contents: read
jobs:
- build-and-test:
+ build:
runs-on: ubuntu-latest
steps:
- - name: Checkout repo
- uses: actions/checkout@v4
- with:
- submodules: 'recursive'
-
- - name: esp-idf build
- uses: espressif/esp-idf-ci-action@v1
- with:
- esp_idf_version: v5.4.2
- target: esp32s3
- command: GITHUB_ACTIONS="true" idf.py build
- path: 'test-ci'
-
- - name: Run tests
- uses: bitaxeorg/esp32-qemu-test-action@main
- with:
- path: 'test-ci'
-
- - name: Upload event file
- uses: actions/upload-artifact@v4
- with:
- name: event-file
- path: ${{ github.event_path }}
-
- - name: Upload test results
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: test-results
- path: report.xml
-
- - name: Check for test failures
- if: always()
- run: |
- FAILURES=$(grep -oP 'failures="\K[0-9]+' report.xml || echo 0)
- if [ "$FAILURES" -gt 0 ]; then
- echo "::error ::Detected $FAILURES test failures."
- exit 1
- fi
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ submodules: 'recursive'
+ - name: esp-idf build
+ uses: espressif/esp-idf-ci-action@v1
+ with:
+ esp_idf_version: v5.4.2
+ target: esp32s3
+ command: |
+ GITHUB_ACTIONS="true" idf.py add-dependency "espressif/mdns^1.8.2"
+ GITHUB_ACTIONS="true" idf.py build
+ path: 'test-ci'
+ - name: Run tests and show result
+ uses: bitaxeorg/esp32-qemu-test-action@main
+ with:
+ path: 'test-ci'
+ - name: Inspect log
+ run: cat report.xml
+ - name: Publish Unit Test Results
+ uses: EnricoMi/publish-unit-test-result-action@v1
+ if: always()
+ with:
+ files: report.xml
diff --git a/components/connect/CMakeLists.txt b/components/connect/CMakeLists.txt
index 91e85c375..f0df62890 100644
--- a/components/connect/CMakeLists.txt
+++ b/components/connect/CMakeLists.txt
@@ -13,4 +13,5 @@ REQUIRES
"esp_wifi"
"esp_event"
"stratum"
+ "mdns"
)
diff --git a/components/connect/connect.c b/components/connect/connect.c
index 673bb4545..3b520be2f 100644
--- a/components/connect/connect.c
+++ b/components/connect/connect.c
@@ -11,6 +11,7 @@
#include "lwip/sys.h"
#include "nvs_flash.h"
#include "esp_wifi_types_generic.h"
+#include "mdns.h"
#include "connect.h"
#include "global_state.h"
@@ -61,6 +62,7 @@ static int clients_connected_to_ap = 0;
static const char *get_wifi_reason_string(int reason);
static void wifi_softap_on(void);
static void wifi_softap_off(void);
+static void mdns_init_hostname(void);
esp_err_t get_wifi_current_rssi(int8_t *rssi)
{
@@ -190,12 +192,12 @@ static void event_handler(void * arg, esp_event_base_t event_base, int32_t event
ESP_LOGI(TAG, "Retrying Wi-Fi connection...");
esp_wifi_connect();
}
-
+
if (event_id == WIFI_EVENT_AP_START) {
ESP_LOGI(TAG, "Configuration Access Point enabled");
GLOBAL_STATE->SYSTEM_MODULE.ap_enabled = true;
}
-
+
if (event_id == WIFI_EVENT_AP_STOP) {
ESP_LOGI(TAG, "Configuration Access Point disabled");
GLOBAL_STATE->SYSTEM_MODULE.ap_enabled = false;
@@ -204,7 +206,7 @@ static void event_handler(void * arg, esp_event_base_t event_base, int32_t event
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
clients_connected_to_ap += 1;
}
-
+
if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
clients_connected_to_ap -= 1;
}
@@ -222,6 +224,8 @@ static void event_handler(void * arg, esp_event_base_t event_base, int32_t event
ESP_LOGI(TAG, "Connected to SSID: %s", GLOBAL_STATE->SYSTEM_MODULE.ssid);
wifi_softap_off();
+
+ mdns_init_hostname();
}
}
@@ -230,8 +234,8 @@ esp_netif_t * wifi_init_softap(char * ap_ssid)
esp_netif_t * esp_netif_ap = esp_netif_create_default_wifi_ap();
uint8_t mac[6];
- esp_wifi_get_mac(ESP_IF_WIFI_AP, mac);
- // Format the last 4 bytes of the MAC address as a hexadecimal string
+ esp_wifi_get_mac(WIFI_IF_STA, mac);
+ // Format the last 4 bytes of the Station MAC address as a hexadecimal string
snprintf(ap_ssid, 32, "Bitaxe_%02X%02X", mac[4], mac[5]);
wifi_config_t wifi_ap_config;
@@ -474,3 +478,60 @@ static const char *get_wifi_reason_string(int reason) {
}
return "Unknown error";
}
+
+static void mdns_init_hostname(void) {
+ char * hostname = nvs_config_get_string(NVS_CONFIG_HOSTNAME, CONFIG_LWIP_LOCAL_HOSTNAME);
+
+ ESP_LOGI(TAG, "Starting mDNS service");
+ esp_err_t err = mdns_init();
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "mDNS initialization failed: %s", esp_err_to_name(err));
+ free(hostname);
+ return;
+ }
+
+ ESP_LOGI(TAG, "Setting mDNS hostname to: %s", hostname);
+ err = mdns_hostname_set(hostname);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "Failed to set mDNS hostname: %s", esp_err_to_name(err));
+ free(hostname);
+ return;
+ }
+
+ ESP_LOGI(TAG, "Setting mDNS instance name to: %s", hostname);
+ err = mdns_instance_name_set(hostname);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "Failed to set mDNS instance name: %s", esp_err_to_name(err));
+ free(hostname);
+ return;
+ }
+
+ ESP_LOGI(TAG, "Adding mDNS service: _http._tcp on port 80");
+ err = mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "Failed to add mDNS HTTP service: %s", esp_err_to_name(err));
+ free(hostname);
+ return;
+ }
+
+ // Add _bitaxe._tcp service with apiSecret as TXT record
+ char * api_secret = nvs_config_get_string(NVS_CONFIG_API_SECRET, "");
+
+ ESP_LOGI(TAG, "Adding mDNS service: _bitaxe._tcp on port 80");
+
+ mdns_txt_item_t bitaxe_txt_data[1];
+ bitaxe_txt_data[0].key = "apiSecret";
+ bitaxe_txt_data[0].value = api_secret;
+
+ err = mdns_service_add(NULL, "_bitaxe", "_tcp", 80, bitaxe_txt_data, 1);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "Failed to add mDNS _bitaxe._tcp service: %s", esp_err_to_name(err));
+ } else {
+ ESP_LOGI(TAG, "mDNS _bitaxe._tcp service added successfully");
+ }
+
+ free(api_secret);
+
+ ESP_LOGI(TAG, "mDNS service started successfully");
+ free(hostname);
+}
diff --git a/config-102.cvs b/config-102.cvs
index 069e0ff12..ba8d40e67 100644
--- a/config-102.cvs
+++ b/config-102.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-201.cvs b/config-201.cvs
index 459d839e0..f5fb951ce 100644
--- a/config-201.cvs
+++ b/config-201.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-202.cvs b/config-202.cvs
index e1003837a..b608bab14 100644
--- a/config-202.cvs
+++ b/config-202.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-203.cvs b/config-203.cvs
index 02ad19728..1bb2bdc59 100644
--- a/config-203.cvs
+++ b/config-203.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-204.cvs b/config-204.cvs
index 3599c707a..25e543df3 100644
--- a/config-204.cvs
+++ b/config-204.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-205.cvs b/config-205.cvs
index ffcc27609..3335d780d 100644
--- a/config-205.cvs
+++ b/config-205.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-207.cvs b/config-207.cvs
index 098cb7d97..1881f186c 100644
--- a/config-207.cvs
+++ b/config-207.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-401.cvs b/config-401.cvs
index 9f5d4bf69..b2009180d 100644
--- a/config-401.cvs
+++ b/config-401.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-402.cvs b/config-402.cvs
index 49a45cc9a..323a8d6f9 100644
--- a/config-402.cvs
+++ b/config-402.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-403.cvs b/config-403.cvs
index ccfac7388..7e4b88019 100644
--- a/config-403.cvs
+++ b/config-403.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-601.cvs b/config-601.cvs
index 23440ddeb..8aade30fb 100644
--- a/config-601.cvs
+++ b/config-601.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-602.cvs b/config-602.cvs
index 474797cb3..3121baa13 100644
--- a/config-602.cvs
+++ b/config-602.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config-800x.cvs b/config-800x.cvs
index ed5f9cc4f..d9a66b3ce 100644
--- a/config-800x.cvs
+++ b/config-800x.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,myssid
wifipass,data,string,password
stratumurl,data,string,public-pool.io
diff --git a/config-custom.cvs b/config-custom.cvs
index 13ef4d1a4..fc234f135 100644
--- a/config-custom.cvs
+++ b/config-custom.cvs
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,
wifipass,data,string,
stratumurl,data,string,public-pool.io
diff --git a/config.cvs.example b/config.cvs.example
index 258773205..c5df670eb 100644
--- a/config.cvs.example
+++ b/config.cvs.example
@@ -1,6 +1,7 @@
key,type,encoding,value
main,namespace,,
hostname,data,string,bitaxe
+apisecret,data,string,
wifissid,data,string,myssid
wifipass,data,string,mypass
stratumurl,data,string,public-pool.io
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index f6bb63c76..d3f6c32fc 100755
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -54,6 +54,7 @@ PRIV_REQUIRES
"esp_adc"
"esp_app_format"
"esp_event"
+ "esp_http_client"
"esp_http_server"
"esp_netif"
"esp_psram"
@@ -64,6 +65,7 @@ PRIV_REQUIRES
"spiffs"
"vfs"
"esp_driver_i2c"
+ "mdns"
EMBED_FILES "http_server/recovery_page.html"
)
diff --git a/main/http_server/axe-os/api/system/asic_settings.c b/main/http_server/axe-os/api/system/asic_settings.c
index 1a87aa0d3..161f5d1ad 100644
--- a/main/http_server/axe-os/api/system/asic_settings.c
+++ b/main/http_server/axe-os/api/system/asic_settings.c
@@ -9,7 +9,7 @@
static GlobalState *GLOBAL_STATE = NULL;
// Function declarations from http_server.c
-extern esp_err_t is_network_allowed(httpd_req_t *req);
+extern esp_err_t is_request_authorized(httpd_req_t *req);
extern esp_err_t set_cors_headers(httpd_req_t *req);
// Initialize the ASIC API with the global state
@@ -20,7 +20,7 @@ void asic_api_init(GlobalState *global_state) {
/* Handler for system asic endpoint */
esp_err_t GET_system_asic(httpd_req_t *req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
diff --git a/main/http_server/axe-os/src/app/components/edit/edit.component.ts b/main/http_server/axe-os/src/app/components/edit/edit.component.ts
index 7c88ac896..aae8bc979 100644
--- a/main/http_server/axe-os/src/app/components/edit/edit.component.ts
+++ b/main/http_server/axe-os/src/app/components/edit/edit.component.ts
@@ -31,6 +31,7 @@ export class EditComponent implements OnInit, OnDestroy, OnChanges {
public settingsUnlocked: boolean = false;
@Input() uri = '';
+ @Input() apiSecret?: string;
// Store frequency and voltage options from API
public defaultFrequency: number = 0;
@@ -99,7 +100,7 @@ export class EditComponent implements OnInit, OnDestroy, OnChanges {
private saveOverclockSetting(enabled: number) {
const deviceUri = this.uri || '';
- this.systemService.updateSystem(deviceUri, { overclockEnabled: enabled })
+ this.systemService.updateSystem(deviceUri, { overclockEnabled: enabled }, this.apiSecret)
.subscribe({
next: () => {
console.log(`Overclock setting saved: ${enabled === 1 ? 'enabled' : 'disabled'}`);
@@ -127,8 +128,8 @@ export class EditComponent implements OnInit, OnDestroy, OnChanges {
// Fetch both system info and ASIC settings in parallel
forkJoin({
- info: this.systemService.getInfo(deviceUri),
- asic: this.systemService.getAsicSettings(deviceUri)
+ info: this.systemService.getInfo(deviceUri, this.apiSecret),
+ asic: this.systemService.getAsicSettings(deviceUri, this.apiSecret)
})
.pipe(
this.loadingService.lockUIUntilComplete(),
@@ -221,7 +222,7 @@ export class EditComponent implements OnInit, OnDestroy, OnChanges {
}
const deviceUri = this.uri || '';
- this.systemService.updateSystem(deviceUri, form)
+ this.systemService.updateSystem(deviceUri, form, this.apiSecret)
.pipe(this.loadingService.lockUIUntilComplete())
.subscribe({
next: () => {
@@ -260,7 +261,7 @@ export class EditComponent implements OnInit, OnDestroy, OnChanges {
}
public restart() {
- this.systemService.restart(this.uri)
+ this.systemService.restart(this.uri, this.apiSecret)
.pipe(this.loadingService.lockUIUntilComplete())
.subscribe({
next: () => {
@@ -344,4 +345,5 @@ export class EditComponent implements OnInit, OnDestroy, OnChanges {
return !! Object.entries(this.form.controls)
.filter(([field, control]) => control.dirty && !this.noRestartFields.includes(field)).length
}
+
}
diff --git a/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.html b/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.html
index 0f34b7790..acb3eacaf 100644
--- a/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.html
+++ b/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.html
@@ -4,6 +4,10 @@
@@ -31,6 +35,23 @@
+
+
+
+
+
Optional 12-32 character string for API authentication
+
+
+
diff --git a/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.ts b/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.ts
index 8e08ba3e8..5cda5d2a7 100644
--- a/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.ts
+++ b/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.ts
@@ -26,6 +26,7 @@ export class NetworkEditComponent implements OnInit {
public form!: FormGroup;
public savedChanges: boolean = false;
public scanning: boolean = false;
+ public isInCaptivePortalMode: boolean = false;
@Input() uri = '';
@@ -43,15 +44,52 @@ export class NetworkEditComponent implements OnInit {
this.systemService.getInfo(this.uri)
.pipe(this.loadingService.lockUIUntilComplete())
.subscribe(info => {
+ // Check if we're in captive portal mode (no SSID configured)
+ this.isInCaptivePortalMode = !info.ssid || info.ssid.trim() === '';
+
+ // Generate suggested hostname from MAC address (bitaxe-xxxx pattern)
+ const suggestedHostname = this.generateSuggestedHostname(info.macAddr);
+ const hostname = this.isInCaptivePortalMode ? suggestedHostname : info.hostname;
+
this.form = this.fb.group({
- hostname: [info.hostname, [Validators.required]],
+ hostname: [hostname, [Validators.required]],
ssid: [info.ssid, [Validators.required]],
wifiPass: ['*****'],
+ apiSecret: [info.apiSecret || '', [Validators.minLength(12), Validators.maxLength(32)]],
});
this.formSubject.next(this.form);
});
}
+ private generateSuggestedHostname(macAddr: string): string {
+ if (!macAddr) return 'bitaxe';
+
+ // Extract last 4 characters (2 bytes) from MAC address
+ // MAC format is XX:XX:XX:XX:XX:XX, so get last 2 pairs
+ const macParts = macAddr.split(':');
+ if (macParts.length >= 6) {
+ const lastTwoBytes = macParts[4] + macParts[5];
+ return `bitaxe-${lastTwoBytes.toLowerCase()}`;
+ }
+
+ return 'bitaxe';
+ }
+
+ public generateApiSecret(): void {
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let result = '';
+ for (let i = 0; i < 24; i++) {
+ result += characters.charAt(Math.floor(Math.random() * characters.length));
+ }
+ this.form.patchValue({ apiSecret: result });
+ this.form.markAsDirty();
+ }
+
+ public getHostnameUrl(): string {
+ const hostname = this.form?.get('hostname')?.value || 'bitaxe';
+ return `http://${hostname}.local`;
+ }
+
public updateSystem() {
@@ -69,6 +107,11 @@ export class NetworkEditComponent implements OnInit {
form.ssid = form.ssid.trim();
}
+ // Handle API secret - remove if empty
+ if (form.apiSecret && form.apiSecret.trim() === '') {
+ form.apiSecret = '';
+ }
+
this.systemService.updateSystem(this.uri, form)
.pipe(this.loadingService.lockUIUntilComplete())
.subscribe({
diff --git a/main/http_server/axe-os/src/app/components/swarm/swarm.component.html b/main/http_server/axe-os/src/app/components/swarm/swarm.component.html
index 00f0721bb..36f8a25cc 100644
--- a/main/http_server/axe-os/src/app/components/swarm/swarm.component.html
+++ b/main/http_server/axe-os/src/app/components/swarm/swarm.component.html
@@ -1,19 +1,3 @@
-
@@ -21,6 +5,12 @@
Refresh List (30)
{{ isRefreshing ? 'Refreshing...' : 'Refresh List (' + refreshIntervalTime + ')' }}
+
+
@@ -34,6 +24,25 @@
+
+
{{axe.IP}}
+ tooltipPosition="top">{{axe.currentIP || axe.IP}}
{{axe.hostname}} |
{{axe.hashRate * 1000000000 | hashSuffix}} |
@@ -189,5 +198,5 @@
-
+
diff --git a/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts b/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts
index 630e5ddc3..3123d15c2 100644
--- a/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts
+++ b/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts
@@ -42,6 +42,8 @@ export class SwarmComponent implements OnInit, OnDestroy {
public sortField: string = '';
public sortDirection: 'asc' | 'desc' = 'asc';
+ public showManualAddition: boolean = false;
+
constructor(
private fb: FormBuilder,
private toastr: ToastrService,
@@ -50,7 +52,8 @@ export class SwarmComponent implements OnInit, OnDestroy {
) {
this.form = this.fb.group({
- manualAddIp: [null, [Validators.required, Validators.pattern('(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)')]]
+ manualAddIp: [null, [Validators.required, Validators.pattern('^(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)*[a-zA-Z0-9](?:[a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)$')]],
+ manualAddApiSecret: ['', [Validators.minLength(12), Validators.maxLength(32)]]
});
const storedRefreshTime = this.localStorageService.getNumber(SWARM_REFRESH_TIME) ?? 30;
@@ -97,59 +100,129 @@ export class SwarmComponent implements OnInit, OnDestroy {
this.form.reset();
}
- private ipToInt(ip: string): number {
- return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
- }
-
- private intToIp(int: number): string {
- return `${(int >>> 24) & 255}.${(int >>> 16) & 255}.${(int >>> 8) & 255}.${int & 255}`;
- }
-
- private calculateIpRange(ip: string, netmask: string): { start: number, end: number } {
- const ipInt = this.ipToInt(ip);
- const netmaskInt = this.ipToInt(netmask);
- const network = ipInt & netmaskInt;
- const broadcast = network | ~netmaskInt;
- return { start: network + 1, end: broadcast - 1 };
- }
scanNetwork() {
this.scanning = true;
- const { start, end } = this.calculateIpRange(window.location.hostname, '255.255.255.0');
- const ips = Array.from({ length: end - start + 1 }, (_, i) => this.intToIp(start + i));
- this.getAllDeviceInfo(ips, () => of(null)).subscribe({
- next: (result) => {
- // Filter out null items first
- const validResults = result.filter((item): item is SwarmDevice => item !== null);
- // Merge new results with existing swarm entries
- const existingIps = new Set(this.swarm.map(item => item.IP));
- const newItems = validResults.filter(item => !existingIps.has(item.IP));
- this.swarm = [...this.swarm, ...newItems];
- this.sortSwarm();
- this.localStorageService.setObject(SWARM_DATA, this.swarm);
- this.calculateTotals();
+ forkJoin({
+ currentDevice: this.httpClient.get
('/api/system/info').pipe(
+ mergeMap(info =>
+ this.httpClient.get('/api/system/asic').pipe(
+ map(asic => ({ ...info, ...asic })),
+ catchError(() => of(info))
+ )
+ )
+ ),
+ foundDevices: this.httpClient.get('/api/system/network/scan')
+ }).subscribe({
+ next: ({ currentDevice, foundDevices }) => {
+ if (!Array.isArray(foundDevices)) {
+ foundDevices = [];
+ }
+
+ // Check if current device should be added to swarm
+ const existingIps = new Set([...this.swarm.map(item => item.IP), ...this.swarm.map(item => item.currentIP)]);
+ const existingHostnames = new Set(this.swarm.map(item => item.hostname));
+
+ const currentDeviceExists = existingIps.has(currentDevice.currentIP) || (currentDevice.hostname && existingHostnames.has(currentDevice.hostname));
+
+ // If current device is not in swarm, add it to the list of devices to process
+ if (!currentDeviceExists && currentDevice.ASICModel) {
+ currentDevice.IP = window.location.host;
+ foundDevices.unshift(currentDevice);
+ }
+
+ // Filter out devices we already have - check both IP and currentIP
+ const newDevices = foundDevices.filter(device => {
+ // Check if device IP matches any existing IP or currentIP
+ const ipNotExists = !existingIps.has(device.IP);
+ const hostnameNotExists = !device.hostname || !existingHostnames.has(device.hostname);
+ return ipNotExists && hostnameNotExists;
+ });
+
+ if (newDevices.length === 0) {
+ this.toastr.info('No new devices found', 'Network Scan Complete');
+ this.scanning = false;
+ return;
+ }
+
+ // Fetch device info for each discovered device
+ const deviceInfoRequests = newDevices.map(device => {
+ const httpOptions = this.getHttpOptionsForDevice(device);
+
+ return forkJoin({
+ info: this.httpClient.get(`http://${device.IP}/api/system/info`, httpOptions),
+ asic: this.httpClient.get(`http://${device.IP}/api/system/asic`, httpOptions).pipe(catchError(() => of({})))
+ }).pipe(
+ map(({ info, asic }) => {
+ // Merge mDNS data with fetched info
+ return { ...device, ...info, ...asic };
+ }),
+ timeout(5000),
+ catchError(error => {
+ this.toastr.error(`Failed to get info from ${device.IP}`, 'Device Error');
+ return of(null);
+ })
+ );
+ });
+
+ forkJoin(deviceInfoRequests).subscribe({
+ next: (devices) => {
+ const validDevices = devices.filter(d => d !== null && d.ASICModel);
+
+ // Add new devices to swarm
+ this.swarm = [...this.swarm, ...validDevices];
+ this.sortSwarm();
+ this.localStorageService.setObject(SWARM_DATA, this.swarm);
+ this.calculateTotals();
+
+ this.toastr.success(`Found ${validDevices.length} new device(s)`, 'Network Scan Complete');
+ this.scanning = false;
+ },
+ error: (error) => {
+ this.toastr.error(`Failed to fetch device information: ${error.message || 'Unknown error'}`, 'Scan Error');
+ this.scanning = false;
+ }
+ });
},
- complete: () => {
+ error: (error) => {
+ this.toastr.error(`Scan failed: ${error.message || 'Unknown error'}`, 'Scan Error');
this.scanning = false;
}
});
}
- private getAllDeviceInfo(ips: string[], errorHandler: (error: any, ip: string) => Observable, fetchAsic: boolean = true) {
- return from(ips).pipe(
- mergeMap(IP => forkJoin({
- info: this.httpClient.get(`http://${IP}/api/system/info`),
- asic: fetchAsic ? this.httpClient.get(`http://${IP}/api/system/asic`).pipe(catchError(() => of({}))) : of({})
- }).pipe(
- map(({ info, asic }) => {
- const existingDevice = this.swarm.find(device => device.IP === IP);
- const result = { IP, ...(existingDevice ? existingDevice : {}), ...info, ...asic };
- return this.fallbackDeviceModel(result);
- }),
- timeout(5000),
- catchError(error => errorHandler(error, IP))
- ),
+
+ private getHttpOptionsForDevice(device: any): { headers: any } {
+ const headers: any = {};
+
+ // Check if device has an API secret stored
+ const apiSecret = device?.apiSecret;
+ if (apiSecret && apiSecret.trim() !== '') {
+ headers['X-Requested-With'] = 'XMLHttpRequest'; // NOTE: This should eventually *always* be sent.
+ headers['Authorization'] = `Bearer ${apiSecret}`;
+ }
+
+ return { headers };
+ }
+
+ private getAllDeviceInfo(errorHandler: (error: any, ip: string) => Observable, fetchAsic: boolean = true) {
+ return from(this.swarm).pipe(
+ mergeMap(device => {
+ const httpOptions = this.getHttpOptionsForDevice(device);
+
+ return forkJoin({
+ info: this.httpClient.get(`http://${device.IP}/api/system/info`, httpOptions),
+ asic: fetchAsic ? this.httpClient.get(`http://${device.IP}/api/system/asic`, httpOptions).pipe(catchError(() => of({}))) : of({})
+ }).pipe(
+ map(({ info, asic }) => {
+ const result = { ...device, ...info, ...asic };
+ return this.fallbackDeviceModel(result);
+ }),
+ timeout(5000),
+ catchError(error => errorHandler(error, device.IP))
+ );
+ },
128
),
toArray()
@@ -158,25 +231,47 @@ export class SwarmComponent implements OnInit, OnDestroy {
public add() {
const IP = this.form.value.manualAddIp;
+ const apiSecret = this.form.value.manualAddApiSecret;
// Check if IP already exists
- if (this.swarm.some(item => item.IP === IP)) {
+ if (this.swarm.some(item => item.IP === IP || item.currentIP === IP)) {
this.toastr.warning('This IP address already exists in the swarm', 'Duplicate Entry');
return;
}
+ // Create temporary device object for header generation
+ const httpOptions = this.getHttpOptionsForDevice({ IP, apiSecret });
+
forkJoin({
- info: this.httpClient.get(`http://${IP}/api/system/info`),
- asic: this.httpClient.get(`http://${IP}/api/system/asic`).pipe(catchError(() => of({})))
- }).subscribe(({ info, asic }) => {
- if (!info.ASICModel || !asic.ASICModel) {
- return;
- }
+ info: this.httpClient.get(`http://${IP}/api/system/info`, httpOptions),
+ asic: this.httpClient.get(`http://${IP}/api/system/asic`, httpOptions).pipe(catchError(() => of({})))
+ }).subscribe({
+ next: ({ info, asic }) => {
+ if (!info.ASICModel || !asic.ASICModel) {
+ return;
+ }
- this.swarm.push({ IP, ...asic, ...info });
- this.sortSwarm();
- this.localStorageService.setObject(SWARM_DATA, this.swarm);
- this.calculateTotals();
+ // Store the API secret with the device for future requests
+ const deviceData = { IP, ...asic, ...info };
+ if (apiSecret && apiSecret.trim() !== '') {
+ deviceData.apiSecret = apiSecret;
+ }
+
+ // Prevent an edge case where users already have a Bitaxe in the Swarm as an IP, but attempt to then add via other names
+ if (this.swarm.some(item => item.IP === info.currentIP || item.currentIP === info.currentIP)) {
+ this.toastr.warning('This IP address already exists in the swarm', 'Duplicate Entry');
+ return;
+ }
+
+ this.swarm.push(deviceData);
+ this.sortSwarm();
+ this.localStorageService.setObject(SWARM_DATA, this.swarm);
+ this.calculateTotals();
+ this.toastr.success(`Device at ${IP} added successfully`, 'Success');
+ },
+ error: (error) => {
+ this.toastr.error(`Failed to add device at ${IP}: ${error.message || 'Unknown error'}`, 'Error');
+ }
});
}
@@ -186,7 +281,7 @@ export class SwarmComponent implements OnInit, OnDestroy {
}
public restart(axe: any) {
- this.httpClient.post(`http://${axe.IP}/api/system/restart`, {}).pipe(
+ this.httpClient.post(`http://${axe.IP}/api/system/restart`, {}, this.getHttpOptionsForDevice(axe)).pipe(
catchError(error => {
if (error.status === 0 || error.status === 200 || error.name === 'HttpErrorResponse') {
return of('success');
@@ -232,10 +327,9 @@ export class SwarmComponent implements OnInit, OnDestroy {
}
this.refreshIntervalTime = this.refreshTimeSet;
- const ips = this.swarm.map(axeOs => axeOs.IP);
this.isRefreshing = true;
- this.getAllDeviceInfo(ips, this.refreshErrorHandler, fetchAsic).subscribe({
+ this.getAllDeviceInfo(this.refreshErrorHandler, fetchAsic).subscribe({
next: (result) => {
this.swarm = result;
this.sortSwarm();
@@ -273,14 +367,29 @@ export class SwarmComponent implements OnInit, OnDestroy {
const fieldType = typeof a[this.sortField];
if (this.sortField === 'IP') {
- // Split IP into octets and compare numerically
- const aOctets = a[this.sortField].split('.').map(Number);
- const bOctets = b[this.sortField].split('.').map(Number);
- for (let i = 0; i < 4; i++) {
- if (aOctets[i] !== bOctets[i]) {
- comparison = aOctets[i] - bOctets[i];
- break;
+ // Check if both are valid IP addresses (4 octets with numeric values)
+ const aIsIP = /^(\d{1,3}\.){3}\d{1,3}$/.test(a[this.sortField]);
+ const bIsIP = /^(\d{1,3}\.){3}\d{1,3}$/.test(b[this.sortField]);
+
+ if (aIsIP && bIsIP) {
+ // Both are IPs - split into octets and compare numerically
+ const aOctets = a[this.sortField].split('.').map(Number);
+ const bOctets = b[this.sortField].split('.').map(Number);
+ for (let i = 0; i < 4; i++) {
+ if (aOctets[i] !== bOctets[i]) {
+ comparison = aOctets[i] - bOctets[i];
+ break;
+ }
}
+ } else if (aIsIP && !bIsIP) {
+ // IP addresses come before hostnames
+ comparison = -1;
+ } else if (!aIsIP && bIsIP) {
+ // Hostnames come after IP addresses
+ comparison = 1;
+ } else {
+ // Both are hostnames - use locale compare
+ comparison = a[this.sortField].localeCompare(b[this.sortField], undefined, { numeric: true });
}
} else if (fieldType === 'number') {
comparison = a[this.sortField] - b[this.sortField];
@@ -354,6 +463,7 @@ export class SwarmComponent implements OnInit, OnDestroy {
case 'Supra': return 'blue';
case 'UltraHex': return 'orange';
case 'Gamma': return 'green';
+ case 'GammaHex': return 'lime'; // New color?
case 'GammaTurbo': return 'cyan';
default: return 'gray';
}
diff --git a/main/http_server/axe-os/src/app/services/system.service.ts b/main/http_server/axe-os/src/app/services/system.service.ts
index 7ffb54bbc..4bb2b4db8 100644
--- a/main/http_server/axe-os/src/app/services/system.service.ts
+++ b/main/http_server/axe-os/src/app/services/system.service.ts
@@ -16,9 +16,22 @@ export class SystemService {
private httpClient: HttpClient
) { }
- public getInfo(uri: string = ''): Observable {
+ private getHttpOptions(apiSecret?: string): { headers?: any } {
+ if (!apiSecret || apiSecret.trim() === '') {
+ return {};
+ }
+
+ return {
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'Authorization': `Bearer ${apiSecret}`
+ }
+ };
+ }
+
+ public getInfo(uri: string = '', apiSecret?: string): Observable {
if (environment.production) {
- return this.httpClient.get(`${uri}/api/system/info`) as Observable;
+ return this.httpClient.get(`${uri}/api/system/info`, this.getHttpOptions(apiSecret)) as Observable;
}
// Mock data for development
@@ -39,6 +52,8 @@ export class SystemService {
coreVoltage: 1200,
coreVoltageActual: 1200,
hostname: "Bitaxe",
+ currentIP: "0.0.0.0",
+ netmask: "255.255.255.255",
macAddr: "2C:54:91:88:C9:E3",
ssid: "default",
wifiPass: "password",
@@ -78,6 +93,7 @@ export class SystemService {
temptarget: 60,
statsFrequency: 30,
fanrpm: 0,
+ apiSecret: "devapisecret",
boardtemp1: 30,
boardtemp2: 40,
@@ -86,9 +102,9 @@ export class SystemService {
).pipe(delay(1000));
}
- public getStatistics(uri: string = ''): Observable {
+ public getStatistics(uri: string = '', apiSecret?: string): Observable {
if (environment.production) {
- return this.httpClient.get(`${uri}/api/system/statistics/dashboard`) as Observable;
+ return this.httpClient.get(`${uri}/api/system/statistics/dashboard`, this.getHttpOptions(apiSecret)) as Observable;
}
// Mock data for development
@@ -109,33 +125,38 @@ export class SystemService {
}).pipe(delay(1000));
}
- public restart(uri: string = '') {
- return this.httpClient.post(`${uri}/api/system/restart`, {}, {responseType: 'text'});
+ public restart(uri: string = '', apiSecret?: string) {
+ const options = { ...this.getHttpOptions(apiSecret), responseType: 'text' as 'text' };
+ return this.httpClient.post(`${uri}/api/system/restart`, {}, options);
}
- public updateSystem(uri: string = '', update: any) {
+ public updateSystem(uri: string = '', update: any, apiSecret?: string) {
if (environment.production) {
- return this.httpClient.patch(`${uri}/api/system`, update);
+ return this.httpClient.patch(`${uri}/api/system`, update, this.getHttpOptions(apiSecret));
} else {
return of(true);
}
}
- private otaUpdate(file: File | Blob, url: string) {
+ private otaUpdate(file: File | Blob, url: string, apiSecret?: string) {
return new Observable>((subscriber) => {
const reader = new FileReader();
reader.onload = (event: any) => {
const fileContent = event.target.result;
+ const httpOptions = this.getHttpOptions(apiSecret);
+ const headers = {
+ 'Content-Type': 'application/octet-stream',
+ ...(httpOptions.headers || {})
+ };
+
return this.httpClient.post(url, fileContent, {
reportProgress: true,
observe: 'events',
responseType: 'text', // Specify the response type
- headers: {
- 'Content-Type': 'application/octet-stream', // Set the content type
- },
+ headers: headers,
}).subscribe({
next: (event) => {
subscriber.next(event);
@@ -152,16 +173,16 @@ export class SystemService {
});
}
- public performOTAUpdate(file: File | Blob) {
- return this.otaUpdate(file, `/api/system/OTA`);
+ public performOTAUpdate(file: File | Blob, apiSecret?: string) {
+ return this.otaUpdate(file, `/api/system/OTA`, apiSecret);
}
- public performWWWOTAUpdate(file: File | Blob) {
- return this.otaUpdate(file, `/api/system/OTAWWW`);
+ public performWWWOTAUpdate(file: File | Blob, apiSecret?: string) {
+ return this.otaUpdate(file, `/api/system/OTAWWW`, apiSecret);
}
- public getAsicSettings(uri: string = ''): Observable {
+ public getAsicSettings(uri: string = '', apiSecret?: string): Observable {
if (environment.production) {
- return this.httpClient.get(`${uri}/api/system/asic`) as Observable;
+ return this.httpClient.get(`${uri}/api/system/asic`, this.getHttpOptions(apiSecret)) as Observable;
}
// Mock data for development
@@ -177,11 +198,11 @@ export class SystemService {
}).pipe(delay(1000));
}
- public getSwarmInfo(uri: string = ''): Observable<{ ip: string }[]> {
- return this.httpClient.get(`${uri}/api/swarm/info`) as Observable<{ ip: string }[]>;
+ public getSwarmInfo(uri: string = '', apiSecret?: string): Observable<{ ip: string }[]> {
+ return this.httpClient.get(`${uri}/api/swarm/info`, this.getHttpOptions(apiSecret)) as Observable<{ ip: string }[]>;
}
- public updateSwarm(uri: string = '', swarmConfig: any) {
- return this.httpClient.patch(`${uri}/api/swarm`, swarmConfig);
+ public updateSwarm(uri: string = '', swarmConfig: any, apiSecret?: string) {
+ return this.httpClient.patch(`${uri}/api/swarm`, swarmConfig, this.getHttpOptions(apiSecret));
}
}
diff --git a/main/http_server/axe-os/src/models/ISystemInfo.ts b/main/http_server/axe-os/src/models/ISystemInfo.ts
index bbf6d94e2..a9d17ad53 100644
--- a/main/http_server/axe-os/src/models/ISystemInfo.ts
+++ b/main/http_server/axe-os/src/models/ISystemInfo.ts
@@ -22,7 +22,10 @@ export interface ISystemInfo {
freeHeap: number,
coreVoltage: number,
hostname: string,
+ apiSecret: string,
macAddr: string,
+ currentIP: string,
+ netmask: string,
ssid: string,
wifiStatus: string,
wifiRSSI: number,
diff --git a/main/http_server/http_server.c b/main/http_server/http_server.c
index 694ffff9b..bd9fa98dd 100644
--- a/main/http_server/http_server.c
+++ b/main/http_server/http_server.c
@@ -26,6 +26,8 @@
#include "lwip/netdb.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
+#include "mdns.h"
+#include
#include "cJSON.h"
#include "global_state.h"
@@ -49,14 +51,93 @@ static const char * CORS_TAG = "CORS";
static char axeOSVersion[32];
+/* Handler for mDNS network scan endpoint - discovers all _bitaxe._tcp services */
+static esp_err_t GET_network_scan(httpd_req_t *req)
+{
+ if (is_request_authorized(req) != ESP_OK) {
+ return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
+ }
+
+ httpd_resp_set_type(req, "application/json");
+
+ // Set CORS headers
+ if (set_cors_headers(req) != ESP_OK) {
+ httpd_resp_send_500(req);
+ return ESP_OK;
+ }
+
+ // Use mDNS to discover _bitaxe._tcp services
+ ESP_LOGI(TAG, "Starting mDNS query for _bitaxe._tcp services");
+
+ cJSON *root = cJSON_CreateArray();
+
+ mdns_result_t *results = NULL;
+ esp_err_t err = mdns_query_ptr("_bitaxe", "_tcp", 3000, 20, &results);
+
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "mDNS query failed: %s", esp_err_to_name(err));
+ cJSON_Delete(root);
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "mDNS query failed");
+ return ESP_OK;
+ }
+
+ if (!results) {
+ ESP_LOGI(TAG, "No _bitaxe._tcp services found");
+ } else {
+ mdns_result_t *r = results;
+ while (r) {
+ cJSON *device = cJSON_CreateObject();
+
+ // Get IP address
+ if (r->addr) {
+ char ip_str[INET_ADDRSTRLEN];
+ inet_ntop(AF_INET, &r->addr->addr.u_addr.ip4, ip_str, sizeof(ip_str));
+ cJSON_AddStringToObject(device, "IP", ip_str);
+
+ // Add hostname if available
+ if (r->hostname) {
+ cJSON_AddStringToObject(device, "hostname", r->hostname);
+ }
+
+ // Add instance name if available
+ if (r->instance_name) {
+ cJSON_AddStringToObject(device, "instance", r->instance_name);
+ }
+
+ // Extract apiSecret from TXT records
+ if (r->txt && r->txt_count > 0) {
+ for (size_t i = 0; i < r->txt_count; i++) {
+ if (strcmp(r->txt[i].key, "apiSecret") == 0 && r->txt[i].value) {
+ cJSON_AddStringToObject(device, "apiSecret", r->txt[i].value);
+ break;
+ }
+ }
+ }
+
+ cJSON_AddItemToArray(root, device);
+ }
+
+ r = r->next;
+ }
+ mdns_query_results_free(results);
+ }
+
+ const char *response = cJSON_Print(root);
+ httpd_resp_sendstr(req, response);
+
+ free((void *)response);
+ cJSON_Delete(root);
+ return ESP_OK;
+}
+
/* Handler for WiFi scan endpoint */
static esp_err_t GET_wifi_scan(httpd_req_t *req)
{
httpd_resp_set_type(req, "application/json");
-
+
// Give some time for the connected flag to take effect
vTaskDelay(100 / portTICK_PERIOD_MS);
-
+
wifi_ap_record_simple_t ap_records[20];
uint16_t ap_count = 0;
@@ -113,102 +194,120 @@ typedef struct rest_server_context
#define CHECK_FILE_EXTENSION(filename, ext) (strcasecmp(&filename[strlen(filename) - strlen(ext)], ext) == 0)
-static esp_err_t ip_in_private_range(uint32_t address) {
- uint32_t ip_address = ntohl(address);
+static bool is_cross_origin_request(httpd_req_t * req)
+{
+ // Get the Host header (server's hostname/IP)
+ char host[128] = {0};
+ if (httpd_req_get_hdr_value_str(req, "Host", host, sizeof(host)) != ESP_OK) {
+ ESP_LOGD(CORS_TAG, "No Host header found");
+ return false;
+ }
- // 10.0.0.0 - 10.255.255.255 (Class A)
- if ((ip_address >= 0x0A000000) && (ip_address <= 0x0AFFFFFF)) {
- return ESP_OK;
+ // Get the Origin header (client's origin)
+ char origin[128] = {0};
+ if (httpd_req_get_hdr_value_str(req, "Origin", origin, sizeof(origin)) != ESP_OK) {
+ ESP_LOGD(CORS_TAG, "No Origin header found - not a cross-origin request");
+ return false;
}
- // 172.16.0.0 - 172.31.255.255 (Class B)
- if ((ip_address >= 0xAC100000) && (ip_address <= 0xAC1FFFFF)) {
- return ESP_OK;
+ // Extract hostname from origin (remove protocol)
+ char *origin_host = origin;
+ if (strncmp(origin, "http://", 7) == 0) {
+ origin_host = origin + 7;
+ } else if (strncmp(origin, "https://", 8) == 0) {
+ origin_host = origin + 8;
}
- // 192.168.0.0 - 192.168.255.255 (Class C)
- if ((ip_address >= 0xC0A80000) && (ip_address <= 0xC0A8FFFF)) {
- return ESP_OK;
+ // Remove path from origin host if present
+ char *path_start = strchr(origin_host, '/');
+ if (path_start) {
+ *path_start = '\0';
}
- return ESP_FAIL;
+ // Compare host headers - if different, it's cross-origin
+ bool is_cross_origin = strcmp(host, origin_host) != 0;
+
+ ESP_LOGD(CORS_TAG, "Host: %s, Origin: %s, Cross-origin: %s",
+ host, origin_host, is_cross_origin ? "yes" : "no");
+
+ return is_cross_origin;
}
-static uint32_t extract_origin_ip_addr(char *origin)
+static bool constant_time_compare(const char *a, const char *b)
{
- char ip_str[16];
- uint32_t origin_ip_addr = 0;
-
- // Find the start of the IP address in the Origin header
- const char *prefix = "http://";
- char *ip_start = strstr(origin, prefix);
- if (ip_start) {
- ip_start += strlen(prefix); // Move past "http://"
-
- // Extract the IP address portion (up to the next '/')
- char *ip_end = strchr(ip_start, '/');
- size_t ip_len = ip_end ? (size_t)(ip_end - ip_start) : strlen(ip_start);
- if (ip_len < sizeof(ip_str)) {
- strncpy(ip_str, ip_start, ip_len);
- ip_str[ip_len] = '\0'; // Null-terminate the string
-
- // Convert the IP address string to uint32_t
- origin_ip_addr = inet_addr(ip_str);
- if (origin_ip_addr == INADDR_NONE) {
- ESP_LOGW(CORS_TAG, "Invalid IP address: %s", ip_str);
- } else {
- ESP_LOGD(CORS_TAG, "Extracted IP address %lu", origin_ip_addr);
- }
- } else {
- ESP_LOGW(CORS_TAG, "IP address string is too long: %s", ip_start);
- }
+ volatile uint8_t result = 0;
+ size_t i = 0;
+
+ // Compare characters until we reach the end of both strings
+ while (a[i] != '\0' || b[i] != '\0') {
+ result |= a[i] ^ b[i];
+ i++;
}
- return origin_ip_addr;
+ return result == 0;
}
-esp_err_t is_network_allowed(httpd_req_t * req)
+esp_err_t is_request_authorized(httpd_req_t * req)
{
+ // Always allow requests in AP mode
if (GLOBAL_STATE->SYSTEM_MODULE.ap_enabled == true) {
- ESP_LOGI(CORS_TAG, "Device in AP mode. Allowing CORS.");
+ ESP_LOGI(CORS_TAG, "Device in AP mode. Allowing request.");
+ return ESP_OK;
+ }
+
+ // If it's not a cross-origin request, allow it
+ if (!is_cross_origin_request(req)) {
+ ESP_LOGD(CORS_TAG, "Same-origin request - allowing");
return ESP_OK;
}
- int sockfd = httpd_req_to_sockfd(req);
- char ipstr[INET6_ADDRSTRLEN];
- struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing
- socklen_t addr_size = sizeof(addr);
+ // For cross-origin requests, require X-Requested-With header
+ ESP_LOGI(CORS_TAG, "Cross-origin request detected - checking requirements");
- if (getpeername(sockfd, (struct sockaddr *)&addr, &addr_size) < 0) {
- ESP_LOGE(CORS_TAG, "Error getting client IP");
+ char x_requested_with[64] = {0};
+ if (httpd_req_get_hdr_value_str(req, "X-Requested-With", x_requested_with, sizeof(x_requested_with)) != ESP_OK) {
+ ESP_LOGW(CORS_TAG, "Cross-origin request blocked - missing X-Requested-With header");
return ESP_FAIL;
}
- uint32_t request_ip_addr = addr.sin6_addr.un.u32_addr[3];
+ char * configured_secret = nvs_config_get_string(NVS_CONFIG_API_SECRET, "");
- // // Convert to IPv6 string
- // inet_ntop(AF_INET, &addr.sin6_addr, ipstr, sizeof(ipstr));
+ // If no API secret is configured, deny cross-origin requests
+ if (strlen(configured_secret) == 0) {
+ ESP_LOGW(CORS_TAG, "Cross-origin request blocked - no API secret configured");
+ free(configured_secret);
+ return ESP_FAIL;
+ }
- // Convert to IPv4 string
- inet_ntop(AF_INET, &request_ip_addr, ipstr, sizeof(ipstr));
+ // Get the Authorization header and check for Bearer token
+ char auth_header[128] = {0};
+ if (httpd_req_get_hdr_value_str(req, "Authorization", auth_header, sizeof(auth_header)) != ESP_OK) {
+ ESP_LOGW(CORS_TAG, "Cross-origin request blocked - no Authorization header");
+ free(configured_secret);
+ return ESP_FAIL;
+ }
- // Attempt to get the Origin header.
- char origin[128];
- uint32_t origin_ip_addr;
- if (httpd_req_get_hdr_value_str(req, "Origin", origin, sizeof(origin)) == ESP_OK) {
- ESP_LOGD(CORS_TAG, "Origin header: %s", origin);
- origin_ip_addr = extract_origin_ip_addr(origin);
- } else {
- ESP_LOGD(CORS_TAG, "No origin header found.");
- origin_ip_addr = request_ip_addr;
+ // Check if it starts with "Bearer "
+ const char *bearer_prefix = "Bearer ";
+ if (strncmp(auth_header, bearer_prefix, strlen(bearer_prefix)) != 0) {
+ ESP_LOGW(CORS_TAG, "Cross-origin request blocked - Authorization header must use Bearer scheme");
+ free(configured_secret);
+ return ESP_FAIL;
}
- if (ip_in_private_range(origin_ip_addr) == ESP_OK && ip_in_private_range(request_ip_addr) == ESP_OK) {
- return ESP_OK;
+ // Extract the token
+ char *provided_secret = auth_header + strlen(bearer_prefix);
+
+ // Compare secrets using constant-time comparison
+ if (!constant_time_compare(configured_secret, provided_secret)) {
+ ESP_LOGW(CORS_TAG, "Cross-origin request blocked - invalid API secret");
+ free(configured_secret);
+ return ESP_FAIL;
}
- ESP_LOGI(CORS_TAG, "Client is NOT in the private ip ranges or same range as server.");
- return ESP_FAIL;
+ ESP_LOGI(CORS_TAG, "Cross-origin request authorized with valid API secret");
+ free(configured_secret);
+ return ESP_OK;
}
static void readAxeOSVersion(void) {
@@ -306,7 +405,7 @@ esp_err_t set_cors_headers(httpd_req_t * req)
return ESP_FAIL;
}
- err = httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
+ err = httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
if (err != ESP_OK) {
return ESP_FAIL;
}
@@ -317,7 +416,7 @@ esp_err_t set_cors_headers(httpd_req_t * req)
/* Recovery handler */
static esp_err_t rest_recovery_handler(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
@@ -332,7 +431,7 @@ static esp_err_t rest_recovery_handler(httpd_req_t * req)
/* Send a 404 as JSON for unhandled api routes */
static esp_err_t rest_api_common_handler(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
@@ -418,10 +517,6 @@ static esp_err_t rest_common_get_handler(httpd_req_t * req)
static esp_err_t handle_options_request(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
- return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
- }
-
// Set CORS headers for OPTIONS request
if (set_cors_headers(req) != ESP_OK) {
httpd_resp_send_500(req);
@@ -437,7 +532,7 @@ static esp_err_t handle_options_request(httpd_req_t * req)
static esp_err_t PATCH_update_settings(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
@@ -519,6 +614,9 @@ static esp_err_t PATCH_update_settings(httpd_req_t * req)
if (cJSON_IsString(item = cJSON_GetObjectItem(root, "hostname"))) {
nvs_config_set_string(NVS_CONFIG_HOSTNAME, item->valuestring);
}
+ if (cJSON_IsString(item = cJSON_GetObjectItem(root, "apiSecret"))) {
+ nvs_config_set_string(NVS_CONFIG_API_SECRET, item->valuestring);
+ }
if ((item = cJSON_GetObjectItem(root, "coreVoltage")) != NULL && item->valueint > 0) {
nvs_config_set_u16(NVS_CONFIG_ASIC_VOLTAGE, item->valueint);
}
@@ -563,7 +661,7 @@ static esp_err_t PATCH_update_settings(httpd_req_t * req)
static esp_err_t POST_restart(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
@@ -593,7 +691,7 @@ static esp_err_t POST_restart(httpd_req_t * req)
/* Simple handler for getting system handler */
static esp_err_t GET_system_info(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
@@ -607,6 +705,7 @@ static esp_err_t GET_system_info(httpd_req_t * req)
char * ssid = nvs_config_get_string(NVS_CONFIG_WIFI_SSID, CONFIG_ESP_WIFI_SSID);
char * hostname = nvs_config_get_string(NVS_CONFIG_HOSTNAME, CONFIG_LWIP_LOCAL_HOSTNAME);
+ char * apiSecret = nvs_config_get_string(NVS_CONFIG_API_SECRET, "");
char * stratumURL = nvs_config_get_string(NVS_CONFIG_STRATUM_URL, CONFIG_STRATUM_URL);
char * fallbackStratumURL = nvs_config_get_string(NVS_CONFIG_FALLBACK_STRATUM_URL, CONFIG_FALLBACK_STRATUM_URL);
char * stratumUser = nvs_config_get_string(NVS_CONFIG_STRATUM_USER, CONFIG_STRATUM_USER);
@@ -620,6 +719,18 @@ static esp_err_t GET_system_info(httpd_req_t * req)
char formattedMac[18];
snprintf(formattedMac, sizeof(formattedMac), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
+ // Get current IP and netmask information
+ char currentIP[16] = "0.0.0.0";
+ char netmask[16] = "0.0.0.0";
+ esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (netif) {
+ esp_netif_ip_info_t ip_info;
+ if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
+ strcpy(currentIP, inet_ntoa(ip_info.ip));
+ strcpy(netmask, inet_ntoa(ip_info.netmask));
+ }
+ }
+
int8_t wifi_rssi = -90;
get_wifi_current_rssi(&wifi_rssi);
@@ -647,7 +758,10 @@ static esp_err_t GET_system_info(httpd_req_t * req)
cJSON_AddNumberToObject(root, "frequency", frequency);
cJSON_AddStringToObject(root, "ssid", ssid);
cJSON_AddStringToObject(root, "macAddr", formattedMac);
+ cJSON_AddStringToObject(root, "currentIP", currentIP);
+ cJSON_AddStringToObject(root, "netmask", netmask);
cJSON_AddStringToObject(root, "hostname", hostname);
+ cJSON_AddStringToObject(root, "apiSecret", apiSecret);
cJSON_AddStringToObject(root, "wifiStatus", GLOBAL_STATE->SYSTEM_MODULE.wifi_status);
cJSON_AddNumberToObject(root, "wifiRSSI", wifi_rssi);
cJSON_AddNumberToObject(root, "apEnabled", GLOBAL_STATE->SYSTEM_MODULE.ap_enabled);
@@ -656,7 +770,7 @@ static esp_err_t GET_system_info(httpd_req_t * req)
cJSON *error_array = cJSON_CreateArray();
cJSON_AddItemToObject(root, "sharesRejectedReasons", error_array);
-
+
for (int i = 0; i < GLOBAL_STATE->SYSTEM_MODULE.rejected_reason_stats_count; i++) {
cJSON *error_obj = cJSON_CreateObject();
cJSON_AddStringToObject(error_obj, "message", GLOBAL_STATE->SYSTEM_MODULE.rejected_reason_stats[i].message);
@@ -692,7 +806,7 @@ static esp_err_t GET_system_info(httpd_req_t * req)
cJSON_AddNumberToObject(root, "rotation", nvs_config_get_u16(NVS_CONFIG_ROTATION, 0));
cJSON_AddNumberToObject(root, "invertscreen", nvs_config_get_u16(NVS_CONFIG_INVERT_SCREEN, 0));
cJSON_AddNumberToObject(root, "displayTimeout", nvs_config_get_i32(NVS_CONFIG_DISPLAY_TIMEOUT, -1));
-
+
cJSON_AddNumberToObject(root, "autofanspeed", nvs_config_get_u16(NVS_CONFIG_AUTO_FAN_SPEED, 1));
cJSON_AddNumberToObject(root, "fanspeed", GLOBAL_STATE->POWER_MANAGEMENT_MODULE.fan_perc);
@@ -707,6 +821,7 @@ static esp_err_t GET_system_info(httpd_req_t * req)
free(ssid);
free(hostname);
+ free(apiSecret);
free(stratumURL);
free(fallbackStratumURL);
free(stratumUser);
@@ -800,7 +915,7 @@ int create_json_statistics_dashboard(cJSON * root)
static esp_err_t GET_system_statistics(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
@@ -829,7 +944,7 @@ static esp_err_t GET_system_statistics(httpd_req_t * req)
static esp_err_t GET_system_statistics_dashboard(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
@@ -858,7 +973,7 @@ static esp_err_t GET_system_statistics_dashboard(httpd_req_t * req)
esp_err_t POST_WWW_update(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
@@ -933,7 +1048,7 @@ esp_err_t POST_WWW_update(httpd_req_t * req)
*/
esp_err_t POST_OTA_update(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
@@ -944,7 +1059,7 @@ esp_err_t POST_OTA_update(httpd_req_t * req)
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Not allowed in AP mode");
return ESP_OK;
}
-
+
GLOBAL_STATE->SYSTEM_MODULE.is_firmware_update = true;
snprintf(GLOBAL_STATE->SYSTEM_MODULE.firmware_update_filename, 20, "esp-miner.bin");
snprintf(GLOBAL_STATE->SYSTEM_MODULE.firmware_update_status, 20, "Starting...");
@@ -1069,7 +1184,7 @@ void send_log_to_websocket(char *message)
*/
esp_err_t echo_handler(httpd_req_t * req)
{
- if (is_network_allowed(req) != ESP_OK) {
+ if (is_request_authorized(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
@@ -1122,7 +1237,7 @@ void websocket_log_handler()
esp_err_t start_rest_server(void * pvParameters)
{
GLOBAL_STATE = (GlobalState *) pvParameters;
-
+
// Initialize the ASIC API with the global state
asic_api_init(GLOBAL_STATE);
const char * base_path = "";
@@ -1157,42 +1272,51 @@ esp_err_t start_rest_server(void * pvParameters)
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &recovery_explicit_get_uri);
-
+
// Register theme API endpoints
ESP_ERROR_CHECK(register_theme_api_endpoints(server, rest_context));
+ /* Respond with * for all CORS preflight requests - all API routes require authentication for cross-origin requests */
+ httpd_uri_t api_options_uri = {
+ .uri = "/api/*",
+ .method = HTTP_OPTIONS,
+ .handler = handle_options_request,
+ .user_ctx = NULL,
+ };
+ httpd_register_uri_handler(server, &api_options_uri);
+
/* URI handler for fetching system info */
httpd_uri_t system_info_get_uri = {
- .uri = "/api/system/info",
- .method = HTTP_GET,
- .handler = GET_system_info,
+ .uri = "/api/system/info",
+ .method = HTTP_GET,
+ .handler = GET_system_info,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &system_info_get_uri);
/* URI handler for fetching system asic values */
httpd_uri_t system_asic_get_uri = {
- .uri = "/api/system/asic",
- .method = HTTP_GET,
- .handler = GET_system_asic,
+ .uri = "/api/system/asic",
+ .method = HTTP_GET,
+ .handler = GET_system_asic,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &system_asic_get_uri);
/* URI handler for fetching system statistic values */
httpd_uri_t system_statistics_get_uri = {
- .uri = "/api/system/statistics",
- .method = HTTP_GET,
- .handler = GET_system_statistics,
+ .uri = "/api/system/statistics",
+ .method = HTTP_GET,
+ .handler = GET_system_statistics,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &system_statistics_get_uri);
/* URI handler for fetching system statistic values for dashboard */
httpd_uri_t system_statistics_dashboard_get_uri = {
- .uri = "/api/system/statistics/dashboard",
- .method = HTTP_GET,
- .handler = GET_system_statistics_dashboard,
+ .uri = "/api/system/statistics/dashboard",
+ .method = HTTP_GET,
+ .handler = GET_system_statistics_dashboard,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &system_statistics_dashboard_get_uri);
@@ -1206,58 +1330,51 @@ esp_err_t start_rest_server(void * pvParameters)
};
httpd_register_uri_handler(server, &wifi_scan_get_uri);
- httpd_uri_t system_restart_uri = {
- .uri = "/api/system/restart", .method = HTTP_POST,
- .handler = POST_restart,
+ /* URI handler for network scan */
+ httpd_uri_t network_scan_get_uri = {
+ .uri = "/api/system/network/scan",
+ .method = HTTP_GET,
+ .handler = GET_network_scan,
.user_ctx = rest_context
};
- httpd_register_uri_handler(server, &system_restart_uri);
+ httpd_register_uri_handler(server, &network_scan_get_uri);
- httpd_uri_t system_restart_options_uri = {
- .uri = "/api/system/restart",
- .method = HTTP_OPTIONS,
- .handler = handle_options_request,
- .user_ctx = NULL
+ httpd_uri_t system_restart_uri = {
+ .uri = "/api/system/restart", .method = HTTP_POST,
+ .handler = POST_restart,
+ .user_ctx = rest_context
};
- httpd_register_uri_handler(server, &system_restart_options_uri);
+ httpd_register_uri_handler(server, &system_restart_uri);
httpd_uri_t update_system_settings_uri = {
- .uri = "/api/system",
- .method = HTTP_PATCH,
- .handler = PATCH_update_settings,
+ .uri = "/api/system",
+ .method = HTTP_PATCH,
+ .handler = PATCH_update_settings,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &update_system_settings_uri);
- httpd_uri_t system_options_uri = {
- .uri = "/api/system",
- .method = HTTP_OPTIONS,
- .handler = handle_options_request,
- .user_ctx = NULL,
- };
- httpd_register_uri_handler(server, &system_options_uri);
-
httpd_uri_t update_post_ota_firmware = {
- .uri = "/api/system/OTA",
- .method = HTTP_POST,
- .handler = POST_OTA_update,
+ .uri = "/api/system/OTA",
+ .method = HTTP_POST,
+ .handler = POST_OTA_update,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &update_post_ota_firmware);
httpd_uri_t update_post_ota_www = {
- .uri = "/api/system/OTAWWW",
- .method = HTTP_POST,
- .handler = POST_WWW_update,
+ .uri = "/api/system/OTAWWW",
+ .method = HTTP_POST,
+ .handler = POST_WWW_update,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &update_post_ota_www);
httpd_uri_t ws = {
- .uri = "/api/ws",
- .method = HTTP_GET,
- .handler = echo_handler,
- .user_ctx = NULL,
+ .uri = "/api/ws",
+ .method = HTTP_GET,
+ .handler = echo_handler,
+ .user_ctx = NULL,
.is_websocket = true
};
httpd_register_uri_handler(server, &ws);
@@ -1265,8 +1382,8 @@ esp_err_t start_rest_server(void * pvParameters)
if (enter_recovery) {
/* Make default route serve Recovery */
httpd_uri_t recovery_implicit_get_uri = {
- .uri = "/*", .method = HTTP_GET,
- .handler = rest_recovery_handler,
+ .uri = "/*", .method = HTTP_GET,
+ .handler = rest_recovery_handler,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &recovery_implicit_get_uri);
@@ -1281,9 +1398,9 @@ esp_err_t start_rest_server(void * pvParameters)
httpd_register_uri_handler(server, &api_common_uri);
/* URI handler for getting web server files */
httpd_uri_t common_get_uri = {
- .uri = "/*",
- .method = HTTP_GET,
- .handler = rest_common_get_handler,
+ .uri = "/*",
+ .method = HTTP_GET,
+ .handler = rest_common_get_handler,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &common_get_uri);
diff --git a/main/http_server/http_server.h b/main/http_server/http_server.h
index 51a1c16ec..cc8245c2a 100644
--- a/main/http_server/http_server.h
+++ b/main/http_server/http_server.h
@@ -3,5 +3,7 @@
#include
esp_err_t start_rest_server(void *pvParameters);
+esp_err_t is_request_authorized(httpd_req_t *req);
+esp_err_t set_cors_headers(httpd_req_t *req);
#endif
\ No newline at end of file
diff --git a/main/idf_component.yml b/main/idf_component.yml
index c3cdc258e..18f9445d7 100644
--- a/main/idf_component.yml
+++ b/main/idf_component.yml
@@ -1,8 +1,9 @@
## IDF Component Manager Manifest File
dependencies:
- lvgl/lvgl: "9.3.0"
- espressif/esp_lvgl_port: "^2.6.0"
- esp_lcd_sh1107: "^1"
+ lvgl/lvgl: 9.3.0
+ espressif/esp_lvgl_port: ^2.6.0
+ esp_lcd_sh1107: ^1
## Required IDF version
idf:
- version: ">=5.4.0"
+ version: '>=5.4.0'
+ espressif/mdns: ^1.8.2
diff --git a/main/nvs_config.h b/main/nvs_config.h
index db8cb077b..23f260ed3 100644
--- a/main/nvs_config.h
+++ b/main/nvs_config.h
@@ -8,6 +8,7 @@
#define NVS_CONFIG_WIFI_SSID "wifissid"
#define NVS_CONFIG_WIFI_PASS "wifipass"
#define NVS_CONFIG_HOSTNAME "hostname"
+#define NVS_CONFIG_API_SECRET "apisecret"
#define NVS_CONFIG_STRATUM_URL "stratumurl"
#define NVS_CONFIG_STRATUM_PORT "stratumport"
#define NVS_CONFIG_FALLBACK_STRATUM_URL "fbstratumurl"