mirror of https://github.com/jetkvm/kvm.git
				
				
				
			feat: add local web server loopback mode configuration (#511)
* 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. * remove extra logs * chore: add VSCode extensions for improved development environment * refactor: rename LocalWebServerLoopbackOnly to LocalLoopbackOnly - Updated the configuration struct and related RPC methods to use the new name `LocalLoopbackOnly` for clarity. - Adjusted the web server binding logic and device response structure to reflect this change. * feat: add loopback-only mode functionality to UI - Implemented a new setting for enabling loopback-only mode, restricting web interface access to localhost. - Added a confirmation dialog to warn users before enabling this feature. - Updated the ConfirmDialog component to accept React nodes for the description prop. - Refactored imports and adjusted component structure for clarity. * refactor: optimize device settings handlers for better performance - Refactored the `handleDevChannelChange` and `handleLoopbackOnlyModeChange` functions to use `useCallback` for improved performance and to prevent unnecessary re-renders. - Consolidated the logic for applying loopback-only mode into a separate `applyLoopbackOnlyMode` function, enhancing code clarity and maintainability. - Updated the confirmation flow for enabling loopback-only mode to ensure user warnings are displayed appropriately.
This commit is contained in:
		
							parent
							
								
									1f7c5c94d8
								
							
						
					
					
						commit
						718b343713
					
				|  | @ -85,6 +85,7 @@ type Config struct { | |||
| 	HashedPassword       string                 `json:"hashed_password"` | ||||
| 	LocalAuthToken       string                 `json:"local_auth_token"` | ||||
| 	LocalAuthMode        string                 `json:"localAuthMode"` //TODO: fix it with migration
 | ||||
| 	LocalLoopbackOnly    bool                   `json:"local_loopback_only"` | ||||
| 	WakeOnLanDevices     []WakeOnLanDevice      `json:"wake_on_lan_devices"` | ||||
| 	KeyboardMacros       []KeyboardMacro        `json:"keyboard_macros"` | ||||
| 	KeyboardLayout       string                 `json:"keyboard_layout"` | ||||
|  |  | |||
							
								
								
									
										21
									
								
								jsonrpc.go
								
								
								
								
							
							
						
						
									
										21
									
								
								jsonrpc.go
								
								
								
								
							|  | @ -1006,6 +1006,25 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { | |||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| func rpcGetLocalLoopbackOnly() (bool, error) { | ||||
| 	return config.LocalLoopbackOnly, nil | ||||
| } | ||||
| 
 | ||||
| func rpcSetLocalLoopbackOnly(enabled bool) error { | ||||
| 	// Check if the setting is actually changing
 | ||||
| 	if config.LocalLoopbackOnly == enabled { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	// Update the setting
 | ||||
| 	config.LocalLoopbackOnly = enabled | ||||
| 	if err := SaveConfig(); err != nil { | ||||
| 		return fmt.Errorf("failed to save config: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| var rpcHandlers = map[string]RPCHandler{ | ||||
| 	"ping":                   {Func: rpcPing}, | ||||
| 	"reboot":                 {Func: rpcReboot, Params: []string{"force"}}, | ||||
|  | @ -1083,4 +1102,6 @@ var rpcHandlers = map[string]RPCHandler{ | |||
| 	"setKeyboardLayout":      {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, | ||||
| 	"getKeyboardMacros":      {Func: getKeyboardMacros}, | ||||
| 	"setKeyboardMacros":      {Func: setKeyboardMacros, Params: []string{"params"}}, | ||||
| 	"getLocalLoopbackOnly":   {Func: rpcGetLocalLoopbackOnly}, | ||||
| 	"setLocalLoopbackOnly":   {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, | ||||
| } | ||||
|  |  | |||
|  | @ -1,12 +1,12 @@ | |||
| import { | ||||
|   ExclamationTriangleIcon, | ||||
|   CheckCircleIcon, | ||||
|   ExclamationTriangleIcon, | ||||
|   InformationCircleIcon, | ||||
| } from "@heroicons/react/24/outline"; | ||||
| 
 | ||||
| import { cx } from "@/cva.config"; | ||||
| import { Button } from "@/components/Button"; | ||||
| import Modal from "@/components/Modal"; | ||||
| import { cx } from "@/cva.config"; | ||||
| 
 | ||||
| type Variant = "danger" | "success" | "warning" | "info"; | ||||
| 
 | ||||
|  | @ -14,7 +14,7 @@ interface ConfirmDialogProps { | |||
|   open: boolean; | ||||
|   onClose: () => void; | ||||
|   title: string; | ||||
|   description: string; | ||||
|   description: React.ReactNode; | ||||
|   variant?: Variant; | ||||
|   confirmText?: string; | ||||
|   cancelText?: string | null; | ||||
|  | @ -84,8 +84,8 @@ export function ConfirmDialog({ | |||
|               > | ||||
|                 <Icon aria-hidden="true" className={cx("size-6", iconClass)} /> | ||||
|               </div> | ||||
|               <div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left"> | ||||
|                 <h2 className="text-lg font-bold leading-tight text-black dark:text-white"> | ||||
|               <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> | ||||
|                 <h2 className="text-lg leading-tight font-bold text-black dark:text-white"> | ||||
|                   {title} | ||||
|                 </h2> | ||||
|                 <div className="mt-2 text-sm leading-snug text-slate-600 dark:text-slate-400"> | ||||
|  | @ -111,4 +111,4 @@ export function ConfirmDialog({ | |||
|       </div> | ||||
|     </Modal> | ||||
|   ); | ||||
| } | ||||
| } | ||||
|  | @ -1,17 +1,16 @@ | |||
| 
 | ||||
| import { useCallback, useState, useEffect } from "react"; | ||||
| import { useCallback, useEffect, useState } from "react"; | ||||
| 
 | ||||
| import { GridCard } from "@components/Card"; | ||||
| 
 | ||||
| import { SettingsPageHeader } from "../components/SettingsPageheader"; | ||||
| import Checkbox from "../components/Checkbox"; | ||||
| import { useJsonRpc } from "../hooks/useJsonRpc"; | ||||
| import notifications from "../notifications"; | ||||
| import { TextAreaWithLabel } from "../components/TextArea"; | ||||
| import { isOnDevice } from "../main"; | ||||
| import { Button } from "../components/Button"; | ||||
| import Checkbox from "../components/Checkbox"; | ||||
| import { ConfirmDialog } from "../components/ConfirmDialog"; | ||||
| import { SettingsPageHeader } from "../components/SettingsPageheader"; | ||||
| import { TextAreaWithLabel } from "../components/TextArea"; | ||||
| import { useSettingsStore } from "../hooks/stores"; | ||||
| 
 | ||||
| import { useJsonRpc } from "../hooks/useJsonRpc"; | ||||
| import { isOnDevice } from "../main"; | ||||
| import notifications from "../notifications"; | ||||
| 
 | ||||
| import { SettingsItem } from "./devices.$id.settings"; | ||||
| 
 | ||||
|  | @ -22,6 +21,8 @@ export default function SettingsAdvancedRoute() { | |||
|   const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode); | ||||
|   const [devChannel, setDevChannel] = useState(false); | ||||
|   const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false); | ||||
|   const [showLoopbackWarning, setShowLoopbackWarning] = useState(false); | ||||
|   const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false); | ||||
| 
 | ||||
|   const settings = useSettingsStore(); | ||||
| 
 | ||||
|  | @ -46,6 +47,11 @@ export default function SettingsAdvancedRoute() { | |||
|       if ("error" in resp) return; | ||||
|       setDevChannel(resp.result as boolean); | ||||
|     }); | ||||
| 
 | ||||
|     send("getLocalLoopbackOnly", {}, resp => { | ||||
|       if ("error" in resp) return; | ||||
|       setLocalLoopbackOnly(resp.result as boolean); | ||||
|     }); | ||||
|   }, [send, setDeveloperMode]); | ||||
| 
 | ||||
|   const getUsbEmulationState = useCallback(() => { | ||||
|  | @ -110,17 +116,62 @@ export default function SettingsAdvancedRoute() { | |||
|     [send, setDeveloperMode], | ||||
|   ); | ||||
| 
 | ||||
|   const handleDevChannelChange = (enabled: boolean) => { | ||||
|     send("setDevChannelState", { enabled }, resp => { | ||||
|       if ("error" in resp) { | ||||
|         notifications.error( | ||||
|           `Failed to set dev channel state: ${resp.error.data || "Unknown error"}`, | ||||
|         ); | ||||
|         return; | ||||
|   const handleDevChannelChange = useCallback( | ||||
|     (enabled: boolean) => { | ||||
|       send("setDevChannelState", { enabled }, resp => { | ||||
|         if ("error" in resp) { | ||||
|           notifications.error( | ||||
|             `Failed to set dev channel state: ${resp.error.data || "Unknown error"}`, | ||||
|           ); | ||||
|           return; | ||||
|         } | ||||
|         setDevChannel(enabled); | ||||
|       }); | ||||
|     }, | ||||
|     [send, setDevChannel], | ||||
|   ); | ||||
| 
 | ||||
|   const applyLoopbackOnlyMode = useCallback( | ||||
|     (enabled: boolean) => { | ||||
|       send("setLocalLoopbackOnly", { enabled }, resp => { | ||||
|         if ("error" in resp) { | ||||
|           notifications.error( | ||||
|             `Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`, | ||||
|           ); | ||||
|           return; | ||||
|         } | ||||
|         setLocalLoopbackOnly(enabled); | ||||
|         if (enabled) { | ||||
|           notifications.success( | ||||
|             "Loopback-only mode enabled. Restart your device to apply.", | ||||
|           ); | ||||
|         } else { | ||||
|           notifications.success( | ||||
|             "Loopback-only mode disabled. Restart your device to apply.", | ||||
|           ); | ||||
|         } | ||||
|       }); | ||||
|     }, | ||||
|     [send, setLocalLoopbackOnly], | ||||
|   ); | ||||
| 
 | ||||
|   const handleLoopbackOnlyModeChange = useCallback( | ||||
|     (enabled: boolean) => { | ||||
|       // If trying to enable loopback-only mode, show warning first
 | ||||
|       if (enabled) { | ||||
|         setShowLoopbackWarning(true); | ||||
|       } else { | ||||
|         // If disabling, just proceed
 | ||||
|         applyLoopbackOnlyMode(false); | ||||
|       } | ||||
|       setDevChannel(enabled); | ||||
|     }); | ||||
|   }; | ||||
|     }, | ||||
|     [applyLoopbackOnlyMode, setShowLoopbackWarning], | ||||
|   ); | ||||
| 
 | ||||
|   const confirmLoopbackModeEnable = useCallback(() => { | ||||
|     applyLoopbackOnlyMode(true); | ||||
|     setShowLoopbackWarning(false); | ||||
|   }, [applyLoopbackOnlyMode, setShowLoopbackWarning]); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="space-y-4"> | ||||
|  | @ -153,7 +204,7 @@ export default function SettingsAdvancedRoute() { | |||
| 
 | ||||
|         {settings.developerMode && ( | ||||
|           <GridCard> | ||||
|             <div className="flex select-none items-start gap-x-4 p-4"> | ||||
|             <div className="flex items-start gap-x-4 p-4 select-none"> | ||||
|               <svg | ||||
|                 xmlns="http://www.w3.org/2000/svg" | ||||
|                 viewBox="0 0 24 24" | ||||
|  | @ -187,6 +238,16 @@ export default function SettingsAdvancedRoute() { | |||
|           </GridCard> | ||||
|         )} | ||||
| 
 | ||||
|         <SettingsItem | ||||
|           title="Loopback-Only Mode" | ||||
|           description="Restrict web interface access to localhost only (127.0.0.1)" | ||||
|         > | ||||
|           <Checkbox | ||||
|             checked={localLoopbackOnly} | ||||
|             onChange={e => handleLoopbackOnlyModeChange(e.target.checked)} | ||||
|           /> | ||||
|         </SettingsItem> | ||||
| 
 | ||||
|         {isOnDevice && settings.developerMode && ( | ||||
|           <div className="space-y-4"> | ||||
|             <SettingsItem | ||||
|  | @ -261,6 +322,30 @@ export default function SettingsAdvancedRoute() { | |||
|           </> | ||||
|         )} | ||||
|       </div> | ||||
| 
 | ||||
|       <ConfirmDialog | ||||
|         open={showLoopbackWarning} | ||||
|         onClose={() => { | ||||
|           setShowLoopbackWarning(false); | ||||
|         }} | ||||
|         title="Enable Loopback-Only Mode?" | ||||
|         description={ | ||||
|           <> | ||||
|             <p> | ||||
|               WARNING: This will restrict web interface access to localhost (127.0.0.1) | ||||
|               only. | ||||
|             </p> | ||||
|             <p>Before enabling this feature, make sure you have either:</p> | ||||
|             <ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300"> | ||||
|               <li>SSH access configured and tested</li> | ||||
|               <li>Cloud access enabled and working</li> | ||||
|             </ul> | ||||
|           </> | ||||
|         } | ||||
|         variant="warning" | ||||
|         confirmText="I Understand, Enable Anyway" | ||||
|         onConfirm={confirmLoopbackModeEnable} | ||||
|       /> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										20
									
								
								web.go
								
								
								
								
							
							
						
						
									
										20
									
								
								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"` | ||||
| 	LoopbackOnly bool    `json:"loopbackOnly"` | ||||
| } | ||||
| 
 | ||||
| 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.LocalLoopbackOnly { | ||||
| 		bindAddress = "localhost:80" // Loopback only (both IPv4 and IPv6)
 | ||||
| 	} | ||||
| 
 | ||||
| 	logger.Info().Str("bindAddress", bindAddress).Bool("loopbackOnly", config.LocalLoopbackOnly).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(), | ||||
| 		LoopbackOnly: config.LocalLoopbackOnly, | ||||
| 	} | ||||
| 
 | ||||
| 	c.JSON(http.StatusOK, response) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue