From 15f5a25f23d1a08068d392d85d3e6d7ea18c0e7e Mon Sep 17 00:00:00 2001
From: Alex Goodkind <alex@goodkind.io>
Date: Thu, 22 May 2025 18:10:02 -0700
Subject: [PATCH] feat: add local web server loopback mode configuration

- Introduced a new configuration option `LocalWebServerLoopbackOnly` to restrict the web server to listen only on the loopback interface.
- Added RPC methods `rpcGetLocalWebServerLoopbackOnly` and `rpcSetLocalWebServerLoopbackOnly` for retrieving and updating this setting.
- Updated the web server startup logic to bind to the appropriate address based on the new configuration.
- Modified the `LocalDevice` struct to include the loopback setting in the response.
---
 config.go  |  72 ++++++++++----------
 jsonrpc.go | 190 +++++++++++++++++++++++++++++++----------------------
 web.go     |  20 ++++--
 3 files changed, 165 insertions(+), 117 deletions(-)

diff --git a/config.go b/config.go
index e699ff3..2f47262 100644
--- a/config.go
+++ b/config.go
@@ -75,46 +75,48 @@ func (m *KeyboardMacro) Validate() error {
 }
 
 type Config struct {
-	CloudURL             string                 `json:"cloud_url"`
-	CloudAppURL          string                 `json:"cloud_app_url"`
-	CloudToken           string                 `json:"cloud_token"`
-	GoogleIdentity       string                 `json:"google_identity"`
-	JigglerEnabled       bool                   `json:"jiggler_enabled"`
-	AutoUpdateEnabled    bool                   `json:"auto_update_enabled"`
-	IncludePreRelease    bool                   `json:"include_pre_release"`
-	HashedPassword       string                 `json:"hashed_password"`
-	LocalAuthToken       string                 `json:"local_auth_token"`
-	LocalAuthMode        string                 `json:"localAuthMode"` //TODO: fix it with migration
-	WakeOnLanDevices     []WakeOnLanDevice      `json:"wake_on_lan_devices"`
-	KeyboardMacros       []KeyboardMacro        `json:"keyboard_macros"`
-	KeyboardLayout       string                 `json:"keyboard_layout"`
-	EdidString           string                 `json:"hdmi_edid_string"`
-	ActiveExtension      string                 `json:"active_extension"`
-	DisplayRotation      string                 `json:"display_rotation"`
-	DisplayMaxBrightness int                    `json:"display_max_brightness"`
-	DisplayDimAfterSec   int                    `json:"display_dim_after_sec"`
-	DisplayOffAfterSec   int                    `json:"display_off_after_sec"`
-	TLSMode              string                 `json:"tls_mode"` // options: "self-signed", "user-defined", ""
-	UsbConfig            *usbgadget.Config      `json:"usb_config"`
-	UsbDevices           *usbgadget.Devices     `json:"usb_devices"`
-	NetworkConfig        *network.NetworkConfig `json:"network_config"`
-	DefaultLogLevel      string                 `json:"default_log_level"`
+	CloudURL                   string                 `json:"cloud_url"`
+	CloudAppURL                string                 `json:"cloud_app_url"`
+	CloudToken                 string                 `json:"cloud_token"`
+	GoogleIdentity             string                 `json:"google_identity"`
+	JigglerEnabled             bool                   `json:"jiggler_enabled"`
+	AutoUpdateEnabled          bool                   `json:"auto_update_enabled"`
+	IncludePreRelease          bool                   `json:"include_pre_release"`
+	HashedPassword             string                 `json:"hashed_password"`
+	LocalAuthToken             string                 `json:"local_auth_token"`
+	LocalAuthMode              string                 `json:"localAuthMode"` //TODO: fix it with migration
+	WakeOnLanDevices           []WakeOnLanDevice      `json:"wake_on_lan_devices"`
+	KeyboardMacros             []KeyboardMacro        `json:"keyboard_macros"`
+	KeyboardLayout             string                 `json:"keyboard_layout"`
+	EdidString                 string                 `json:"hdmi_edid_string"`
+	ActiveExtension            string                 `json:"active_extension"`
+	DisplayRotation            string                 `json:"display_rotation"`
+	DisplayMaxBrightness       int                    `json:"display_max_brightness"`
+	DisplayDimAfterSec         int                    `json:"display_dim_after_sec"`
+	DisplayOffAfterSec         int                    `json:"display_off_after_sec"`
+	TLSMode                    string                 `json:"tls_mode"` // options: "self-signed", "user-defined", ""
+	LocalWebServerLoopbackOnly bool                   `json:"local_web_server_loopback_only"`
+	UsbConfig                  *usbgadget.Config      `json:"usb_config"`
+	UsbDevices                 *usbgadget.Devices     `json:"usb_devices"`
+	NetworkConfig              *network.NetworkConfig `json:"network_config"`
+	DefaultLogLevel            string                 `json:"default_log_level"`
 }
 
 const configPath = "/userdata/kvm_config.json"
 
 var defaultConfig = &Config{
-	CloudURL:             "https://api.jetkvm.com",
-	CloudAppURL:          "https://app.jetkvm.com",
-	AutoUpdateEnabled:    true, // Set a default value
-	ActiveExtension:      "",
-	KeyboardMacros:       []KeyboardMacro{},
-	DisplayRotation:      "270",
-	KeyboardLayout:       "en-US",
-	DisplayMaxBrightness: 64,
-	DisplayDimAfterSec:   120,  // 2 minutes
-	DisplayOffAfterSec:   1800, // 30 minutes
-	TLSMode:              "",
+	CloudURL:                   "https://api.jetkvm.com",
+	CloudAppURL:                "https://app.jetkvm.com",
+	AutoUpdateEnabled:          true, // Set a default value
+	ActiveExtension:            "",
+	KeyboardMacros:             []KeyboardMacro{},
+	DisplayRotation:            "270",
+	KeyboardLayout:             "en-US",
+	DisplayMaxBrightness:       64,
+	DisplayDimAfterSec:         120,  // 2 minutes
+	DisplayOffAfterSec:         1800, // 30 minutes
+	TLSMode:                    "",
+	LocalWebServerLoopbackOnly: false, // Allow access from any network interface by default
 	UsbConfig: &usbgadget.Config{
 		VendorId:     "0x1d6b", //The Linux Foundation
 		ProductId:    "0x0104", //Multifunction Composite Gadget
diff --git a/jsonrpc.go b/jsonrpc.go
index a32cab2..578dacf 100644
--- a/jsonrpc.go
+++ b/jsonrpc.go
@@ -1006,81 +1006,117 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
 	return nil, nil
 }
 
-var rpcHandlers = map[string]RPCHandler{
-	"ping":                   {Func: rpcPing},
-	"reboot":                 {Func: rpcReboot, Params: []string{"force"}},
-	"getDeviceID":            {Func: rpcGetDeviceID},
-	"deregisterDevice":       {Func: rpcDeregisterDevice},
-	"getCloudState":          {Func: rpcGetCloudState},
-	"getNetworkState":        {Func: rpcGetNetworkState},
-	"getNetworkSettings":     {Func: rpcGetNetworkSettings},
-	"setNetworkSettings":     {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
-	"renewDHCPLease":         {Func: rpcRenewDHCPLease},
-	"keyboardReport":         {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
-	"getKeyboardLedState":    {Func: rpcGetKeyboardLedState},
-	"absMouseReport":         {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
-	"relMouseReport":         {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
-	"wheelReport":            {Func: rpcWheelReport, Params: []string{"wheelY"}},
-	"getVideoState":          {Func: rpcGetVideoState},
-	"getUSBState":            {Func: rpcGetUSBState},
-	"unmountImage":           {Func: rpcUnmountImage},
-	"rpcMountBuiltInImage":   {Func: rpcMountBuiltInImage, Params: []string{"filename"}},
-	"setJigglerState":        {Func: rpcSetJigglerState, Params: []string{"enabled"}},
-	"getJigglerState":        {Func: rpcGetJigglerState},
-	"sendWOLMagicPacket":     {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}},
-	"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
-	"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},
-	"getAutoUpdateState":     {Func: rpcGetAutoUpdateState},
-	"setAutoUpdateState":     {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}},
-	"getEDID":                {Func: rpcGetEDID},
-	"setEDID":                {Func: rpcSetEDID, Params: []string{"edid"}},
-	"getDevChannelState":     {Func: rpcGetDevChannelState},
-	"setDevChannelState":     {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
-	"getUpdateStatus":        {Func: rpcGetUpdateStatus},
-	"tryUpdate":              {Func: rpcTryUpdate},
-	"getDevModeState":        {Func: rpcGetDevModeState},
-	"setDevModeState":        {Func: rpcSetDevModeState, Params: []string{"enabled"}},
-	"getSSHKeyState":         {Func: rpcGetSSHKeyState},
-	"setSSHKeyState":         {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}},
-	"getTLSState":            {Func: rpcGetTLSState},
-	"setTLSState":            {Func: rpcSetTLSState, Params: []string{"state"}},
-	"setMassStorageMode":     {Func: rpcSetMassStorageMode, Params: []string{"mode"}},
-	"getMassStorageMode":     {Func: rpcGetMassStorageMode},
-	"isUpdatePending":        {Func: rpcIsUpdatePending},
-	"getUsbEmulationState":   {Func: rpcGetUsbEmulationState},
-	"setUsbEmulationState":   {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}},
-	"getUsbConfig":           {Func: rpcGetUsbConfig},
-	"setUsbConfig":           {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}},
-	"checkMountUrl":          {Func: rpcCheckMountUrl, Params: []string{"url"}},
-	"getVirtualMediaState":   {Func: rpcGetVirtualMediaState},
-	"getStorageSpace":        {Func: rpcGetStorageSpace},
-	"mountWithHTTP":          {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
-	"mountWithWebRTC":        {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}},
-	"mountWithStorage":       {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
-	"listStorageFiles":       {Func: rpcListStorageFiles},
-	"deleteStorageFile":      {Func: rpcDeleteStorageFile, Params: []string{"filename"}},
-	"startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}},
-	"getWakeOnLanDevices":    {Func: rpcGetWakeOnLanDevices},
-	"setWakeOnLanDevices":    {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
-	"resetConfig":            {Func: rpcResetConfig},
-	"setDisplayRotation":     {Func: rpcSetDisplayRotation, Params: []string{"params"}},
-	"getDisplayRotation":     {Func: rpcGetDisplayRotation},
-	"setBacklightSettings":   {Func: rpcSetBacklightSettings, Params: []string{"params"}},
-	"getBacklightSettings":   {Func: rpcGetBacklightSettings},
-	"getDCPowerState":        {Func: rpcGetDCPowerState},
-	"setDCPowerState":        {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
-	"getActiveExtension":     {Func: rpcGetActiveExtension},
-	"setActiveExtension":     {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
-	"getATXState":            {Func: rpcGetATXState},
-	"setATXPowerAction":      {Func: rpcSetATXPowerAction, Params: []string{"action"}},
-	"getSerialSettings":      {Func: rpcGetSerialSettings},
-	"setSerialSettings":      {Func: rpcSetSerialSettings, Params: []string{"settings"}},
-	"getUsbDevices":          {Func: rpcGetUsbDevices},
-	"setUsbDevices":          {Func: rpcSetUsbDevices, Params: []string{"devices"}},
-	"setUsbDeviceState":      {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
-	"setCloudUrl":            {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
-	"getKeyboardLayout":      {Func: rpcGetKeyboardLayout},
-	"setKeyboardLayout":      {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
-	"getKeyboardMacros":      {Func: getKeyboardMacros},
-	"setKeyboardMacros":      {Func: setKeyboardMacros, Params: []string{"params"}},
+func rpcGetLocalWebServerLoopbackOnly() (bool, error) {
+	return config.LocalWebServerLoopbackOnly, nil
+}
+
+func rpcSetLocalWebServerLoopbackOnly(enabled bool) error {
+	// Check if the setting is actually changing
+	if config.LocalWebServerLoopbackOnly == enabled {
+		return nil
+	}
+
+	// Update the setting
+	config.LocalWebServerLoopbackOnly = enabled
+	if err := SaveConfig(); err != nil {
+		return fmt.Errorf("failed to save config: %w", err)
+	}
+
+	// Log the change
+	if enabled {
+		logger.Info().Msg("Web server now set to only listen on loopback interface, this will take effect after reboot")
+	} else {
+		logger.Info().Msg("Web server now set to listen on all interfaces, this will take effect after reboot")
+	}
+
+	// Return a message that changes require a reboot
+	message := "Web server binding changed. You must reboot the device for this change to take effect."
+	if enabled {
+		message = "Web server set to loopback-only mode. After reboot, the web interface will only be accessible from the device itself. You must reboot the device for this change to take effect."
+	} else {
+		message = "Web server set to listen on all interfaces. After reboot, the web interface will be accessible from other devices on the network. You must reboot the device for this change to take effect."
+	}
+
+	return fmt.Errorf(message)
+}
+
+var rpcHandlers = map[string]RPCHandler{
+	"ping":                          {Func: rpcPing},
+	"reboot":                        {Func: rpcReboot, Params: []string{"force"}},
+	"getDeviceID":                   {Func: rpcGetDeviceID},
+	"deregisterDevice":              {Func: rpcDeregisterDevice},
+	"getCloudState":                 {Func: rpcGetCloudState},
+	"getNetworkState":               {Func: rpcGetNetworkState},
+	"getNetworkSettings":            {Func: rpcGetNetworkSettings},
+	"setNetworkSettings":            {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
+	"renewDHCPLease":                {Func: rpcRenewDHCPLease},
+	"keyboardReport":                {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
+	"getKeyboardLedState":           {Func: rpcGetKeyboardLedState},
+	"absMouseReport":                {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
+	"relMouseReport":                {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
+	"wheelReport":                   {Func: rpcWheelReport, Params: []string{"wheelY"}},
+	"getVideoState":                 {Func: rpcGetVideoState},
+	"getUSBState":                   {Func: rpcGetUSBState},
+	"unmountImage":                  {Func: rpcUnmountImage},
+	"rpcMountBuiltInImage":          {Func: rpcMountBuiltInImage, Params: []string{"filename"}},
+	"setJigglerState":               {Func: rpcSetJigglerState, Params: []string{"enabled"}},
+	"getJigglerState":               {Func: rpcGetJigglerState},
+	"sendWOLMagicPacket":            {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}},
+	"getStreamQualityFactor":        {Func: rpcGetStreamQualityFactor},
+	"setStreamQualityFactor":        {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},
+	"getAutoUpdateState":            {Func: rpcGetAutoUpdateState},
+	"setAutoUpdateState":            {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}},
+	"getEDID":                       {Func: rpcGetEDID},
+	"setEDID":                       {Func: rpcSetEDID, Params: []string{"edid"}},
+	"getDevChannelState":            {Func: rpcGetDevChannelState},
+	"setDevChannelState":            {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
+	"getUpdateStatus":               {Func: rpcGetUpdateStatus},
+	"tryUpdate":                     {Func: rpcTryUpdate},
+	"getDevModeState":               {Func: rpcGetDevModeState},
+	"setDevModeState":               {Func: rpcSetDevModeState, Params: []string{"enabled"}},
+	"getSSHKeyState":                {Func: rpcGetSSHKeyState},
+	"setSSHKeyState":                {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}},
+	"getTLSState":                   {Func: rpcGetTLSState},
+	"setTLSState":                   {Func: rpcSetTLSState, Params: []string{"state"}},
+	"setMassStorageMode":            {Func: rpcSetMassStorageMode, Params: []string{"mode"}},
+	"getMassStorageMode":            {Func: rpcGetMassStorageMode},
+	"isUpdatePending":               {Func: rpcIsUpdatePending},
+	"getUsbEmulationState":          {Func: rpcGetUsbEmulationState},
+	"setUsbEmulationState":          {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}},
+	"getUsbConfig":                  {Func: rpcGetUsbConfig},
+	"setUsbConfig":                  {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}},
+	"checkMountUrl":                 {Func: rpcCheckMountUrl, Params: []string{"url"}},
+	"getVirtualMediaState":          {Func: rpcGetVirtualMediaState},
+	"getStorageSpace":               {Func: rpcGetStorageSpace},
+	"mountWithHTTP":                 {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
+	"mountWithWebRTC":               {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}},
+	"mountWithStorage":              {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
+	"listStorageFiles":              {Func: rpcListStorageFiles},
+	"deleteStorageFile":             {Func: rpcDeleteStorageFile, Params: []string{"filename"}},
+	"startStorageFileUpload":        {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}},
+	"getWakeOnLanDevices":           {Func: rpcGetWakeOnLanDevices},
+	"setWakeOnLanDevices":           {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
+	"resetConfig":                   {Func: rpcResetConfig},
+	"setDisplayRotation":            {Func: rpcSetDisplayRotation, Params: []string{"params"}},
+	"getDisplayRotation":            {Func: rpcGetDisplayRotation},
+	"setBacklightSettings":          {Func: rpcSetBacklightSettings, Params: []string{"params"}},
+	"getBacklightSettings":          {Func: rpcGetBacklightSettings},
+	"getDCPowerState":               {Func: rpcGetDCPowerState},
+	"setDCPowerState":               {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
+	"getActiveExtension":            {Func: rpcGetActiveExtension},
+	"setActiveExtension":            {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
+	"getATXState":                   {Func: rpcGetATXState},
+	"setATXPowerAction":             {Func: rpcSetATXPowerAction, Params: []string{"action"}},
+	"getSerialSettings":             {Func: rpcGetSerialSettings},
+	"setSerialSettings":             {Func: rpcSetSerialSettings, Params: []string{"settings"}},
+	"getUsbDevices":                 {Func: rpcGetUsbDevices},
+	"setUsbDevices":                 {Func: rpcSetUsbDevices, Params: []string{"devices"}},
+	"setUsbDeviceState":             {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
+	"setCloudUrl":                   {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
+	"getKeyboardLayout":             {Func: rpcGetKeyboardLayout},
+	"setKeyboardLayout":             {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
+	"getKeyboardMacros":             {Func: getKeyboardMacros},
+	"setKeyboardMacros":             {Func: setKeyboardMacros, Params: []string{"params"}},
+	"getLocalWebServerLoopbackOnly": {Func: rpcGetLocalWebServerLoopbackOnly},
+	"setLocalWebServerLoopbackOnly": {Func: rpcSetLocalWebServerLoopbackOnly, Params: []string{"enabled"}},
 }
diff --git a/web.go b/web.go
index 766eaf5..9b57eee 100644
--- a/web.go
+++ b/web.go
@@ -52,8 +52,9 @@ type ChangePasswordRequest struct {
 }
 
 type LocalDevice struct {
-	AuthMode *string `json:"authMode"`
-	DeviceID string  `json:"deviceId"`
+	AuthMode                   *string `json:"authMode"`
+	DeviceID                   string  `json:"deviceId"`
+	LocalWebServerLoopbackOnly bool    `json:"localWebServerLoopbackOnly"`
 }
 
 type DeviceStatus struct {
@@ -532,7 +533,15 @@ func basicAuthProtectedMiddleware(requireDeveloperMode bool) gin.HandlerFunc {
 
 func RunWebServer() {
 	r := setupRouter()
-	err := r.Run(":80")
+
+	// Determine the binding address based on the config
+	bindAddress := ":80" // Default to all interfaces
+	if config.LocalWebServerLoopbackOnly {
+		bindAddress = "localhost:80" // Loopback only (both IPv4 and IPv6)
+	}
+
+	logger.Info().Str("bindAddress", bindAddress).Bool("loopbackOnly", config.LocalWebServerLoopbackOnly).Msg("Starting web server")
+	err := r.Run(bindAddress)
 	if err != nil {
 		panic(err)
 	}
@@ -540,8 +549,9 @@ func RunWebServer() {
 
 func handleDevice(c *gin.Context) {
 	response := LocalDevice{
-		AuthMode: &config.LocalAuthMode,
-		DeviceID: GetDeviceID(),
+		AuthMode:                   &config.LocalAuthMode,
+		DeviceID:                   GetDeviceID(),
+		LocalWebServerLoopbackOnly: config.LocalWebServerLoopbackOnly,
 	}
 
 	c.JSON(http.StatusOK, response)