From 189b84380b6e9b54166c897f2d5e2592a01dca1e Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Wed, 16 Apr 2025 01:39:23 +0200 Subject: [PATCH] network enhanecment / refactor (#361) * chore(network): improve connectivity check * refactor(network): rewrite network and timesync component * feat(display): show cloud connection status * chore: change logging verbosity * chore(websecure): update log message * fix(ota): validate root certificate when downloading update * feat(ui): add network settings tab * fix(display): cloud connecting animation * fix: golintci issues * feat: add network settings tab * feat(timesync): query servers in parallel * refactor(network): move to internal/network package * feat(timesync): add metrics * refactor(log): move log to internal/logging package * refactor(mdms): move mdns to internal/mdns package * feat(developer): add pprof endpoint * feat(logging): add a simple logging streaming endpoint * fix(mdns): do not start mdns until network is up * feat(network): allow users to update network settings from ui * fix(network): handle errors when net.IPAddr is nil * fix(mdns): scopedLogger SIGSEGV * fix(dhcp): watch directory instead of file to catch fsnotify.Create event * refactor(nbd): move platform-specific code to different files * refactor(native): move platform-specific code to different files * chore: fix linter issues * chore(dev_deploy): allow to override PION_LOG_TRACE --- block_device.go | 28 -- block_device_linux.go | 34 ++ block_device_notlinux.go | 17 + cloud.go | 57 ++- config.go | 56 ++- dev_deploy.sh | 3 +- display.go | 139 +++++- go.mod | 2 + go.sum | 4 + hw.go | 10 + internal/confparser/confparser.go | 381 ++++++++++++++++ internal/confparser/confparser_test.go | 100 +++++ internal/confparser/utils.go | 28 ++ internal/logging/logger.go | 197 +++++++++ internal/logging/pion.go | 63 +++ internal/logging/root.go | 20 + internal/logging/sse.go | 137 ++++++ internal/logging/sse.html | 319 ++++++++++++++ internal/logging/utils.go | 32 ++ internal/mdns/mdns.go | 190 ++++++++ internal/mdns/utils.go | 1 + internal/network/config.go | 110 +++++ internal/network/dhcp.go | 11 + internal/network/hostname.go | 137 ++++++ internal/network/netif.go | 346 +++++++++++++++ internal/network/netif_linux.go | 58 +++ internal/network/netif_notlinux.go | 21 + internal/network/rpc.go | 126 ++++++ internal/network/utils.go | 26 ++ internal/timesync/http.go | 132 ++++++ internal/timesync/metrics.go | 147 +++++++ internal/timesync/ntp.go | 113 +++++ internal/timesync/rtc.go | 26 ++ internal/timesync/rtc_linux.go | 105 +++++ internal/timesync/rtc_notlinux.go | 16 + internal/timesync/timesync.go | 208 +++++++++ internal/udhcpc/options.go | 12 + internal/udhcpc/parser.go | 186 ++++++++ internal/udhcpc/parser_test.go | 74 ++++ internal/udhcpc/proc.go | 212 +++++++++ internal/udhcpc/udhcpc.go | 191 ++++++++ internal/websecure/store.go | 8 +- jsonrpc.go | 4 + log.go | 299 +------------ main.go | 38 +- mdns.go | 29 ++ native.go | 47 +- native_linux.go | 57 +++ native_notlinux.go | 12 + network.go | 288 ++++--------- ntp.go | 197 --------- ota.go | 10 +- resource/jetkvm_native | Bin 1545740 -> 1545928 bytes resource/jetkvm_native.sha256 | 2 +- timesync.go | 53 +++ ui/dev_device.sh | 10 + ui/package-lock.json | 6 + ui/package.json | 1 + ui/public/sse.html | 1 + ui/src/hooks/stores.ts | 93 +++- ui/src/main.tsx | 5 + .../routes/devices.$id.settings.network.tsx | 408 ++++++++++++++++++ ui/src/routes/devices.$id.settings.tsx | 12 + ui/src/routes/devices.$id.tsx | 9 + ui/vite.config.ts | 1 + usb.go | 2 +- usb_mass_storage.go | 7 +- video.go | 2 +- web.go | 78 +++- web_tls.go | 4 +- webrtc.go | 5 +- 71 files changed, 4938 insertions(+), 825 deletions(-) create mode 100644 block_device_linux.go create mode 100644 block_device_notlinux.go create mode 100644 internal/confparser/confparser.go create mode 100644 internal/confparser/confparser_test.go create mode 100644 internal/confparser/utils.go create mode 100644 internal/logging/logger.go create mode 100644 internal/logging/pion.go create mode 100644 internal/logging/root.go create mode 100644 internal/logging/sse.go create mode 100644 internal/logging/sse.html create mode 100644 internal/logging/utils.go create mode 100644 internal/mdns/mdns.go create mode 100644 internal/mdns/utils.go create mode 100644 internal/network/config.go create mode 100644 internal/network/dhcp.go create mode 100644 internal/network/hostname.go create mode 100644 internal/network/netif.go create mode 100644 internal/network/netif_linux.go create mode 100644 internal/network/netif_notlinux.go create mode 100644 internal/network/rpc.go create mode 100644 internal/network/utils.go create mode 100644 internal/timesync/http.go create mode 100644 internal/timesync/metrics.go create mode 100644 internal/timesync/ntp.go create mode 100644 internal/timesync/rtc.go create mode 100644 internal/timesync/rtc_linux.go create mode 100644 internal/timesync/rtc_notlinux.go create mode 100644 internal/timesync/timesync.go create mode 100644 internal/udhcpc/options.go create mode 100644 internal/udhcpc/parser.go create mode 100644 internal/udhcpc/parser_test.go create mode 100644 internal/udhcpc/proc.go create mode 100644 internal/udhcpc/udhcpc.go create mode 100644 mdns.go create mode 100644 native_linux.go create mode 100644 native_notlinux.go delete mode 100644 ntp.go create mode 100644 timesync.go create mode 120000 ui/public/sse.html create mode 100644 ui/src/routes/devices.$id.settings.network.tsx diff --git a/block_device.go b/block_device.go index e4eab80..2274098 100644 --- a/block_device.go +++ b/block_device.go @@ -7,7 +7,6 @@ import ( "os" "time" - "github.com/pojntfx/go-nbd/pkg/client" "github.com/pojntfx/go-nbd/pkg/server" "github.com/rs/zerolog" ) @@ -149,30 +148,3 @@ func (d *NBDDevice) runServerConn() { d.l.Info().Err(err).Msg("nbd server exited") } - -func (d *NBDDevice) runClientConn() { - err := client.Connect(d.clientConn, d.dev, &client.Options{ - ExportName: "jetkvm", - BlockSize: uint32(4 * 1024), - }) - d.l.Info().Err(err).Msg("nbd client exited") -} - -func (d *NBDDevice) Close() { - if d.dev != nil { - err := client.Disconnect(d.dev) - if err != nil { - d.l.Warn().Err(err).Msg("error disconnecting nbd client") - } - _ = d.dev.Close() - } - if d.listener != nil { - _ = d.listener.Close() - } - if d.clientConn != nil { - _ = d.clientConn.Close() - } - if d.serverConn != nil { - _ = d.serverConn.Close() - } -} diff --git a/block_device_linux.go b/block_device_linux.go new file mode 100644 index 0000000..8ca9372 --- /dev/null +++ b/block_device_linux.go @@ -0,0 +1,34 @@ +//go:build linux + +package kvm + +import ( + "github.com/pojntfx/go-nbd/pkg/client" +) + +func (d *NBDDevice) runClientConn() { + err := client.Connect(d.clientConn, d.dev, &client.Options{ + ExportName: "jetkvm", + BlockSize: uint32(4 * 1024), + }) + d.l.Info().Err(err).Msg("nbd client exited") +} + +func (d *NBDDevice) Close() { + if d.dev != nil { + err := client.Disconnect(d.dev) + if err != nil { + d.l.Warn().Err(err).Msg("error disconnecting nbd client") + } + _ = d.dev.Close() + } + if d.listener != nil { + _ = d.listener.Close() + } + if d.clientConn != nil { + _ = d.clientConn.Close() + } + if d.serverConn != nil { + _ = d.serverConn.Close() + } +} diff --git a/block_device_notlinux.go b/block_device_notlinux.go new file mode 100644 index 0000000..b6a9aba --- /dev/null +++ b/block_device_notlinux.go @@ -0,0 +1,17 @@ +//go:build !linux + +package kvm + +import ( + "os" +) + +func (d *NBDDevice) runClientConn() { + d.l.Error().Msg("platform not supported") + os.Exit(1) +} + +func (d *NBDDevice) Close() { + d.l.Error().Msg("platform not supported") + os.Exit(1) +} diff --git a/cloud.go b/cloud.go index fd96c41..fb1998a 100644 --- a/cloud.go +++ b/cloud.go @@ -139,11 +139,40 @@ var ( ) ) +type CloudConnectionState uint8 + +const ( + CloudConnectionStateNotConfigured CloudConnectionState = iota + CloudConnectionStateDisconnected + CloudConnectionStateConnecting + CloudConnectionStateConnected +) + var ( + cloudConnectionState CloudConnectionState = CloudConnectionStateNotConfigured + cloudConnectionStateLock = &sync.Mutex{} + cloudDisconnectChan chan error cloudDisconnectLock = &sync.Mutex{} ) +func setCloudConnectionState(state CloudConnectionState) { + cloudConnectionStateLock.Lock() + defer cloudConnectionStateLock.Unlock() + + if cloudConnectionState == CloudConnectionStateDisconnected && + (config.CloudToken == "" || config.CloudURL == "") { + state = CloudConnectionStateNotConfigured + } + + previousState := cloudConnectionState + cloudConnectionState = state + + go waitCtrlAndRequestDisplayUpdate( + previousState != state, + ) +} + func wsResetMetrics(established bool, sourceType string, source string) { metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1) metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1) @@ -285,6 +314,8 @@ func runWebsocketClient() error { wsURL.Scheme = "wss" } + setCloudConnectionState(CloudConnectionStateConnecting) + header := http.Header{} header.Set("X-Device-ID", GetDeviceID()) header.Set("X-App-Version", builtAppVersion) @@ -302,20 +333,26 @@ func runWebsocketClient() error { c, resp, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ HTTPHeader: header, OnPingReceived: func(ctx context.Context, payload []byte) bool { - scopedLogger.Info().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received") + scopedLogger.Debug().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received") metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc() metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime() + setCloudConnectionState(CloudConnectionStateConnected) + return true }, }) - // get the request id from the response header - connectionId := resp.Header.Get("X-Request-ID") - if connectionId == "" { - connectionId = resp.Header.Get("Cf-Ray") + var connectionId string + if resp != nil { + // get the request id from the response header + connectionId = resp.Header.Get("X-Request-ID") + if connectionId == "" { + connectionId = resp.Header.Get("Cf-Ray") + } } + if connectionId == "" { connectionId = uuid.New().String() scopedLogger.Warn(). @@ -332,6 +369,8 @@ func runWebsocketClient() error { if err != nil { if errors.Is(err, context.Canceled) { cloudLogger.Info().Msg("websocket connection canceled") + setCloudConnectionState(CloudConnectionStateDisconnected) + return nil } return err @@ -450,14 +489,14 @@ func RunWebsocketClient() { } // If the network is not up, well, we can't connect to the cloud. - if !networkState.Up { - cloudLogger.Warn().Msg("waiting for network to be up, will retry in 3 seconds") + if !networkState.IsOnline() { + cloudLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds") time.Sleep(3 * time.Second) continue } // If the system time is not synchronized, the API request will fail anyway because the TLS handshake will fail. - if isTimeSyncNeeded() && !timeSyncSuccess { + if isTimeSyncNeeded() && !timeSync.IsSyncSuccess() { cloudLogger.Warn().Msg("system time is not synced, will retry in 3 seconds") time.Sleep(3 * time.Second) continue @@ -520,6 +559,8 @@ func rpcDeregisterDevice() error { cloudLogger.Info().Msg("device deregistered, disconnecting from cloud") disconnectCloud(fmt.Errorf("device deregistered")) + setCloudConnectionState(CloudConnectionStateNotConfigured) + return nil } diff --git a/config.go b/config.go index cf096a7..23d4c84 100644 --- a/config.go +++ b/config.go @@ -6,6 +6,8 @@ import ( "os" "sync" + "github.com/jetkvm/kvm/internal/logging" + "github.com/jetkvm/kvm/internal/network" "github.com/jetkvm/kvm/internal/usbgadget" ) @@ -73,27 +75,28 @@ 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"` - EdidString string `json:"hdmi_edid_string"` - ActiveExtension string `json:"active_extension"` - 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"` - 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"` + EdidString string `json:"hdmi_edid_string"` + ActiveExtension string `json:"active_extension"` + 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"` } const configPath = "/userdata/kvm_config.json" @@ -121,6 +124,7 @@ var defaultConfig = &Config{ Keyboard: true, MassStorage: true, }, + NetworkConfig: &network.NetworkConfig{}, DefaultLogLevel: "INFO", } @@ -134,7 +138,7 @@ func LoadConfig() { defer configLock.Unlock() if config != nil { - logger.Info().Msg("config already loaded, skipping") + logger.Debug().Msg("config already loaded, skipping") return } @@ -164,9 +168,15 @@ func LoadConfig() { loadedConfig.UsbDevices = defaultConfig.UsbDevices } + if loadedConfig.NetworkConfig == nil { + loadedConfig.NetworkConfig = defaultConfig.NetworkConfig + } + config = &loadedConfig - rootLogger.UpdateLogLevel() + logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel) + + logger.Info().Str("path", configPath).Msg("config loaded") } func SaveConfig() error { diff --git a/dev_deploy.sh b/dev_deploy.sh index 02bbb24..d0ccaf2 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -24,6 +24,7 @@ show_help() { REMOTE_USER="root" REMOTE_PATH="/userdata/jetkvm/bin" SKIP_UI_BUILD=false +LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}" # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -91,7 +92,7 @@ cd "${REMOTE_PATH}" chmod +x jetkvm_app_debug # Run the application in the background -PION_LOG_TRACE=jetkvm,cloud,websocket ./jetkvm_app_debug +PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug EOF echo "Deployment complete." diff --git a/display.go b/display.go index cbe9ddd..e2e82e1 100644 --- a/display.go +++ b/display.go @@ -33,50 +33,153 @@ func switchToScreen(screen string) { var displayedTexts = make(map[string]string) +func lvObjSetState(objName string, state string) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": objName, "state": state}) +} + +func lvObjAddFlag(objName string, flag string) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_add_flag", map[string]interface{}{"obj": objName, "flag": flag}) +} + +func lvObjClearFlag(objName string, flag string) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_clear_flag", map[string]interface{}{"obj": objName, "flag": flag}) +} + +func lvObjHide(objName string) (*CtrlResponse, error) { + return lvObjAddFlag(objName, "LV_OBJ_FLAG_HIDDEN") +} + +func lvObjShow(objName string) (*CtrlResponse, error) { + return lvObjClearFlag(objName, "LV_OBJ_FLAG_HIDDEN") +} + +func lvObjSetOpacity(objName string, opacity int) (*CtrlResponse, error) { // nolint:unused + return CallCtrlAction("lv_obj_set_style_opa_layered", map[string]interface{}{"obj": objName, "opa": opacity}) +} + +func lvObjFadeIn(objName string, duration uint32) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_fade_in", map[string]interface{}{"obj": objName, "time": duration}) +} + +func lvObjFadeOut(objName string, duration uint32) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_fade_out", map[string]interface{}{"obj": objName, "time": duration}) +} + +func lvLabelSetText(objName string, text string) (*CtrlResponse, error) { + return CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": text}) +} + +func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) { + return CallCtrlAction("lv_img_set_src", map[string]interface{}{"obj": objName, "src": src}) +} + func updateLabelIfChanged(objName string, newText string) { if newText != "" && newText != displayedTexts[objName] { - _, _ = CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": newText}) + _, _ = lvLabelSetText(objName, newText) displayedTexts[objName] = newText } } func switchToScreenIfDifferent(screenName string) { - displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen") if currentScreen != screenName { + displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen") switchToScreen(screenName) } } +var ( + cloudBlinkLock sync.Mutex = sync.Mutex{} + cloudBlinkStopped bool + cloudBlinkTicker *time.Ticker +) + func updateDisplay() { - updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4) + updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4String()) if usbState == "configured" { updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Connected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_DEFAULT"}) + _, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_DEFAULT") } else { updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Disconnected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_USER_2"}) + _, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_USER_2") } if lastVideoState.Ready { updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Connected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_DEFAULT"}) + _, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_DEFAULT") } else { updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Disconnected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_USER_2"}) + _, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_USER_2") } updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", actionSessions)) - if networkState.Up { + + if networkState.IsUp() { switchToScreenIfDifferent("ui_Home_Screen") } else { switchToScreenIfDifferent("ui_No_Network_Screen") } + + if cloudConnectionState == CloudConnectionStateNotConfigured { + _, _ = lvObjHide("ui_Home_Header_Cloud_Status_Icon") + } else { + _, _ = lvObjShow("ui_Home_Header_Cloud_Status_Icon") + } + + switch cloudConnectionState { + case CloudConnectionStateDisconnected: + _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud_disconnected.png") + stopCloudBlink() + case CloudConnectionStateConnecting: + _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") + startCloudBlink() + case CloudConnectionStateConnected: + _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") + stopCloudBlink() + } +} + +func startCloudBlink() { + if cloudBlinkTicker == nil { + cloudBlinkTicker = time.NewTicker(2 * time.Second) + } else { + // do nothing if the blink isn't stopped + if cloudBlinkStopped { + cloudBlinkLock.Lock() + defer cloudBlinkLock.Unlock() + + cloudBlinkStopped = false + cloudBlinkTicker.Reset(2 * time.Second) + } + } + + go func() { + for range cloudBlinkTicker.C { + if cloudConnectionState != CloudConnectionStateConnecting { + continue + } + _, _ = lvObjFadeOut("ui_Home_Header_Cloud_Status_Icon", 1000) + time.Sleep(1000 * time.Millisecond) + _, _ = lvObjFadeIn("ui_Home_Header_Cloud_Status_Icon", 1000) + time.Sleep(1000 * time.Millisecond) + } + }() +} + +func stopCloudBlink() { + if cloudBlinkTicker != nil { + cloudBlinkTicker.Stop() + } + + cloudBlinkLock.Lock() + defer cloudBlinkLock.Unlock() + cloudBlinkStopped = true } var ( displayInited = false displayUpdateLock = sync.Mutex{} + waitDisplayUpdate = sync.Mutex{} ) -func requestDisplayUpdate() { +func requestDisplayUpdate(shouldWakeDisplay bool) { displayUpdateLock.Lock() defer displayUpdateLock.Unlock() @@ -85,16 +188,26 @@ func requestDisplayUpdate() { return } go func() { - wakeDisplay(false) - displayLogger.Info().Msg("display updating") + if shouldWakeDisplay { + wakeDisplay(false) + } + displayLogger.Debug().Msg("display updating") //TODO: only run once regardless how many pending updates updateDisplay() }() } +func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool) { + waitDisplayUpdate.Lock() + defer waitDisplayUpdate.Unlock() + + waitCtrlClientConnected() + requestDisplayUpdate(shouldWakeDisplay) +} + func updateStaticContents() { //contents that never change - updateLabelIfChanged("ui_Home_Content_Mac", networkState.MAC) + updateLabelIfChanged("ui_Home_Content_Mac", networkState.MACString()) systemVersion, appVersion, err := GetLocalVersion() if err == nil { updateLabelIfChanged("ui_About_Content_Operating_System_Version_ContentLabel", systemVersion.String()) @@ -265,7 +378,7 @@ func init() { displayLogger.Info().Msg("display inited") startBacklightTickers() wakeDisplay(true) - requestDisplayUpdate() + requestDisplayUpdate(true) }() go watchTsEvents() diff --git a/go.mod b/go.mod index 1311a33..6784a59 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/coder/websocket v1.8.13 github.com/coreos/go-oidc/v3 v3.11.0 github.com/creack/pty v1.1.23 + github.com/fsnotify/fsnotify v1.9.0 github.com/gin-contrib/logger v1.2.5 github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 @@ -44,6 +45,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/guregu/null/v6 v6.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect diff --git a/go.sum b/go.sum index 565c0cc..3ad832a 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM= @@ -54,6 +56,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ= +github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ= github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA= github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ= diff --git a/hw.go b/hw.go index 21bffad..20d88eb 100644 --- a/hw.go +++ b/hw.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "regexp" + "strings" "sync" "time" ) @@ -51,6 +52,15 @@ func GetDeviceID() string { return deviceID } +func GetDefaultHostname() string { + deviceId := GetDeviceID() + if deviceId == "unknown_device_id" { + return "jetkvm" + } + + return fmt.Sprintf("jetkvm-%s", strings.ToLower(deviceId)) +} + func runWatchdog() { file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0) if err != nil { diff --git a/internal/confparser/confparser.go b/internal/confparser/confparser.go new file mode 100644 index 0000000..76102a3 --- /dev/null +++ b/internal/confparser/confparser.go @@ -0,0 +1,381 @@ +package confparser + +import ( + "fmt" + "net" + "reflect" + "slices" + "strconv" + "strings" + + "github.com/guregu/null/v6" + "golang.org/x/net/idna" +) + +type FieldConfig struct { + Name string + Required bool + RequiredIf map[string]interface{} + OneOf []string + ValidateTypes []string + Defaults interface{} + IsEmpty bool + CurrentValue interface{} + TypeString string + Delegated bool + shouldUpdateValue bool +} + +func SetDefaultsAndValidate(config interface{}) error { + return setDefaultsAndValidate(config, true) +} + +func setDefaultsAndValidate(config interface{}, isRoot bool) error { + // first we need to check if the config is a pointer + if reflect.TypeOf(config).Kind() != reflect.Ptr { + return fmt.Errorf("config is not a pointer") + } + + // now iterate over the lease struct and set the values + configType := reflect.TypeOf(config).Elem() + configValue := reflect.ValueOf(config).Elem() + + fields := make(map[string]FieldConfig) + + for i := 0; i < configType.NumField(); i++ { + field := configType.Field(i) + fieldValue := configValue.Field(i) + + defaultValue := field.Tag.Get("default") + + fieldType := field.Type.String() + + fieldConfig := FieldConfig{ + Name: field.Name, + OneOf: splitString(field.Tag.Get("one_of")), + ValidateTypes: splitString(field.Tag.Get("validate_type")), + RequiredIf: make(map[string]interface{}), + CurrentValue: fieldValue.Interface(), + IsEmpty: false, + TypeString: fieldType, + } + + // check if the field is required + required := field.Tag.Get("required") + if required != "" { + requiredBool, _ := strconv.ParseBool(required) + fieldConfig.Required = requiredBool + } + + var canUseOneOff = false + + // use switch to get the type + switch fieldValue.Interface().(type) { + case string, null.String: + if defaultValue != "" { + fieldConfig.Defaults = defaultValue + } + canUseOneOff = true + case []string: + if defaultValue != "" { + fieldConfig.Defaults = strings.Split(defaultValue, ",") + } + canUseOneOff = true + case int, null.Int: + if defaultValue != "" { + defaultValueInt, err := strconv.Atoi(defaultValue) + if err != nil { + return fmt.Errorf("invalid default value for field `%s`: %s", field.Name, defaultValue) + } + + fieldConfig.Defaults = defaultValueInt + } + case bool, null.Bool: + if defaultValue != "" { + defaultValueBool, err := strconv.ParseBool(defaultValue) + if err != nil { + return fmt.Errorf("invalid default value for field `%s`: %s", field.Name, defaultValue) + } + + fieldConfig.Defaults = defaultValueBool + } + default: + if defaultValue != "" { + return fmt.Errorf("field `%s` cannot use default value: unsupported type: %s", field.Name, fieldType) + } + + // check if it's a pointer + if fieldValue.Kind() == reflect.Ptr { + // check if the pointer is nil + if fieldValue.IsNil() { + fieldConfig.IsEmpty = true + } else { + fieldConfig.CurrentValue = fieldValue.Elem().Addr() + fieldConfig.Delegated = true + } + } else { + fieldConfig.Delegated = true + } + } + + // now check if the field is nullable interface + switch fieldValue.Interface().(type) { + case null.String: + if fieldValue.Interface().(null.String).IsZero() { + fieldConfig.IsEmpty = true + } + case null.Int: + if fieldValue.Interface().(null.Int).IsZero() { + fieldConfig.IsEmpty = true + } + case null.Bool: + if fieldValue.Interface().(null.Bool).IsZero() { + fieldConfig.IsEmpty = true + } + case []string: + if len(fieldValue.Interface().([]string)) == 0 { + fieldConfig.IsEmpty = true + } + } + + // now check if the field has required_if + requiredIf := field.Tag.Get("required_if") + if requiredIf != "" { + requiredIfParts := strings.Split(requiredIf, ",") + for _, part := range requiredIfParts { + partVal := strings.SplitN(part, "=", 2) + if len(partVal) != 2 { + return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf) + } + + fieldConfig.RequiredIf[partVal[0]] = partVal[1] + } + } + + // check if the field can use one_of + if !canUseOneOff && len(fieldConfig.OneOf) > 0 { + return fmt.Errorf("field `%s` cannot use one_of: unsupported type: %s", field.Name, fieldType) + } + + fields[field.Name] = fieldConfig + } + + if err := validateFields(config, fields); err != nil { + return err + } + + return nil +} + +func validateFields(config interface{}, fields map[string]FieldConfig) error { + // now we can start to validate the fields + for _, fieldConfig := range fields { + if err := fieldConfig.validate(fields); err != nil { + return err + } + + fieldConfig.populate(config) + } + + return nil +} + +func (f *FieldConfig) validate(fields map[string]FieldConfig) error { + var required bool + var err error + + if required, err = f.validateRequired(fields); err != nil { + return err + } + + // check if the field needs to be updated and set defaults if needed + if err := f.checkIfFieldNeedsUpdate(); err != nil { + return err + } + + // then we can check if the field is one_of + if err := f.validateOneOf(); err != nil { + return err + } + + // and validate the type + if err := f.validateField(); err != nil { + return err + } + + // if the field is delegated, we need to validate the nested field + // but before that, let's check if the field is required + if required && f.Delegated { + if err := setDefaultsAndValidate(f.CurrentValue.(reflect.Value).Interface(), false); err != nil { + return err + } + } + + return nil +} + +func (f *FieldConfig) populate(config interface{}) { + // update the field if it's not empty + if !f.shouldUpdateValue { + return + } + + reflect.ValueOf(config).Elem().FieldByName(f.Name).Set(reflect.ValueOf(f.CurrentValue)) +} + +func (f *FieldConfig) checkIfFieldNeedsUpdate() error { + // populate the field if it's empty and has a default value + if f.IsEmpty && f.Defaults != nil { + switch f.CurrentValue.(type) { + case null.String: + f.CurrentValue = null.StringFrom(f.Defaults.(string)) + case null.Int: + f.CurrentValue = null.IntFrom(int64(f.Defaults.(int))) + case null.Bool: + f.CurrentValue = null.BoolFrom(f.Defaults.(bool)) + case string: + f.CurrentValue = f.Defaults.(string) + case int: + f.CurrentValue = f.Defaults.(int) + case bool: + f.CurrentValue = f.Defaults.(bool) + case []string: + f.CurrentValue = f.Defaults.([]string) + default: + return fmt.Errorf("field `%s` cannot use default value: unsupported type: %s", f.Name, f.TypeString) + } + + f.shouldUpdateValue = true + } + + return nil +} + +func (f *FieldConfig) validateRequired(fields map[string]FieldConfig) (bool, error) { + var required = f.Required + + // if the field is not required, we need to check if it's required_if + if !required && len(f.RequiredIf) > 0 { + for key, value := range f.RequiredIf { + // check if the field's result matches the required_if + // right now we only support string and int + requiredField, ok := fields[key] + if !ok { + return required, fmt.Errorf("required_if field `%s` not found", key) + } + + switch requiredField.CurrentValue.(type) { + case string: + if requiredField.CurrentValue.(string) == value.(string) { + required = true + } + case int: + if requiredField.CurrentValue.(int) == value.(int) { + required = true + } + case null.String: + if !requiredField.CurrentValue.(null.String).IsZero() && + requiredField.CurrentValue.(null.String).String == value.(string) { + required = true + } + case null.Int: + if !requiredField.CurrentValue.(null.Int).IsZero() && + requiredField.CurrentValue.(null.Int).Int64 == value.(int64) { + required = true + } + } + + // if the field is required, we can break the loop + // because we only need one of the required_if fields to be true + if required { + break + } + } + } + + if required && f.IsEmpty { + return false, fmt.Errorf("field `%s` is required", f.Name) + } + + return required, nil +} + +func checkIfSliceContains(slice []string, one_of []string) bool { + for _, oneOf := range one_of { + if slices.Contains(slice, oneOf) { + return true + } + } + + return false +} + +func (f *FieldConfig) validateOneOf() error { + if len(f.OneOf) == 0 { + return nil + } + + var val []string + switch f.CurrentValue.(type) { + case string: + val = []string{f.CurrentValue.(string)} + case null.String: + val = []string{f.CurrentValue.(null.String).String} + case []string: + // let's validate the value here + val = f.CurrentValue.([]string) + default: + return fmt.Errorf("field `%s` cannot use one_of: unsupported type: %s", f.Name, f.TypeString) + } + + if !checkIfSliceContains(val, f.OneOf) { + return fmt.Errorf( + "field `%s` is not one of the allowed values: %s, current value: %s", + f.Name, + strings.Join(f.OneOf, ", "), + strings.Join(val, ", "), + ) + } + + return nil +} + +func (f *FieldConfig) validateField() error { + if len(f.ValidateTypes) == 0 || f.IsEmpty { + return nil + } + + val, err := toString(f.CurrentValue) + if err != nil { + return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err) + } + + if val == "" { + return nil + } + + for _, validateType := range f.ValidateTypes { + switch validateType { + case "ipv4": + if net.ParseIP(val).To4() == nil { + return fmt.Errorf("field `%s` is not a valid IPv4 address: %s", f.Name, val) + } + case "ipv6": + if net.ParseIP(val).To16() == nil { + return fmt.Errorf("field `%s` is not a valid IPv6 address: %s", f.Name, val) + } + case "hwaddr": + if _, err := net.ParseMAC(val); err != nil { + return fmt.Errorf("field `%s` is not a valid MAC address: %s", f.Name, val) + } + case "hostname": + if _, err := idna.Lookup.ToASCII(val); err != nil { + return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val) + } + default: + return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType) + } + } + + return nil +} diff --git a/internal/confparser/confparser_test.go b/internal/confparser/confparser_test.go new file mode 100644 index 0000000..dd5e00a --- /dev/null +++ b/internal/confparser/confparser_test.go @@ -0,0 +1,100 @@ +package confparser + +import ( + "net" + "testing" + "time" + + "github.com/guregu/null/v6" +) + +type testIPv6Address struct { //nolint:unused + Address net.IP `json:"address"` + Prefix net.IPNet `json:"prefix"` + ValidLifetime *time.Time `json:"valid_lifetime"` + PreferredLifetime *time.Time `json:"preferred_lifetime"` + Scope int `json:"scope"` +} + +type testIPv4StaticConfig struct { + Address null.String `json:"address" validate_type:"ipv4" required:"true"` + Netmask null.String `json:"netmask" validate_type:"ipv4" required:"true"` + Gateway null.String `json:"gateway" validate_type:"ipv4" required:"true"` + DNS []string `json:"dns" validate_type:"ipv4" required:"true"` +} + +type testIPv6StaticConfig struct { + Address null.String `json:"address" validate_type:"ipv6" required:"true"` + Prefix null.String `json:"prefix" validate_type:"ipv6" required:"true"` + Gateway null.String `json:"gateway" validate_type:"ipv6" required:"true"` + DNS []string `json:"dns" validate_type:"ipv6" required:"true"` +} +type testNetworkConfig struct { + Hostname null.String `json:"hostname,omitempty"` + Domain null.String `json:"domain,omitempty"` + + IPv4Mode null.String `json:"ipv4_mode" one_of:"dhcp,static,disabled" default:"dhcp"` + IPv4Static *testIPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"` + + IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"` + IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"` + + LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` + LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` + MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` + TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` + TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"` + TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` + TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` +} + +func TestValidateConfig(t *testing.T) { + config := &testNetworkConfig{} + + err := SetDefaultsAndValidate(config) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestValidateIPv4StaticConfigRequired(t *testing.T) { + config := &testNetworkConfig{ + IPv4Static: &testIPv4StaticConfig{ + Address: null.StringFrom("192.168.1.1"), + Gateway: null.StringFrom("192.168.1.1"), + }, + } + + err := SetDefaultsAndValidate(config) + if err == nil { + t.Fatalf("expected error, got nil") + } +} + +func TestValidateIPv4StaticConfigRequiredIf(t *testing.T) { + config := &testNetworkConfig{ + IPv4Mode: null.StringFrom("static"), + } + + err := SetDefaultsAndValidate(config) + if err == nil { + t.Fatalf("expected error, got nil") + } +} + +func TestValidateIPv4StaticConfigValidateType(t *testing.T) { + config := &testNetworkConfig{ + IPv4Static: &testIPv4StaticConfig{ + Address: null.StringFrom("X"), + Netmask: null.StringFrom("255.255.255.0"), + Gateway: null.StringFrom("192.168.1.1"), + DNS: []string{"8.8.8.8", "8.8.4.4"}, + }, + IPv4Mode: null.StringFrom("static"), + } + + err := SetDefaultsAndValidate(config) + if err == nil { + t.Fatalf("expected error, got nil") + } +} diff --git a/internal/confparser/utils.go b/internal/confparser/utils.go new file mode 100644 index 0000000..a46871e --- /dev/null +++ b/internal/confparser/utils.go @@ -0,0 +1,28 @@ +package confparser + +import ( + "fmt" + "reflect" + "strings" + + "github.com/guregu/null/v6" +) + +func splitString(s string) []string { + if s == "" { + return []string{} + } + + return strings.Split(s, ",") +} + +func toString(v interface{}) (string, error) { + switch v := v.(type) { + case string: + return v, nil + case null.String: + return v.String, nil + } + + return "", fmt.Errorf("unsupported type: %s", reflect.TypeOf(v)) +} diff --git a/internal/logging/logger.go b/internal/logging/logger.go new file mode 100644 index 0000000..39156ec --- /dev/null +++ b/internal/logging/logger.go @@ -0,0 +1,197 @@ +package logging + +import ( + "fmt" + "io" + "os" + "strings" + "sync" + "time" + + "github.com/rs/zerolog" +) + +type Logger struct { + l *zerolog.Logger + scopeLoggers map[string]*zerolog.Logger + scopeLevels map[string]zerolog.Level + scopeLevelMutex sync.Mutex + + defaultLogLevelFromEnv zerolog.Level + defaultLogLevelFromConfig zerolog.Level + defaultLogLevel zerolog.Level +} + +const ( + defaultLogLevel = zerolog.ErrorLevel +) + +type logOutput struct { + mu *sync.Mutex +} + +func (w *logOutput) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + // TODO: write to file or syslog + if sseServer != nil { + // use a goroutine to avoid blocking the Write method + go func() { + sseServer.Message <- string(p) + }() + } + return len(p), nil +} + +var ( + consoleLogOutput io.Writer = zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + PartsOrder: []string{"time", "level", "scope", "component", "message"}, + FieldsExclude: []string{"scope", "component"}, + FormatPartValueByName: func(value interface{}, name string) string { + val := fmt.Sprintf("%s", value) + if name == "component" { + if value == nil { + return "-" + } + } + return val + }, + } + fileLogOutput io.Writer = &logOutput{mu: &sync.Mutex{}} + defaultLogOutput = zerolog.MultiLevelWriter(consoleLogOutput, fileLogOutput) + + zerologLevels = map[string]zerolog.Level{ + "DISABLE": zerolog.Disabled, + "NOLEVEL": zerolog.NoLevel, + "PANIC": zerolog.PanicLevel, + "FATAL": zerolog.FatalLevel, + "ERROR": zerolog.ErrorLevel, + "WARN": zerolog.WarnLevel, + "INFO": zerolog.InfoLevel, + "DEBUG": zerolog.DebugLevel, + "TRACE": zerolog.TraceLevel, + } +) + +func NewLogger(zerologLogger zerolog.Logger) *Logger { + return &Logger{ + l: &zerologLogger, + scopeLoggers: make(map[string]*zerolog.Logger), + scopeLevels: make(map[string]zerolog.Level), + scopeLevelMutex: sync.Mutex{}, + defaultLogLevelFromEnv: -2, + defaultLogLevelFromConfig: -2, + defaultLogLevel: defaultLogLevel, + } +} + +func (l *Logger) updateLogLevel() { + l.scopeLevelMutex.Lock() + defer l.scopeLevelMutex.Unlock() + + l.scopeLevels = make(map[string]zerolog.Level) + + finalDefaultLogLevel := l.defaultLogLevel + + for name, level := range zerologLevels { + env := os.Getenv(fmt.Sprintf("JETKVM_LOG_%s", name)) + + if env == "" { + env = os.Getenv(fmt.Sprintf("PION_LOG_%s", name)) + } + + if env == "" { + env = os.Getenv(fmt.Sprintf("PIONS_LOG_%s", name)) + } + + if env == "" { + continue + } + + if strings.ToLower(env) == "all" { + l.defaultLogLevelFromEnv = level + + if finalDefaultLogLevel > level { + finalDefaultLogLevel = level + } + + continue + } + + scopes := strings.Split(strings.ToLower(env), ",") + for _, scope := range scopes { + l.scopeLevels[scope] = level + } + } + + l.defaultLogLevel = finalDefaultLogLevel +} + +func (l *Logger) getScopeLoggerLevel(scope string) zerolog.Level { + if l.scopeLevels == nil { + l.updateLogLevel() + } + + var scopeLevel zerolog.Level + if l.defaultLogLevelFromConfig != -2 { + scopeLevel = l.defaultLogLevelFromConfig + } + if l.defaultLogLevelFromEnv != -2 { + scopeLevel = l.defaultLogLevelFromEnv + } + + // if the scope is not in the map, use the default level from the root logger + if level, ok := l.scopeLevels[scope]; ok { + scopeLevel = level + } + + return scopeLevel +} + +func (l *Logger) newScopeLogger(scope string) zerolog.Logger { + scopeLevel := l.getScopeLoggerLevel(scope) + logger := l.l.Level(scopeLevel).With().Str("component", scope).Logger() + + return logger +} + +func (l *Logger) getLogger(scope string) *zerolog.Logger { + logger, ok := l.scopeLoggers[scope] + if !ok || logger == nil { + scopeLogger := l.newScopeLogger(scope) + l.scopeLoggers[scope] = &scopeLogger + } + + return l.scopeLoggers[scope] +} + +func (l *Logger) UpdateLogLevel(configDefaultLogLevel string) { + needUpdate := false + + if configDefaultLogLevel != "" { + if logLevel, ok := zerologLevels[configDefaultLogLevel]; ok { + l.defaultLogLevelFromConfig = logLevel + } else { + l.l.Warn().Str("logLevel", configDefaultLogLevel).Msg("invalid defaultLogLevel from config, using ERROR") + } + + if l.defaultLogLevelFromConfig != l.defaultLogLevel { + needUpdate = true + } + } + + l.updateLogLevel() + + if needUpdate { + for scope, logger := range l.scopeLoggers { + currentLevel := logger.GetLevel() + targetLevel := l.getScopeLoggerLevel(scope) + if currentLevel != targetLevel { + *logger = l.newScopeLogger(scope) + } + } + } +} diff --git a/internal/logging/pion.go b/internal/logging/pion.go new file mode 100644 index 0000000..453b8bc --- /dev/null +++ b/internal/logging/pion.go @@ -0,0 +1,63 @@ +package logging + +import ( + "github.com/pion/logging" + "github.com/rs/zerolog" +) + +type pionLogger struct { + logger *zerolog.Logger +} + +// Print all messages except trace. +func (c pionLogger) Trace(msg string) { + c.logger.Trace().Msg(msg) +} +func (c pionLogger) Tracef(format string, args ...interface{}) { + c.logger.Trace().Msgf(format, args...) +} + +func (c pionLogger) Debug(msg string) { + c.logger.Debug().Msg(msg) +} +func (c pionLogger) Debugf(format string, args ...interface{}) { + c.logger.Debug().Msgf(format, args...) +} +func (c pionLogger) Info(msg string) { + c.logger.Info().Msg(msg) +} +func (c pionLogger) Infof(format string, args ...interface{}) { + c.logger.Info().Msgf(format, args...) +} +func (c pionLogger) Warn(msg string) { + c.logger.Warn().Msg(msg) +} +func (c pionLogger) Warnf(format string, args ...interface{}) { + c.logger.Warn().Msgf(format, args...) +} +func (c pionLogger) Error(msg string) { + c.logger.Error().Msg(msg) +} +func (c pionLogger) Errorf(format string, args ...interface{}) { + c.logger.Error().Msgf(format, args...) +} + +// customLoggerFactory satisfies the interface logging.LoggerFactory +// This allows us to create different loggers per subsystem. So we can +// add custom behavior. +type pionLoggerFactory struct{} + +func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { + logger := rootLogger.getLogger(subsystem).With(). + Str("scope", "pion"). + Str("component", subsystem). + Logger() + + return pionLogger{logger: &logger} +} + +var defaultLoggerFactory = &pionLoggerFactory{} + +func GetPionDefaultLoggerFactory() logging.LoggerFactory { + return defaultLoggerFactory +} diff --git a/internal/logging/root.go b/internal/logging/root.go new file mode 100644 index 0000000..397ca64 --- /dev/null +++ b/internal/logging/root.go @@ -0,0 +1,20 @@ +package logging + +import "github.com/rs/zerolog" + +var ( + rootZerologLogger = zerolog.New(defaultLogOutput).With(). + Str("scope", "jetkvm"). + Timestamp(). + Stack(). + Logger() + rootLogger = NewLogger(rootZerologLogger) +) + +func GetRootLogger() *Logger { + return rootLogger +} + +func GetSubsystemLogger(subsystem string) *zerolog.Logger { + return rootLogger.getLogger(subsystem) +} diff --git a/internal/logging/sse.go b/internal/logging/sse.go new file mode 100644 index 0000000..05e6e9e --- /dev/null +++ b/internal/logging/sse.go @@ -0,0 +1,137 @@ +package logging + +import ( + "embed" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" +) + +//go:embed sse.html +var sseHTML embed.FS + +type sseEvent struct { + Message chan string + NewClients chan chan string + ClosedClients chan chan string + TotalClients map[chan string]bool +} + +// New event messages are broadcast to all registered client connection channels +type sseClientChan chan string + +var ( + sseServer *sseEvent + sseLogger *zerolog.Logger +) + +func init() { + sseServer = newSseServer() + sseLogger = GetSubsystemLogger("sse") +} + +// Initialize event and Start procnteessing requests +func newSseServer() (event *sseEvent) { + event = &sseEvent{ + Message: make(chan string), + NewClients: make(chan chan string), + ClosedClients: make(chan chan string), + TotalClients: make(map[chan string]bool), + } + + go event.listen() + + return +} + +// It Listens all incoming requests from clients. +// Handles addition and removal of clients and broadcast messages to clients. +func (stream *sseEvent) listen() { + for { + select { + // Add new available client + case client := <-stream.NewClients: + stream.TotalClients[client] = true + sseLogger.Info(). + Int("total_clients", len(stream.TotalClients)). + Msg("new client connected") + + // Remove closed client + case client := <-stream.ClosedClients: + delete(stream.TotalClients, client) + close(client) + sseLogger.Info().Int("total_clients", len(stream.TotalClients)).Msg("client disconnected") + + // Broadcast message to client + case eventMsg := <-stream.Message: + for clientMessageChan := range stream.TotalClients { + select { + case clientMessageChan <- eventMsg: + // Message sent successfully + default: + // Failed to send, dropping message + } + } + } + } +} + +func (stream *sseEvent) serveHTTP() gin.HandlerFunc { + return func(c *gin.Context) { + clientChan := make(sseClientChan) + stream.NewClients <- clientChan + + go func() { + <-c.Writer.CloseNotify() + + for range clientChan { + } + + stream.ClosedClients <- clientChan + }() + + c.Set("clientChan", clientChan) + c.Next() + } +} + +func sseHeadersMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.Method == "GET" && c.NegotiateFormat(gin.MIMEHTML) == gin.MIMEHTML { + c.FileFromFS("/sse.html", http.FS(sseHTML)) + c.Status(http.StatusOK) + c.Abort() + return + } + + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("Transfer-Encoding", "chunked") + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Next() + } +} + +func AttachSSEHandler(router *gin.RouterGroup) { + router.StaticFS("/log-stream", http.FS(sseHTML)) + router.GET("/log-stream", sseHeadersMiddleware(), sseServer.serveHTTP(), func(c *gin.Context) { + v, ok := c.Get("clientChan") + if !ok { + return + } + clientChan, ok := v.(sseClientChan) + if !ok { + return + } + c.Stream(func(w io.Writer) bool { + if msg, ok := <-clientChan; ok { + c.SSEvent("message", msg) + return true + } + return false + }) + }) +} diff --git a/internal/logging/sse.html b/internal/logging/sse.html new file mode 100644 index 0000000..192b464 --- /dev/null +++ b/internal/logging/sse.html @@ -0,0 +1,319 @@ + + + + + + Server Sent Event + + + + +
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/internal/logging/utils.go b/internal/logging/utils.go new file mode 100644 index 0000000..e622d96 --- /dev/null +++ b/internal/logging/utils.go @@ -0,0 +1,32 @@ +package logging + +import ( + "fmt" + "os" + + "github.com/rs/zerolog" +) + +var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) + +func GetDefaultLogger() *zerolog.Logger { + return &defaultLogger +} + +func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error { + // TODO: move rootLogger to logging package + if l == nil { + l = &defaultLogger + } + + l.Error().Err(err).Msgf(format, args...) + + if err == nil { + return fmt.Errorf(format, args...) + } + + err_msg := err.Error() + ": %v" + err_args := append(args, err) + + return fmt.Errorf(err_msg, err_args...) +} diff --git a/internal/mdns/mdns.go b/internal/mdns/mdns.go new file mode 100644 index 0000000..b882b93 --- /dev/null +++ b/internal/mdns/mdns.go @@ -0,0 +1,190 @@ +package mdns + +import ( + "fmt" + "net" + "reflect" + "strings" + "sync" + + "github.com/jetkvm/kvm/internal/logging" + pion_mdns "github.com/pion/mdns/v2" + "github.com/rs/zerolog" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +type MDNS struct { + conn *pion_mdns.Conn + lock sync.Mutex + l *zerolog.Logger + + localNames []string + listenOptions *MDNSListenOptions +} + +type MDNSListenOptions struct { + IPv4 bool + IPv6 bool +} + +type MDNSOptions struct { + Logger *zerolog.Logger + LocalNames []string + ListenOptions *MDNSListenOptions +} + +const ( + DefaultAddressIPv4 = pion_mdns.DefaultAddressIPv4 + DefaultAddressIPv6 = pion_mdns.DefaultAddressIPv6 +) + +func NewMDNS(opts *MDNSOptions) (*MDNS, error) { + if opts.Logger == nil { + opts.Logger = logging.GetDefaultLogger() + } + + if opts.ListenOptions == nil { + opts.ListenOptions = &MDNSListenOptions{ + IPv4: true, + IPv6: true, + } + } + + return &MDNS{ + l: opts.Logger, + lock: sync.Mutex{}, + localNames: opts.LocalNames, + listenOptions: opts.ListenOptions, + }, nil +} + +func (m *MDNS) start(allowRestart bool) error { + m.lock.Lock() + defer m.lock.Unlock() + + if m.conn != nil { + if !allowRestart { + return fmt.Errorf("mDNS server already running") + } + + m.conn.Close() + } + + if m.listenOptions == nil { + return fmt.Errorf("listen options not set") + } + + if !m.listenOptions.IPv4 && !m.listenOptions.IPv6 { + m.l.Info().Msg("mDNS server disabled") + return nil + } + + var ( + addr4, addr6 *net.UDPAddr + l4, l6 *net.UDPConn + p4 *ipv4.PacketConn + p6 *ipv6.PacketConn + err error + ) + + if m.listenOptions.IPv4 { + addr4, err = net.ResolveUDPAddr("udp4", DefaultAddressIPv4) + if err != nil { + return err + } + + l4, err = net.ListenUDP("udp4", addr4) + if err != nil { + return err + } + + p4 = ipv4.NewPacketConn(l4) + } + + if m.listenOptions.IPv6 { + addr6, err = net.ResolveUDPAddr("udp6", DefaultAddressIPv6) + if err != nil { + return err + } + + l6, err = net.ListenUDP("udp6", addr6) + if err != nil { + return err + } + + p6 = ipv6.NewPacketConn(l6) + } + + scopeLogger := m.l.With(). + Interface("local_names", m.localNames). + Bool("ipv4", m.listenOptions.IPv4). + Bool("ipv6", m.listenOptions.IPv6). + Logger() + + newLocalNames := make([]string, len(m.localNames)) + for i, name := range m.localNames { + newLocalNames[i] = strings.TrimRight(strings.ToLower(name), ".") + if !strings.HasSuffix(newLocalNames[i], ".local") { + newLocalNames[i] = newLocalNames[i] + ".local" + } + } + + mDNSConn, err := pion_mdns.Server(p4, p6, &pion_mdns.Config{ + LocalNames: newLocalNames, + LoggerFactory: logging.GetPionDefaultLoggerFactory(), + }) + + if err != nil { + scopeLogger.Warn().Err(err).Msg("failed to start mDNS server") + return err + } + + m.conn = mDNSConn + scopeLogger.Info().Msg("mDNS server started") + + return nil +} + +func (m *MDNS) Start() error { + return m.start(false) +} + +func (m *MDNS) Restart() error { + return m.start(true) +} + +func (m *MDNS) Stop() error { + m.lock.Lock() + defer m.lock.Unlock() + + if m.conn == nil { + return nil + } + + return m.conn.Close() +} + +func (m *MDNS) SetLocalNames(localNames []string, always bool) error { + if reflect.DeepEqual(m.localNames, localNames) && !always { + return nil + } + + m.localNames = localNames + _ = m.Restart() + + return nil +} + +func (m *MDNS) SetListenOptions(listenOptions *MDNSListenOptions) error { + if m.listenOptions != nil && + m.listenOptions.IPv4 == listenOptions.IPv4 && + m.listenOptions.IPv6 == listenOptions.IPv6 { + return nil + } + + m.listenOptions = listenOptions + _ = m.Restart() + + return nil +} diff --git a/internal/mdns/utils.go b/internal/mdns/utils.go new file mode 100644 index 0000000..7565eee --- /dev/null +++ b/internal/mdns/utils.go @@ -0,0 +1 @@ +package mdns diff --git a/internal/network/config.go b/internal/network/config.go new file mode 100644 index 0000000..74ddf19 --- /dev/null +++ b/internal/network/config.go @@ -0,0 +1,110 @@ +package network + +import ( + "fmt" + "net" + "time" + + "github.com/guregu/null/v6" + "github.com/jetkvm/kvm/internal/mdns" + "golang.org/x/net/idna" +) + +type IPv6Address struct { + Address net.IP `json:"address"` + Prefix net.IPNet `json:"prefix"` + ValidLifetime *time.Time `json:"valid_lifetime"` + PreferredLifetime *time.Time `json:"preferred_lifetime"` + Scope int `json:"scope"` +} + +type IPv4StaticConfig struct { + Address null.String `json:"address,omitempty" validate_type:"ipv4" required:"true"` + Netmask null.String `json:"netmask,omitempty" validate_type:"ipv4" required:"true"` + Gateway null.String `json:"gateway,omitempty" validate_type:"ipv4" required:"true"` + DNS []string `json:"dns,omitempty" validate_type:"ipv4" required:"true"` +} + +type IPv6StaticConfig struct { + Address null.String `json:"address,omitempty" validate_type:"ipv6" required:"true"` + Prefix null.String `json:"prefix,omitempty" validate_type:"ipv6" required:"true"` + Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"` + DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"` +} +type NetworkConfig struct { + Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"` + Domain null.String `json:"domain,omitempty" validate_type:"hostname"` + + IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"` + IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"` + + IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"` + IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"` + + LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` + LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` + MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` + TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` + TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"` + TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` + TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` +} + +func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions { + mode := c.MDNSMode.String + listenOptions := &mdns.MDNSListenOptions{ + IPv4: true, + IPv6: true, + } + + switch mode { + case "ipv4_only": + listenOptions.IPv6 = false + case "ipv6_only": + listenOptions.IPv4 = false + case "disabled": + listenOptions.IPv4 = false + listenOptions.IPv6 = false + } + + return listenOptions +} +func (s *NetworkInterfaceState) GetHostname() string { + hostname := ToValidHostname(s.config.Hostname.String) + + if hostname == "" { + return s.defaultHostname + } + + return hostname +} + +func ToValidDomain(domain string) string { + ascii, err := idna.Lookup.ToASCII(domain) + if err != nil { + return "" + } + + return ascii +} + +func (s *NetworkInterfaceState) GetDomain() string { + domain := ToValidDomain(s.config.Domain.String) + + if domain == "" { + lease := s.dhcpClient.GetLease() + if lease != nil && lease.Domain != "" { + domain = ToValidDomain(lease.Domain) + } + } + + if domain == "" { + return "local" + } + + return domain +} + +func (s *NetworkInterfaceState) GetFQDN() string { + return fmt.Sprintf("%s.%s", s.GetHostname(), s.GetDomain()) +} diff --git a/internal/network/dhcp.go b/internal/network/dhcp.go new file mode 100644 index 0000000..9e173cc --- /dev/null +++ b/internal/network/dhcp.go @@ -0,0 +1,11 @@ +package network + +type DhcpTargetState int + +const ( + DhcpTargetStateDoNothing DhcpTargetState = iota + DhcpTargetStateStart + DhcpTargetStateStop + DhcpTargetStateRenew + DhcpTargetStateRelease +) diff --git a/internal/network/hostname.go b/internal/network/hostname.go new file mode 100644 index 0000000..d75255c --- /dev/null +++ b/internal/network/hostname.go @@ -0,0 +1,137 @@ +package network + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + "sync" + + "golang.org/x/net/idna" +) + +const ( + hostnamePath = "/etc/hostname" + hostsPath = "/etc/hosts" +) + +var ( + hostnameLock sync.Mutex = sync.Mutex{} +) + +func updateEtcHosts(hostname string, fqdn string) error { + // update /etc/hosts + hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive) + if err != nil { + return fmt.Errorf("failed to open %s: %w", hostsPath, err) + } + defer hostsFile.Close() + + // read all lines + if _, err := hostsFile.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("failed to seek %s: %w", hostsPath, err) + } + + lines, err := io.ReadAll(hostsFile) + if err != nil { + return fmt.Errorf("failed to read %s: %w", hostsPath, err) + } + + newLines := []string{} + hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn) + hostLineExists := false + + for _, line := range strings.Split(string(lines), "\n") { + if strings.HasPrefix(line, "127.0.1.1") { + hostLineExists = true + line = hostLine + } + newLines = append(newLines, line) + } + + if !hostLineExists { + newLines = append(newLines, hostLine) + } + + if err := hostsFile.Truncate(0); err != nil { + return fmt.Errorf("failed to truncate %s: %w", hostsPath, err) + } + + if _, err := hostsFile.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("failed to seek %s: %w", hostsPath, err) + } + + if _, err := hostsFile.Write([]byte(strings.Join(newLines, "\n"))); err != nil { + return fmt.Errorf("failed to write %s: %w", hostsPath, err) + } + + return nil +} + +func ToValidHostname(hostname string) string { + ascii, err := idna.Lookup.ToASCII(hostname) + if err != nil { + return "" + } + return ascii +} + +func SetHostname(hostname string, fqdn string) error { + hostnameLock.Lock() + defer hostnameLock.Unlock() + + hostname = ToValidHostname(strings.TrimSpace(hostname)) + fqdn = ToValidHostname(strings.TrimSpace(fqdn)) + + if hostname == "" { + return fmt.Errorf("invalid hostname: %s", hostname) + } + + if fqdn == "" { + fqdn = hostname + } + + // update /etc/hostname + if err := os.WriteFile(hostnamePath, []byte(hostname), 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", hostnamePath, err) + } + + // update /etc/hosts + if err := updateEtcHosts(hostname, fqdn); err != nil { + return fmt.Errorf("failed to update /etc/hosts: %w", err) + } + + // run hostname + if err := exec.Command("hostname", "-F", hostnamePath).Run(); err != nil { + return fmt.Errorf("failed to run hostname: %w", err) + } + + return nil +} + +func (s *NetworkInterfaceState) setHostnameIfNotSame() error { + hostname := s.GetHostname() + currentHostname, _ := os.Hostname() + + fqdn := fmt.Sprintf("%s.%s", hostname, s.GetDomain()) + + if currentHostname == hostname && s.currentFqdn == fqdn && s.currentHostname == hostname { + return nil + } + + scopedLogger := s.l.With().Str("hostname", hostname).Str("fqdn", fqdn).Logger() + + err := SetHostname(hostname, fqdn) + if err != nil { + scopedLogger.Error().Err(err).Msg("failed to set hostname") + return err + } + + s.currentHostname = hostname + s.currentFqdn = fqdn + + scopedLogger.Info().Msg("hostname set") + + return nil +} diff --git a/internal/network/netif.go b/internal/network/netif.go new file mode 100644 index 0000000..c5db806 --- /dev/null +++ b/internal/network/netif.go @@ -0,0 +1,346 @@ +package network + +import ( + "fmt" + "net" + "sync" + + "github.com/jetkvm/kvm/internal/confparser" + "github.com/jetkvm/kvm/internal/logging" + "github.com/jetkvm/kvm/internal/udhcpc" + "github.com/rs/zerolog" + + "github.com/vishvananda/netlink" +) + +type NetworkInterfaceState struct { + interfaceName string + interfaceUp bool + ipv4Addr *net.IP + ipv4Addresses []string + ipv6Addr *net.IP + ipv6Addresses []IPv6Address + ipv6LinkLocal *net.IP + macAddr *net.HardwareAddr + + l *zerolog.Logger + stateLock sync.Mutex + + config *NetworkConfig + dhcpClient *udhcpc.DHCPClient + + defaultHostname string + currentHostname string + currentFqdn string + + onStateChange func(state *NetworkInterfaceState) + onInitialCheck func(state *NetworkInterfaceState) + cbConfigChange func(config *NetworkConfig) + + checked bool +} + +type NetworkInterfaceOptions struct { + InterfaceName string + DhcpPidFile string + Logger *zerolog.Logger + DefaultHostname string + OnStateChange func(state *NetworkInterfaceState) + OnInitialCheck func(state *NetworkInterfaceState) + OnDhcpLeaseChange func(lease *udhcpc.Lease) + OnConfigChange func(config *NetworkConfig) + NetworkConfig *NetworkConfig +} + +func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceState, error) { + if opts.NetworkConfig == nil { + return nil, fmt.Errorf("NetworkConfig can not be nil") + } + + if opts.DefaultHostname == "" { + opts.DefaultHostname = "jetkvm" + } + + err := confparser.SetDefaultsAndValidate(opts.NetworkConfig) + if err != nil { + return nil, err + } + + l := opts.Logger + s := &NetworkInterfaceState{ + interfaceName: opts.InterfaceName, + defaultHostname: opts.DefaultHostname, + stateLock: sync.Mutex{}, + l: l, + onStateChange: opts.OnStateChange, + onInitialCheck: opts.OnInitialCheck, + cbConfigChange: opts.OnConfigChange, + config: opts.NetworkConfig, + } + + // create the dhcp client + dhcpClient := udhcpc.NewDHCPClient(&udhcpc.DHCPClientOptions{ + InterfaceName: opts.InterfaceName, + PidFile: opts.DhcpPidFile, + Logger: l, + OnLeaseChange: func(lease *udhcpc.Lease) { + _, err := s.update() + if err != nil { + opts.Logger.Error().Err(err).Msg("failed to update network state") + return + } + + _ = s.setHostnameIfNotSame() + + opts.OnDhcpLeaseChange(lease) + }, + }) + + s.dhcpClient = dhcpClient + + return s, nil +} + +func (s *NetworkInterfaceState) IsUp() bool { + return s.interfaceUp +} + +func (s *NetworkInterfaceState) HasIPAssigned() bool { + return s.ipv4Addr != nil || s.ipv6Addr != nil +} + +func (s *NetworkInterfaceState) IsOnline() bool { + return s.IsUp() && s.HasIPAssigned() +} + +func (s *NetworkInterfaceState) IPv4() *net.IP { + return s.ipv4Addr +} + +func (s *NetworkInterfaceState) IPv4String() string { + if s.ipv4Addr == nil { + return "..." + } + return s.ipv4Addr.String() +} + +func (s *NetworkInterfaceState) IPv6() *net.IP { + return s.ipv6Addr +} + +func (s *NetworkInterfaceState) IPv6String() string { + if s.ipv6Addr == nil { + return "..." + } + return s.ipv6Addr.String() +} + +func (s *NetworkInterfaceState) MAC() *net.HardwareAddr { + return s.macAddr +} + +func (s *NetworkInterfaceState) MACString() string { + if s.macAddr == nil { + return "" + } + return s.macAddr.String() +} + +func (s *NetworkInterfaceState) update() (DhcpTargetState, error) { + s.stateLock.Lock() + defer s.stateLock.Unlock() + + dhcpTargetState := DhcpTargetStateDoNothing + + iface, err := netlink.LinkByName(s.interfaceName) + if err != nil { + s.l.Error().Err(err).Msg("failed to get interface") + return dhcpTargetState, err + } + + // detect if the interface status changed + var changed bool + attrs := iface.Attrs() + state := attrs.OperState + newInterfaceUp := state == netlink.OperUp + + // check if the interface is coming up + interfaceGoingUp := !s.interfaceUp && newInterfaceUp + interfaceGoingDown := s.interfaceUp && !newInterfaceUp + + if s.interfaceUp != newInterfaceUp { + s.interfaceUp = newInterfaceUp + changed = true + } + + if changed { + if interfaceGoingUp { + s.l.Info().Msg("interface state transitioned to up") + dhcpTargetState = DhcpTargetStateRenew + } else if interfaceGoingDown { + s.l.Info().Msg("interface state transitioned to down") + } + } + + // set the mac address + s.macAddr = &attrs.HardwareAddr + + // get the ip addresses + addrs, err := netlinkAddrs(iface) + if err != nil { + return dhcpTargetState, logging.ErrorfL(s.l, "failed to get ip addresses", err) + } + + var ( + ipv4Addresses = make([]net.IP, 0) + ipv4AddressesString = make([]string, 0) + ipv6Addresses = make([]IPv6Address, 0) + // ipv6AddressesString = make([]string, 0) + ipv6LinkLocal *net.IP + ) + + for _, addr := range addrs { + if addr.IP.To4() != nil { + scopedLogger := s.l.With().Str("ipv4", addr.IP.String()).Logger() + if interfaceGoingDown { + // remove all IPv4 addresses from the interface. + scopedLogger.Info().Msg("state transitioned to down, removing IPv4 address") + err := netlink.AddrDel(iface, &addr) + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to delete address") + } + // notify the DHCP client to release the lease + dhcpTargetState = DhcpTargetStateRelease + continue + } + ipv4Addresses = append(ipv4Addresses, addr.IP) + ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String()) + } else if addr.IP.To16() != nil { + scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger() + // check if it's a link local address + if addr.IP.IsLinkLocalUnicast() { + ipv6LinkLocal = &addr.IP + continue + } + + if !addr.IP.IsGlobalUnicast() { + scopedLogger.Trace().Msg("not a global unicast address, skipping") + continue + } + + if interfaceGoingDown { + scopedLogger.Info().Msg("state transitioned to down, removing IPv6 address") + err := netlink.AddrDel(iface, &addr) + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to delete address") + } + continue + } + ipv6Addresses = append(ipv6Addresses, IPv6Address{ + Address: addr.IP, + Prefix: *addr.IPNet, + ValidLifetime: lifetimeToTime(addr.ValidLft), + PreferredLifetime: lifetimeToTime(addr.PreferedLft), + Scope: addr.Scope, + }) + // ipv6AddressesString = append(ipv6AddressesString, addr.IPNet.String()) + } + } + + if len(ipv4Addresses) > 0 { + // compare the addresses to see if there's a change + if s.ipv4Addr == nil || s.ipv4Addr.String() != ipv4Addresses[0].String() { + scopedLogger := s.l.With().Str("ipv4", ipv4Addresses[0].String()).Logger() + if s.ipv4Addr != nil { + scopedLogger.Info(). + Str("old_ipv4", s.ipv4Addr.String()). + Msg("IPv4 address changed") + } else { + scopedLogger.Info().Msg("IPv4 address found") + } + s.ipv4Addr = &ipv4Addresses[0] + changed = true + } + } + s.ipv4Addresses = ipv4AddressesString + + if ipv6LinkLocal != nil { + if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() { + scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger() + if s.ipv6LinkLocal != nil { + scopedLogger.Info(). + Str("old_ipv6", s.ipv6LinkLocal.String()). + Msg("IPv6 link local address changed") + } else { + scopedLogger.Info().Msg("IPv6 link local address found") + } + s.ipv6LinkLocal = ipv6LinkLocal + changed = true + } + } + s.ipv6Addresses = ipv6Addresses + + if len(ipv6Addresses) > 0 { + // compare the addresses to see if there's a change + if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() { + scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger() + if s.ipv6Addr != nil { + scopedLogger.Info(). + Str("old_ipv6", s.ipv6Addr.String()). + Msg("IPv6 address changed") + } else { + scopedLogger.Info().Msg("IPv6 address found") + } + s.ipv6Addr = &ipv6Addresses[0].Address + changed = true + } + } + + // if it's the initial check, we'll set changed to false + initialCheck := !s.checked + if initialCheck { + s.checked = true + changed = false + if dhcpTargetState == DhcpTargetStateRenew { + // it's the initial check, we'll start the DHCP client + // dhcpTargetState = DhcpTargetStateStart + // TODO: manage DHCP client start/stop + dhcpTargetState = DhcpTargetStateDoNothing + } + } + + if initialCheck { + s.onInitialCheck(s) + } else if changed { + s.onStateChange(s) + } + + return dhcpTargetState, nil +} + +func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error { + dhcpTargetState, err := s.update() + if err != nil { + return logging.ErrorfL(s.l, "failed to update network state", err) + } + + switch dhcpTargetState { + case DhcpTargetStateRenew: + s.l.Info().Msg("renewing DHCP lease") + _ = s.dhcpClient.Renew() + case DhcpTargetStateRelease: + s.l.Info().Msg("releasing DHCP lease") + _ = s.dhcpClient.Release() + case DhcpTargetStateStart: + s.l.Warn().Msg("dhcpTargetStateStart not implemented") + case DhcpTargetStateStop: + s.l.Warn().Msg("dhcpTargetStateStop not implemented") + } + + return nil +} + +func (s *NetworkInterfaceState) onConfigChange(config *NetworkConfig) { + _ = s.setHostnameIfNotSame() + s.cbConfigChange(config) +} diff --git a/internal/network/netif_linux.go b/internal/network/netif_linux.go new file mode 100644 index 0000000..ec057f1 --- /dev/null +++ b/internal/network/netif_linux.go @@ -0,0 +1,58 @@ +//go:build linux + +package network + +import ( + "time" + + "github.com/vishvananda/netlink" + "github.com/vishvananda/netlink/nl" +) + +func (s *NetworkInterfaceState) HandleLinkUpdate(update netlink.LinkUpdate) { + if update.Link.Attrs().Name == s.interfaceName { + s.l.Info().Interface("update", update).Msg("interface link update received") + _ = s.CheckAndUpdateDhcp() + } +} + +func (s *NetworkInterfaceState) Run() error { + updates := make(chan netlink.LinkUpdate) + done := make(chan struct{}) + + if err := netlink.LinkSubscribe(updates, done); err != nil { + s.l.Warn().Err(err).Msg("failed to subscribe to link updates") + return err + } + + _ = s.setHostnameIfNotSame() + + // run the dhcp client + go s.dhcpClient.Run() // nolint:errcheck + + if err := s.CheckAndUpdateDhcp(); err != nil { + return err + } + + go func() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case update := <-updates: + s.HandleLinkUpdate(update) + case <-ticker.C: + _ = s.CheckAndUpdateDhcp() + case <-done: + return + } + } + }() + + return nil +} + +func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) { + return netlink.AddrList(iface, nl.FAMILY_ALL) +} diff --git a/internal/network/netif_notlinux.go b/internal/network/netif_notlinux.go new file mode 100644 index 0000000..d101630 --- /dev/null +++ b/internal/network/netif_notlinux.go @@ -0,0 +1,21 @@ +//go:build !linux + +package network + +import ( + "fmt" + + "github.com/vishvananda/netlink" +) + +func (s *NetworkInterfaceState) HandleLinkUpdate() error { + return fmt.Errorf("not implemented") +} + +func (s *NetworkInterfaceState) Run() error { + return fmt.Errorf("not implemented") +} + +func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/internal/network/rpc.go b/internal/network/rpc.go new file mode 100644 index 0000000..32f34f5 --- /dev/null +++ b/internal/network/rpc.go @@ -0,0 +1,126 @@ +package network + +import ( + "fmt" + "time" + + "github.com/jetkvm/kvm/internal/confparser" + "github.com/jetkvm/kvm/internal/udhcpc" +) + +type RpcIPv6Address struct { + Address string `json:"address"` + ValidLifetime *time.Time `json:"valid_lifetime,omitempty"` + PreferredLifetime *time.Time `json:"preferred_lifetime,omitempty"` + Scope int `json:"scope"` +} + +type RpcNetworkState struct { + InterfaceName string `json:"interface_name"` + MacAddress string `json:"mac_address"` + IPv4 string `json:"ipv4,omitempty"` + IPv6 string `json:"ipv6,omitempty"` + IPv6LinkLocal string `json:"ipv6_link_local,omitempty"` + IPv4Addresses []string `json:"ipv4_addresses,omitempty"` + IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses,omitempty"` + DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"` +} + +type RpcNetworkSettings struct { + NetworkConfig +} + +func (s *NetworkInterfaceState) MacAddress() string { + if s.macAddr == nil { + return "" + } + + return s.macAddr.String() +} + +func (s *NetworkInterfaceState) IPv4Address() string { + if s.ipv4Addr == nil { + return "" + } + + return s.ipv4Addr.String() +} + +func (s *NetworkInterfaceState) IPv6Address() string { + if s.ipv6Addr == nil { + return "" + } + + return s.ipv6Addr.String() +} + +func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string { + if s.ipv6LinkLocal == nil { + return "" + } + + return s.ipv6LinkLocal.String() +} + +func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState { + ipv6Addresses := make([]RpcIPv6Address, 0) + + if s.ipv6Addresses != nil { + for _, addr := range s.ipv6Addresses { + ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{ + Address: addr.Prefix.String(), + ValidLifetime: addr.ValidLifetime, + PreferredLifetime: addr.PreferredLifetime, + Scope: addr.Scope, + }) + } + } + + return RpcNetworkState{ + InterfaceName: s.interfaceName, + MacAddress: s.MacAddress(), + IPv4: s.IPv4Address(), + IPv6: s.IPv6Address(), + IPv6LinkLocal: s.IPv6LinkLocalAddress(), + IPv4Addresses: s.ipv4Addresses, + IPv6Addresses: ipv6Addresses, + DHCPLease: s.dhcpClient.GetLease(), + } +} + +func (s *NetworkInterfaceState) RpcGetNetworkSettings() RpcNetworkSettings { + if s.config == nil { + return RpcNetworkSettings{} + } + + return RpcNetworkSettings{ + NetworkConfig: *s.config, + } +} + +func (s *NetworkInterfaceState) RpcSetNetworkSettings(settings RpcNetworkSettings) error { + currentSettings := s.config + + err := confparser.SetDefaultsAndValidate(&settings.NetworkConfig) + if err != nil { + return err + } + + if IsSame(currentSettings, settings.NetworkConfig) { + // no changes, do nothing + return nil + } + + s.config = &settings.NetworkConfig + s.onConfigChange(s.config) + + return nil +} + +func (s *NetworkInterfaceState) RpcRenewDHCPLease() error { + if s.dhcpClient == nil { + return fmt.Errorf("dhcp client not initialized") + } + + return s.dhcpClient.Renew() +} diff --git a/internal/network/utils.go b/internal/network/utils.go new file mode 100644 index 0000000..6d64332 --- /dev/null +++ b/internal/network/utils.go @@ -0,0 +1,26 @@ +package network + +import ( + "encoding/json" + "time" +) + +func lifetimeToTime(lifetime int) *time.Time { + if lifetime == 0 { + return nil + } + t := time.Now().Add(time.Duration(lifetime) * time.Second) + return &t +} + +func IsSame(a, b interface{}) bool { + aJSON, err := json.Marshal(a) + if err != nil { + return false + } + bJSON, err := json.Marshal(b) + if err != nil { + return false + } + return string(aJSON) == string(bJSON) +} diff --git a/internal/timesync/http.go b/internal/timesync/http.go new file mode 100644 index 0000000..3a51463 --- /dev/null +++ b/internal/timesync/http.go @@ -0,0 +1,132 @@ +package timesync + +import ( + "context" + "errors" + "math/rand" + "net/http" + "strconv" + "time" +) + +var defaultHTTPUrls = []string{ + "http://www.gstatic.com/generate_204", + "http://cp.cloudflare.com/", + "http://edge-http.microsoft.com/captiveportal/generate_204", + // Firefox, Apple, and Microsoft have inconsistent results, so we don't use it + // "http://detectportal.firefox.com/", + // "http://www.apple.com/library/test/success.html", + // "http://www.msftconnecttest.com/connecttest.txt", +} + +func (t *TimeSync) queryAllHttpTime() (now *time.Time) { + chunkSize := 4 + httpUrls := t.httpUrls + + // shuffle the http urls to avoid always querying the same servers + rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] }) + + for i := 0; i < len(httpUrls); i += chunkSize { + chunk := httpUrls[i:min(i+chunkSize, len(httpUrls))] + results := t.queryMultipleHttp(chunk, timeSyncTimeout) + if results != nil { + return results + } + } + + return nil +} + +func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now *time.Time) { + results := make(chan *time.Time, len(urls)) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + for _, url := range urls { + go func(url string) { + scopedLogger := t.l.With(). + Str("http_url", url). + Logger() + + metricHttpRequestCount.WithLabelValues(url).Inc() + metricHttpTotalRequestCount.Inc() + + startTime := time.Now() + now, response, err := queryHttpTime( + ctx, + url, + timeout, + ) + duration := time.Since(startTime) + + metricHttpServerLastRTT.WithLabelValues(url).Set(float64(duration.Milliseconds())) + metricHttpServerRttHistogram.WithLabelValues(url).Observe(float64(duration.Milliseconds())) + + status := 0 + if response != nil { + status = response.StatusCode + } + metricHttpServerInfo.WithLabelValues( + url, + strconv.Itoa(status), + ).Set(1) + + if err == nil { + metricHttpTotalSuccessCount.Inc() + metricHttpSuccessCount.WithLabelValues(url).Inc() + + requestId := response.Header.Get("X-Request-Id") + if requestId != "" { + requestId = response.Header.Get("X-Msedge-Ref") + } + if requestId == "" { + requestId = response.Header.Get("Cf-Ray") + } + scopedLogger.Info(). + Str("time", now.Format(time.RFC3339)). + Int("status", status). + Str("request_id", requestId). + Str("time_taken", duration.String()). + Msg("HTTP server returned time") + + cancel() + results <- now + } else if errors.Is(err, context.Canceled) { + metricHttpCancelCount.WithLabelValues(url).Inc() + metricHttpTotalCancelCount.Inc() + } else { + scopedLogger.Warn(). + Str("error", err.Error()). + Int("status", status). + Msg("failed to query HTTP server") + } + }(url) + } + + return <-results +} + +func queryHttpTime( + ctx context.Context, + url string, + timeout time.Duration, +) (now *time.Time, response *http.Response, err error) { + client := http.Client{ + Timeout: timeout, + } + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, nil, err + } + dateStr := resp.Header.Get("Date") + parsedTime, err := time.Parse(time.RFC1123, dateStr) + if err != nil { + return nil, nil, err + } + return &parsedTime, resp, nil +} diff --git a/internal/timesync/metrics.go b/internal/timesync/metrics.go new file mode 100644 index 0000000..0e28acb --- /dev/null +++ b/internal/timesync/metrics.go @@ -0,0 +1,147 @@ +package timesync + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + metricTimeSyncStatus = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_status", + Help: "The status of the timesync, 1 if successful, 0 if not", + }, + ) + metricTimeSyncCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_count", + Help: "The number of times the timesync has been run", + }, + ) + metricTimeSyncSuccessCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_success_count", + Help: "The number of times the timesync has been successful", + }, + ) + metricRTCUpdateCount = promauto.NewCounter( //nolint:unused + prometheus.CounterOpts{ + Name: "jetkvm_timesync_rtc_update_count", + Help: "The number of times the RTC has been updated", + }, + ) + metricNtpTotalSuccessCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_total_success_count", + Help: "The total number of successful NTP requests", + }, + ) + metricNtpTotalRequestCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_total_request_count", + Help: "The total number of NTP requests sent", + }, + ) + metricNtpSuccessCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_success_count", + Help: "The number of successful NTP requests", + }, + []string{"url"}, + ) + metricNtpRequestCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_request_count", + Help: "The number of NTP requests sent to the server", + }, + []string{"url"}, + ) + metricNtpServerLastRTT = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_ntp_server_last_rtt", + Help: "The last RTT of the NTP server in milliseconds", + }, + []string{"url"}, + ) + metricNtpServerRttHistogram = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "jetkvm_timesync_ntp_server_rtt", + Help: "The histogram of the RTT of the NTP server in milliseconds", + Buckets: []float64{ + 10, 25, 50, 100, 200, 300, 500, 1000, + }, + }, + []string{"url"}, + ) + metricNtpServerInfo = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_ntp_server_info", + Help: "The info of the NTP server", + }, + []string{"url", "reference", "stratum", "precision"}, + ) + + metricHttpTotalSuccessCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_total_success_count", + Help: "The total number of successful HTTP requests", + }, + ) + metricHttpTotalRequestCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_total_request_count", + Help: "The total number of HTTP requests sent", + }, + ) + metricHttpTotalCancelCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_total_cancel_count", + Help: "The total number of HTTP requests cancelled", + }, + ) + metricHttpSuccessCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_success_count", + Help: "The number of successful HTTP requests", + }, + []string{"url"}, + ) + metricHttpRequestCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_request_count", + Help: "The number of HTTP requests sent to the server", + }, + []string{"url"}, + ) + metricHttpCancelCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_cancel_count", + Help: "The number of HTTP requests cancelled", + }, + []string{"url"}, + ) + metricHttpServerLastRTT = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_http_server_last_rtt", + Help: "The last RTT of the HTTP server in milliseconds", + }, + []string{"url"}, + ) + metricHttpServerRttHistogram = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "jetkvm_timesync_http_server_rtt", + Help: "The histogram of the RTT of the HTTP server in milliseconds", + Buckets: []float64{ + 10, 25, 50, 100, 200, 300, 500, 1000, + }, + }, + []string{"url"}, + ) + metricHttpServerInfo = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_http_server_info", + Help: "The info of the HTTP server", + }, + []string{"url", "http_code"}, + ) +) diff --git a/internal/timesync/ntp.go b/internal/timesync/ntp.go new file mode 100644 index 0000000..41656b7 --- /dev/null +++ b/internal/timesync/ntp.go @@ -0,0 +1,113 @@ +package timesync + +import ( + "math/rand/v2" + "strconv" + "time" + + "github.com/beevik/ntp" +) + +var defaultNTPServers = []string{ + "time.apple.com", + "time.aws.com", + "time.windows.com", + "time.google.com", + "162.159.200.123", // time.cloudflare.com + "0.pool.ntp.org", + "1.pool.ntp.org", + "2.pool.ntp.org", + "3.pool.ntp.org", +} + +func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) { + chunkSize := 4 + ntpServers := t.ntpServers + + // shuffle the ntp servers to avoid always querying the same servers + rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] }) + + for i := 0; i < len(ntpServers); i += chunkSize { + chunk := ntpServers[i:min(i+chunkSize, len(ntpServers))] + now, offset := t.queryMultipleNTP(chunk, timeSyncTimeout) + if now != nil { + return now, offset + } + } + + return nil, nil +} + +type ntpResult struct { + now *time.Time + offset *time.Duration +} + +func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) { + results := make(chan *ntpResult, len(servers)) + for _, server := range servers { + go func(server string) { + scopedLogger := t.l.With(). + Str("server", server). + Logger() + + // increase request count + metricNtpTotalRequestCount.Inc() + metricNtpRequestCount.WithLabelValues(server).Inc() + + // query the server + now, response, err := queryNtpServer(server, timeout) + + // set the last RTT + metricNtpServerLastRTT.WithLabelValues( + server, + ).Set(float64(response.RTT.Milliseconds())) + + // set the RTT histogram + metricNtpServerRttHistogram.WithLabelValues( + server, + ).Observe(float64(response.RTT.Milliseconds())) + + // set the server info + metricNtpServerInfo.WithLabelValues( + server, + response.ReferenceString(), + strconv.Itoa(int(response.Stratum)), + strconv.Itoa(int(response.Precision)), + ).Set(1) + + if err == nil { + // increase success count + metricNtpTotalSuccessCount.Inc() + metricNtpSuccessCount.WithLabelValues(server).Inc() + + scopedLogger.Info(). + Str("time", now.Format(time.RFC3339)). + Str("reference", response.ReferenceString()). + Str("rtt", response.RTT.String()). + Str("clockOffset", response.ClockOffset.String()). + Uint8("stratum", response.Stratum). + Msg("NTP server returned time") + results <- &ntpResult{ + now: now, + offset: &response.ClockOffset, + } + } else { + scopedLogger.Warn(). + Str("error", err.Error()). + Msg("failed to query NTP server") + } + }(server) + } + + result := <-results + return result.now, result.offset +} + +func queryNtpServer(server string, timeout time.Duration) (now *time.Time, response *ntp.Response, err error) { + resp, err := ntp.QueryWithOptions(server, ntp.QueryOptions{Timeout: timeout}) + if err != nil { + return nil, nil, err + } + return &resp.Time, resp, nil +} diff --git a/internal/timesync/rtc.go b/internal/timesync/rtc.go new file mode 100644 index 0000000..92ee485 --- /dev/null +++ b/internal/timesync/rtc.go @@ -0,0 +1,26 @@ +package timesync + +import ( + "fmt" + "os" +) + +var ( + rtcDeviceSearchPaths = []string{ + "/dev/rtc", + "/dev/rtc0", + "/dev/rtc1", + "/dev/misc/rtc", + "/dev/misc/rtc0", + "/dev/misc/rtc1", + } +) + +func getRtcDevicePath() (string, error) { + for _, path := range rtcDeviceSearchPaths { + if _, err := os.Stat(path); err == nil { + return path, nil + } + } + return "", fmt.Errorf("rtc device not found") +} diff --git a/internal/timesync/rtc_linux.go b/internal/timesync/rtc_linux.go new file mode 100644 index 0000000..27e4ec7 --- /dev/null +++ b/internal/timesync/rtc_linux.go @@ -0,0 +1,105 @@ +//go:build linux + +package timesync + +import ( + "fmt" + "os" + "time" + + "golang.org/x/sys/unix" +) + +func TimetoRtcTime(t time.Time) unix.RTCTime { + return unix.RTCTime{ + Sec: int32(t.Second()), + Min: int32(t.Minute()), + Hour: int32(t.Hour()), + Mday: int32(t.Day()), + Mon: int32(t.Month() - 1), + Year: int32(t.Year() - 1900), + Wday: int32(0), + Yday: int32(0), + Isdst: int32(0), + } +} + +func RtcTimetoTime(t unix.RTCTime) time.Time { + return time.Date( + int(t.Year)+1900, + time.Month(t.Mon+1), + int(t.Mday), + int(t.Hour), + int(t.Min), + int(t.Sec), + 0, + time.UTC, + ) +} + +func (t *TimeSync) getRtcDevice() (*os.File, error) { + if t.rtcDevice == nil { + file, err := os.OpenFile(t.rtcDevicePath, os.O_RDWR, 0666) + if err != nil { + return nil, err + } + t.rtcDevice = file + } + return t.rtcDevice, nil +} + +func (t *TimeSync) getRtcDeviceFd() (int, error) { + device, err := t.getRtcDevice() + if err != nil { + return 0, err + } + return int(device.Fd()), nil +} + +// Read implements Read for the Linux RTC +func (t *TimeSync) readRtcTime() (time.Time, error) { + fd, err := t.getRtcDeviceFd() + if err != nil { + return time.Time{}, fmt.Errorf("failed to get RTC device fd: %w", err) + } + + rtcTime, err := unix.IoctlGetRTCTime(fd) + if err != nil { + return time.Time{}, fmt.Errorf("failed to get RTC time: %w", err) + } + + date := RtcTimetoTime(*rtcTime) + + return date, nil +} + +// Set implements Set for the Linux RTC +// ... +// It might be not accurate as the time consumed by the system call is not taken into account +// but it's good enough for our purposes +func (t *TimeSync) setRtcTime(tu time.Time) error { + rt := TimetoRtcTime(tu) + + fd, err := t.getRtcDeviceFd() + if err != nil { + return fmt.Errorf("failed to get RTC device fd: %w", err) + } + + currentRtcTime, err := t.readRtcTime() + if err != nil { + return fmt.Errorf("failed to read RTC time: %w", err) + } + + t.l.Info(). + Interface("rtc_time", tu). + Str("offset", tu.Sub(currentRtcTime).String()). + Msg("set rtc time") + + if err := unix.IoctlSetRTCTime(fd, &rt); err != nil { + return fmt.Errorf("failed to set RTC time: %w", err) + } + + metricRTCUpdateCount.Inc() + + return nil +} diff --git a/internal/timesync/rtc_notlinux.go b/internal/timesync/rtc_notlinux.go new file mode 100644 index 0000000..e3c1b20 --- /dev/null +++ b/internal/timesync/rtc_notlinux.go @@ -0,0 +1,16 @@ +//go:build !linux + +package timesync + +import ( + "errors" + "time" +) + +func (t *TimeSync) readRtcTime() (time.Time, error) { + return time.Now(), nil +} + +func (t *TimeSync) setRtcTime(tu time.Time) error { + return errors.New("not supported") +} diff --git a/internal/timesync/timesync.go b/internal/timesync/timesync.go new file mode 100644 index 0000000..e956cf9 --- /dev/null +++ b/internal/timesync/timesync.go @@ -0,0 +1,208 @@ +package timesync + +import ( + "fmt" + "os" + "os/exec" + "sync" + "time" + + "github.com/jetkvm/kvm/internal/network" + "github.com/rs/zerolog" +) + +const ( + timeSyncRetryStep = 5 * time.Second + timeSyncRetryMaxInt = 1 * time.Minute + timeSyncWaitNetChkInt = 100 * time.Millisecond + timeSyncWaitNetUpInt = 3 * time.Second + timeSyncInterval = 1 * time.Hour + timeSyncTimeout = 2 * time.Second +) + +var ( + timeSyncRetryInterval = 0 * time.Second +) + +type TimeSync struct { + syncLock *sync.Mutex + l *zerolog.Logger + + ntpServers []string + httpUrls []string + networkConfig *network.NetworkConfig + + rtcDevicePath string + rtcDevice *os.File //nolint:unused + rtcLock *sync.Mutex + + syncSuccess bool + + preCheckFunc func() (bool, error) +} + +type TimeSyncOptions struct { + PreCheckFunc func() (bool, error) + Logger *zerolog.Logger + NetworkConfig *network.NetworkConfig +} + +type SyncMode struct { + Ntp bool + Http bool + Ordering []string + NtpUseFallback bool + HttpUseFallback bool +} + +func NewTimeSync(opts *TimeSyncOptions) *TimeSync { + rtcDevice, err := getRtcDevicePath() + if err != nil { + opts.Logger.Error().Err(err).Msg("failed to get RTC device path") + } else { + opts.Logger.Info().Str("path", rtcDevice).Msg("RTC device found") + } + + t := &TimeSync{ + syncLock: &sync.Mutex{}, + l: opts.Logger, + rtcDevicePath: rtcDevice, + rtcLock: &sync.Mutex{}, + preCheckFunc: opts.PreCheckFunc, + ntpServers: defaultNTPServers, + httpUrls: defaultHTTPUrls, + networkConfig: opts.NetworkConfig, + } + + if t.rtcDevicePath != "" { + rtcTime, _ := t.readRtcTime() + t.l.Info().Interface("rtc_time", rtcTime).Msg("read RTC time") + } + + return t +} + +func (t *TimeSync) getSyncMode() SyncMode { + syncMode := SyncMode{ + NtpUseFallback: true, + HttpUseFallback: true, + } + var syncModeString string + + if t.networkConfig != nil { + syncModeString = t.networkConfig.TimeSyncMode.String + if t.networkConfig.TimeSyncDisableFallback.Bool { + syncMode.NtpUseFallback = false + syncMode.HttpUseFallback = false + } + } + + switch syncModeString { + case "ntp_only": + syncMode.Ntp = true + case "http_only": + syncMode.Http = true + default: + syncMode.Ntp = true + syncMode.Http = true + } + + return syncMode +} + +func (t *TimeSync) doTimeSync() { + metricTimeSyncStatus.Set(0) + for { + if ok, err := t.preCheckFunc(); !ok { + if err != nil { + t.l.Error().Err(err).Msg("pre-check failed") + } + time.Sleep(timeSyncWaitNetChkInt) + continue + } + + t.l.Info().Msg("syncing system time") + start := time.Now() + err := t.Sync() + if err != nil { + t.l.Error().Str("error", err.Error()).Msg("failed to sync system time") + + // retry after a delay + timeSyncRetryInterval += timeSyncRetryStep + time.Sleep(timeSyncRetryInterval) + // reset the retry interval if it exceeds the max interval + if timeSyncRetryInterval > timeSyncRetryMaxInt { + timeSyncRetryInterval = 0 + } + + continue + } + t.syncSuccess = true + t.l.Info().Str("now", time.Now().Format(time.RFC3339)). + Str("time_taken", time.Since(start).String()). + Msg("time sync successful") + + metricTimeSyncStatus.Set(1) + + time.Sleep(timeSyncInterval) // after the first sync is done + } +} + +func (t *TimeSync) Sync() error { + var ( + now *time.Time + offset *time.Duration + ) + + syncMode := t.getSyncMode() + + metricTimeSyncCount.Inc() + + if syncMode.Ntp { + now, offset = t.queryNetworkTime() + } + + if syncMode.Http && now == nil { + now = t.queryAllHttpTime() + } + + if now == nil { + return fmt.Errorf("failed to get time from any source") + } + + if offset != nil { + newNow := time.Now().Add(*offset) + now = &newNow + } + + err := t.setSystemTime(*now) + if err != nil { + return fmt.Errorf("failed to set system time: %w", err) + } + + metricTimeSyncSuccessCount.Inc() + + return nil +} + +func (t *TimeSync) IsSyncSuccess() bool { + return t.syncSuccess +} + +func (t *TimeSync) Start() { + go t.doTimeSync() +} + +func (t *TimeSync) setSystemTime(now time.Time) error { + nowStr := now.Format("2006-01-02 15:04:05") + output, err := exec.Command("date", "-s", nowStr).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run date -s: %w, %s", err, string(output)) + } + + if t.rtcDevicePath != "" { + return t.setRtcTime(now) + } + + return nil +} diff --git a/internal/udhcpc/options.go b/internal/udhcpc/options.go new file mode 100644 index 0000000..10c9f75 --- /dev/null +++ b/internal/udhcpc/options.go @@ -0,0 +1,12 @@ +package udhcpc + +func (u *DHCPClient) GetNtpServers() []string { + if u.lease == nil { + return nil + } + servers := make([]string, len(u.lease.NTPServers)) + for i, server := range u.lease.NTPServers { + servers[i] = server.String() + } + return servers +} diff --git a/internal/udhcpc/parser.go b/internal/udhcpc/parser.go new file mode 100644 index 0000000..66c3ba2 --- /dev/null +++ b/internal/udhcpc/parser.go @@ -0,0 +1,186 @@ +package udhcpc + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +type Lease struct { + // from https://udhcp.busybox.net/README.udhcpc + IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP + Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask + Broadcast net.IP `env:"broadcast" json:"broadcast"` // The broadcast address for this network + TTL int `env:"ipttl" json:"ttl,omitempty"` // The TTL to use for this network + MTU int `env:"mtu" json:"mtu,omitempty"` // The MTU to use for this network + HostName string `env:"hostname" json:"hostname,omitempty"` // The assigned hostname + Domain string `env:"domain" json:"domain,omitempty"` // The domain name of the network + BootPNextServer net.IP `env:"siaddr" json:"bootp_next_server,omitempty"` // The bootp next server option + BootPServerName string `env:"sname" json:"bootp_server_name,omitempty"` // The bootp server name option + BootPFile string `env:"boot_file" json:"bootp_file,omitempty"` // The bootp boot file option + Timezone string `env:"timezone" json:"timezone,omitempty"` // Offset in seconds from UTC + Routers []net.IP `env:"router" json:"routers,omitempty"` // A list of routers + DNS []net.IP `env:"dns" json:"dns_servers,omitempty"` // A list of DNS servers + NTPServers []net.IP `env:"ntpsrv" json:"ntp_servers,omitempty"` // A list of NTP servers + LPRServers []net.IP `env:"lprsvr" json:"lpr_servers,omitempty"` // A list of LPR servers + TimeServers []net.IP `env:"timesvr" json:"_time_servers,omitempty"` // A list of time servers (obsolete) + IEN116NameServers []net.IP `env:"namesvr" json:"_name_servers,omitempty"` // A list of IEN 116 name servers (obsolete) + LogServers []net.IP `env:"logsvr" json:"_log_servers,omitempty"` // A list of MIT-LCS UDP log servers (obsolete) + CookieServers []net.IP `env:"cookiesvr" json:"_cookie_servers,omitempty"` // A list of RFC 865 cookie servers (obsolete) + WINSServers []net.IP `env:"wins" json:"_wins_servers,omitempty"` // A list of WINS servers + SwapServer net.IP `env:"swapsvr" json:"_swap_server,omitempty"` // The IP address of the client's swap server + BootSize int `env:"bootsize" json:"bootsize,omitempty"` // The length in 512 octect blocks of the bootfile + RootPath string `env:"rootpath" json:"root_path,omitempty"` // The path name of the client's root disk + LeaseTime time.Duration `env:"lease" json:"lease,omitempty"` // The lease time, in seconds + DHCPType string `env:"dhcptype" json:"dhcp_type,omitempty"` // DHCP message type (safely ignored) + ServerID string `env:"serverid" json:"server_id,omitempty"` // The IP of the server + Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK + TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name + BootFileName string `env:"bootfile" json:"bootfile,omitempty"` // The boot file name + Uptime time.Duration `env:"uptime" json:"uptime,omitempty"` // The uptime of the device when the lease was obtained, in seconds + LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease + isEmpty map[string]bool +} + +func (l *Lease) setIsEmpty(m map[string]bool) { + l.isEmpty = m +} + +func (l *Lease) IsEmpty(key string) bool { + return l.isEmpty[key] +} + +func (l *Lease) ToJSON() string { + json, err := json.Marshal(l) + if err != nil { + return "" + } + return string(json) +} + +func (l *Lease) SetLeaseExpiry() (time.Time, error) { + if l.Uptime == 0 || l.LeaseTime == 0 { + return time.Time{}, fmt.Errorf("uptime or lease time isn't set") + } + + // get the uptime of the device + + file, err := os.Open("/proc/uptime") + if err != nil { + return time.Time{}, fmt.Errorf("failed to open uptime file: %w", err) + } + defer file.Close() + + var uptime time.Duration + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + text := scanner.Text() + parts := strings.Split(text, " ") + uptime, err = time.ParseDuration(parts[0] + "s") + + if err != nil { + return time.Time{}, fmt.Errorf("failed to parse uptime: %w", err) + } + } + + relativeLeaseRemaining := (l.Uptime + l.LeaseTime) - uptime + leaseExpiry := time.Now().Add(relativeLeaseRemaining) + + l.LeaseExpiry = &leaseExpiry + + return leaseExpiry, nil +} + +func UnmarshalDHCPCLease(lease *Lease, str string) error { + // parse the lease file as a map + data := make(map[string]string) + for _, line := range strings.Split(str, "\n") { + line = strings.TrimSpace(line) + // skip empty lines and comments + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + data[key] = value + } + + // now iterate over the lease struct and set the values + leaseType := reflect.TypeOf(lease).Elem() + leaseValue := reflect.ValueOf(lease).Elem() + + valuesParsed := make(map[string]bool) + + for i := 0; i < leaseType.NumField(); i++ { + field := leaseValue.Field(i) + + // get the env tag + key := leaseType.Field(i).Tag.Get("env") + if key == "" { + continue + } + + valuesParsed[key] = false + + // get the value from the data map + value, ok := data[key] + if !ok || value == "" { + continue + } + + switch field.Interface().(type) { + case string: + field.SetString(value) + case int: + val, err := strconv.Atoi(value) + if err != nil { + continue + } + field.SetInt(int64(val)) + case time.Duration: + val, err := time.ParseDuration(value + "s") + if err != nil { + continue + } + field.Set(reflect.ValueOf(val)) + case net.IP: + ip := net.ParseIP(value) + if ip == nil { + continue + } + field.Set(reflect.ValueOf(ip)) + case []net.IP: + val := make([]net.IP, 0) + for _, ipStr := range strings.Fields(value) { + ip := net.ParseIP(ipStr) + if ip == nil { + continue + } + val = append(val, ip) + } + field.Set(reflect.ValueOf(val)) + default: + return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String()) + } + + valuesParsed[key] = true + } + + lease.setIsEmpty(valuesParsed) + + return nil +} diff --git a/internal/udhcpc/parser_test.go b/internal/udhcpc/parser_test.go new file mode 100644 index 0000000..423ab53 --- /dev/null +++ b/internal/udhcpc/parser_test.go @@ -0,0 +1,74 @@ +package udhcpc + +import ( + "testing" + "time" +) + +func TestUnmarshalDHCPCLease(t *testing.T) { + lease := &Lease{} + err := UnmarshalDHCPCLease(lease, ` +# generated @ Mon Jan 4 19:31:53 UTC 2021 +# 19:31:53 up 0 min, 0 users, load average: 0.72, 0.14, 0.04 +# the date might be inaccurate if the clock is not set +ip=192.168.0.240 +siaddr=192.168.0.1 +sname= +boot_file= +subnet=255.255.255.0 +timezone= +router=192.168.0.1 +timesvr= +namesvr= +dns=172.19.53.2 +logsvr= +cookiesvr= +lprsvr= +hostname= +bootsize= +domain= +swapsvr= +rootpath= +ipttl= +mtu= +broadcast= +ntpsrv=162.159.200.123 +wins= +lease=172800 +dhcptype= +serverid=192.168.0.1 +message= +tftp= +bootfile= + `) + if lease.IPAddress.String() != "192.168.0.240" { + t.Fatalf("expected ip to be 192.168.0.240, got %s", lease.IPAddress.String()) + } + if lease.Netmask.String() != "255.255.255.0" { + t.Fatalf("expected netmask to be 255.255.255.0, got %s", lease.Netmask.String()) + } + if len(lease.Routers) != 1 { + t.Fatalf("expected 1 router, got %d", len(lease.Routers)) + } + if lease.Routers[0].String() != "192.168.0.1" { + t.Fatalf("expected router to be 192.168.0.1, got %s", lease.Routers[0].String()) + } + if len(lease.NTPServers) != 1 { + t.Fatalf("expected 1 timeserver, got %d", len(lease.NTPServers)) + } + if lease.NTPServers[0].String() != "162.159.200.123" { + t.Fatalf("expected timeserver to be 162.159.200.123, got %s", lease.NTPServers[0].String()) + } + if len(lease.DNS) != 1 { + t.Fatalf("expected 1 dns, got %d", len(lease.DNS)) + } + if lease.DNS[0].String() != "172.19.53.2" { + t.Fatalf("expected dns to be 172.19.53.2, got %s", lease.DNS[0].String()) + } + if lease.LeaseTime != 172800*time.Second { + t.Fatalf("expected lease time to be 172800 seconds, got %d", lease.LeaseTime) + } + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/udhcpc/proc.go b/internal/udhcpc/proc.go new file mode 100644 index 0000000..69c2ab9 --- /dev/null +++ b/internal/udhcpc/proc.go @@ -0,0 +1,212 @@ +package udhcpc + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" +) + +func readFileNoStat(filename string) ([]byte, error) { + const maxBufferSize = 1024 * 1024 + + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + + reader := io.LimitReader(f, maxBufferSize) + return io.ReadAll(reader) +} + +func toCmdline(path string) ([]string, error) { + data, err := readFileNoStat(path) + if err != nil { + return nil, err + } + + if len(data) < 1 { + return []string{}, nil + } + + return strings.Split(string(bytes.TrimRight(data, "\x00")), "\x00"), nil +} + +func (p *DHCPClient) findUdhcpcProcess() (int, error) { + // read procfs for udhcpc processes + // we do not use procfs.AllProcs() because we want to avoid the overhead of reading the entire procfs + processes, err := os.ReadDir("/proc") + if err != nil { + return 0, err + } + + // iterate over the processes + for _, d := range processes { + // check if file is numeric + pid, err := strconv.Atoi(d.Name()) + if err != nil { + continue + } + + // check if it's a directory + if !d.IsDir() { + continue + } + + cmdline, err := toCmdline(filepath.Join("/proc", d.Name(), "cmdline")) + if err != nil { + continue + } + + if len(cmdline) < 1 { + continue + } + + if cmdline[0] != "udhcpc" { + continue + } + + cmdlineText := strings.Join(cmdline, " ") + + // check if it's a udhcpc process + if strings.Contains(cmdlineText, fmt.Sprintf("-i %s", p.InterfaceName)) { + p.logger.Debug(). + Str("pid", d.Name()). + Interface("cmdline", cmdline). + Msg("found udhcpc process") + return pid, nil + } + } + + return 0, errors.New("udhcpc process not found") +} + +func (c *DHCPClient) getProcessPid() (int, error) { + var pid int + if c.pidFile != "" { + // try to read the pid file + pidHandle, err := os.ReadFile(c.pidFile) + if err != nil { + c.logger.Warn().Err(err). + Str("pidFile", c.pidFile).Msg("failed to read udhcpc pid file") + } + + // if it exists, try to read the pid + if pidHandle != nil { + pidFromFile, err := strconv.Atoi(string(pidHandle)) + if err != nil { + c.logger.Warn().Err(err). + Str("pidFile", c.pidFile).Msg("failed to convert pid file to int") + } + pid = pidFromFile + } + } + + // if the pid is 0, try to find the pid using procfs + if pid == 0 { + newPid, err := c.findUdhcpcProcess() + if err != nil { + return 0, err + } + pid = newPid + } + + return pid, nil +} + +func (c *DHCPClient) getProcess() *os.Process { + pid, err := c.getProcessPid() + if err != nil { + return nil + } + + process, err := os.FindProcess(pid) + if err != nil { + c.logger.Warn().Err(err). + Int("pid", pid).Msg("failed to find process") + return nil + } + + return process +} + +func (c *DHCPClient) GetProcess() *os.Process { + if c.process == nil { + process := c.getProcess() + if process == nil { + return nil + } + c.process = process + } + + err := c.process.Signal(syscall.Signal(0)) + if err != nil && errors.Is(err, os.ErrProcessDone) { + oldPid := c.process.Pid + + c.process = nil + c.process = c.getProcess() + if c.process == nil { + c.logger.Error().Msg("failed to find new udhcpc process") + return nil + } + c.logger.Warn(). + Int("oldPid", oldPid). + Int("newPid", c.process.Pid). + Msg("udhcpc process pid changed") + } else if err != nil { + c.logger.Warn().Err(err). + Int("pid", c.process.Pid).Msg("udhcpc process is not running") + } + + return c.process +} + +func (c *DHCPClient) KillProcess() error { + process := c.GetProcess() + if process == nil { + return nil + } + + return process.Kill() +} + +func (c *DHCPClient) ReleaseProcess() error { + process := c.GetProcess() + if process == nil { + return nil + } + + return process.Release() +} + +func (c *DHCPClient) signalProcess(sig syscall.Signal) error { + process := c.GetProcess() + if process == nil { + return nil + } + + s := process.Signal(sig) + if s != nil { + c.logger.Warn().Err(s). + Int("pid", process.Pid). + Str("signal", sig.String()). + Msg("failed to signal udhcpc process") + return s + } + + return nil +} + +func (c *DHCPClient) Renew() error { + return c.signalProcess(syscall.SIGUSR1) +} + +func (c *DHCPClient) Release() error { + return c.signalProcess(syscall.SIGUSR2) +} diff --git a/internal/udhcpc/udhcpc.go b/internal/udhcpc/udhcpc.go new file mode 100644 index 0000000..70ac1b8 --- /dev/null +++ b/internal/udhcpc/udhcpc.go @@ -0,0 +1,191 @@ +package udhcpc + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/rs/zerolog" +) + +const ( + DHCPLeaseFile = "/run/udhcpc.%s.info" + DHCPPidFile = "/run/udhcpc.%s.pid" +) + +type DHCPClient struct { + InterfaceName string + leaseFile string + pidFile string + lease *Lease + logger *zerolog.Logger + process *os.Process + onLeaseChange func(lease *Lease) +} + +type DHCPClientOptions struct { + InterfaceName string + PidFile string + Logger *zerolog.Logger + OnLeaseChange func(lease *Lease) +} + +var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) + +func NewDHCPClient(options *DHCPClientOptions) *DHCPClient { + if options.Logger == nil { + options.Logger = &defaultLogger + } + + l := options.Logger.With().Str("interface", options.InterfaceName).Logger() + return &DHCPClient{ + InterfaceName: options.InterfaceName, + logger: &l, + leaseFile: fmt.Sprintf(DHCPLeaseFile, options.InterfaceName), + pidFile: options.PidFile, + onLeaseChange: options.OnLeaseChange, + } +} + +func (c *DHCPClient) getWatchPaths() []string { + watchPaths := make(map[string]interface{}) + watchPaths[filepath.Dir(c.leaseFile)] = nil + + if c.pidFile != "" { + watchPaths[filepath.Dir(c.pidFile)] = nil + } + + paths := make([]string, 0) + for path := range watchPaths { + paths = append(paths, path) + } + return paths +} + +// Run starts the DHCP client and watches the lease file for changes. +// this isn't a blocking call, and the lease file is reloaded when a change is detected. +func (c *DHCPClient) Run() error { + err := c.loadLeaseFile() + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + defer watcher.Close() + + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + continue + } + if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) { + continue + } + + if event.Name == c.leaseFile { + c.logger.Debug(). + Str("event", event.Op.String()). + Str("path", event.Name). + Msg("udhcpc lease file updated, reloading lease") + _ = c.loadLeaseFile() + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + c.logger.Error().Err(err).Msg("error watching lease file") + } + } + }() + + for _, path := range c.getWatchPaths() { + err = watcher.Add(path) + if err != nil { + c.logger.Error(). + Err(err). + Str("path", path). + Msg("failed to watch directory") + return err + } + } + + // TODO: update udhcpc pid file + // we'll comment this out for now because the pid might change + // process := c.GetProcess() + // if process == nil { + // c.logger.Error().Msg("udhcpc process not found") + // } + + // block the goroutine until the lease file is updated + <-make(chan struct{}) + + return nil +} + +func (c *DHCPClient) loadLeaseFile() error { + file, err := os.ReadFile(c.leaseFile) + if err != nil { + return err + } + + data := string(file) + if data == "" { + c.logger.Debug().Msg("udhcpc lease file is empty") + return nil + } + + lease := &Lease{} + err = UnmarshalDHCPCLease(lease, string(file)) + if err != nil { + return err + } + + isFirstLoad := c.lease == nil + c.lease = lease + + if lease.IPAddress == nil { + c.logger.Info(). + Interface("lease", lease). + Str("data", string(file)). + Msg("udhcpc lease cleared") + return nil + } + + msg := "udhcpc lease updated" + if isFirstLoad { + msg = "udhcpc lease loaded" + } + + leaseExpiry, err := lease.SetLeaseExpiry() + if err != nil { + c.logger.Error().Err(err).Msg("failed to get dhcp lease expiry") + } else { + expiresIn := time.Until(leaseExpiry) + c.logger.Info(). + Interface("expiry", leaseExpiry). + Str("expiresIn", expiresIn.String()). + Msg("current dhcp lease expiry time calculated") + } + + c.onLeaseChange(lease) + + c.logger.Info(). + Str("ip", lease.IPAddress.String()). + Str("leaseTime", lease.LeaseTime.String()). + Interface("data", lease). + Msg(msg) + + return nil +} + +func (c *DHCPClient) GetLease() *Lease { + return c.lease +} diff --git a/internal/websecure/store.go b/internal/websecure/store.go index 69ae3ef..ea7911c 100644 --- a/internal/websecure/store.go +++ b/internal/websecure/store.go @@ -96,7 +96,11 @@ func (s *CertStore) loadCertificate(hostname string) { s.certificates[hostname] = &cert - s.log.Info().Str("hostname", hostname).Msg("Loaded certificate") + if hostname == selfSignerCAMagicName { + s.log.Info().Msg("loaded CA certificate") + } else { + s.log.Info().Str("hostname", hostname).Msg("loaded certificate") + } } // GetCertificate returns the certificate for the given hostname @@ -131,7 +135,7 @@ func (s *CertStore) ValidateAndSaveCertificate(hostname string, cert string, key if !ignoreWarning { return nil, fmt.Errorf("certificate does not match hostname: %w", err) } - s.log.Warn().Err(err).Msg("Certificate does not match hostname") + s.log.Warn().Err(err).Msg("certificate does not match hostname") } } diff --git a/jsonrpc.go b/jsonrpc.go index 248390e..d35f635 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -962,6 +962,10 @@ var rpcHandlers = map[string]RPCHandler{ "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"}}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, diff --git a/log.go b/log.go index ed46852..b353a2c 100644 --- a/log.go +++ b/log.go @@ -1,291 +1,32 @@ package kvm import ( - "fmt" - "io" - "os" - "strings" - "sync" - "time" - - "github.com/pion/logging" + "github.com/jetkvm/kvm/internal/logging" "github.com/rs/zerolog" ) -type Logger struct { - l *zerolog.Logger - scopeLoggers map[string]*zerolog.Logger - scopeLevels map[string]zerolog.Level - scopeLevelMutex sync.Mutex - - defaultLogLevelFromEnv zerolog.Level - defaultLogLevelFromConfig zerolog.Level - defaultLogLevel zerolog.Level -} - -const ( - defaultLogLevel = zerolog.ErrorLevel -) - -type logOutput struct { - mu *sync.Mutex -} - -func (w *logOutput) Write(p []byte) (n int, err error) { - w.mu.Lock() - defer w.mu.Unlock() - - // TODO: write to file or syslog - - return len(p), nil -} - -var ( - consoleLogOutput io.Writer = zerolog.ConsoleWriter{ - Out: os.Stdout, - TimeFormat: time.RFC3339, - PartsOrder: []string{"time", "level", "scope", "component", "message"}, - FieldsExclude: []string{"scope", "component"}, - FormatPartValueByName: func(value interface{}, name string) string { - val := fmt.Sprintf("%s", value) - if name == "component" { - if value == nil { - return "-" - } - } - return val - }, - } - fileLogOutput io.Writer = &logOutput{mu: &sync.Mutex{}} - defaultLogOutput = zerolog.MultiLevelWriter(consoleLogOutput, fileLogOutput) - - zerologLevels = map[string]zerolog.Level{ - "DISABLE": zerolog.Disabled, - "NOLEVEL": zerolog.NoLevel, - "PANIC": zerolog.PanicLevel, - "FATAL": zerolog.FatalLevel, - "ERROR": zerolog.ErrorLevel, - "WARN": zerolog.WarnLevel, - "INFO": zerolog.InfoLevel, - "DEBUG": zerolog.DebugLevel, - "TRACE": zerolog.TraceLevel, - } - - rootZerologLogger = zerolog.New(defaultLogOutput).With(). - Str("scope", "jetkvm"). - Timestamp(). - Stack(). - Logger() - rootLogger = NewLogger(rootZerologLogger) -) - -func NewLogger(zerologLogger zerolog.Logger) *Logger { - return &Logger{ - l: &zerologLogger, - scopeLoggers: make(map[string]*zerolog.Logger), - scopeLevels: make(map[string]zerolog.Level), - scopeLevelMutex: sync.Mutex{}, - defaultLogLevelFromEnv: -2, - defaultLogLevelFromConfig: -2, - defaultLogLevel: defaultLogLevel, - } -} - -func (l *Logger) updateLogLevel() { - l.scopeLevelMutex.Lock() - defer l.scopeLevelMutex.Unlock() - - l.scopeLevels = make(map[string]zerolog.Level) - - finalDefaultLogLevel := l.defaultLogLevel - - for name, level := range zerologLevels { - env := os.Getenv(fmt.Sprintf("JETKVM_LOG_%s", name)) - - if env == "" { - env = os.Getenv(fmt.Sprintf("PION_LOG_%s", name)) - } - - if env == "" { - env = os.Getenv(fmt.Sprintf("PIONS_LOG_%s", name)) - } - - if env == "" { - continue - } - - if strings.ToLower(env) == "all" { - l.defaultLogLevelFromEnv = level - - if finalDefaultLogLevel > level { - finalDefaultLogLevel = level - } - - continue - } - - scopes := strings.Split(strings.ToLower(env), ",") - for _, scope := range scopes { - l.scopeLevels[scope] = level - } - } - - l.defaultLogLevel = finalDefaultLogLevel -} - -func (l *Logger) getScopeLoggerLevel(scope string) zerolog.Level { - if l.scopeLevels == nil { - l.updateLogLevel() - } - - var scopeLevel zerolog.Level - if l.defaultLogLevelFromConfig != -2 { - scopeLevel = l.defaultLogLevelFromConfig - } - if l.defaultLogLevelFromEnv != -2 { - scopeLevel = l.defaultLogLevelFromEnv - } - - // if the scope is not in the map, use the default level from the root logger - if level, ok := l.scopeLevels[scope]; ok { - scopeLevel = level - } - - return scopeLevel -} - -func (l *Logger) newScopeLogger(scope string) zerolog.Logger { - scopeLevel := l.getScopeLoggerLevel(scope) - logger := l.l.Level(scopeLevel).With().Str("component", scope).Logger() - - return logger -} - -func (l *Logger) getLogger(scope string) *zerolog.Logger { - logger, ok := l.scopeLoggers[scope] - if !ok || logger == nil { - scopeLogger := l.newScopeLogger(scope) - l.scopeLoggers[scope] = &scopeLogger - } - - return l.scopeLoggers[scope] -} - -func (l *Logger) UpdateLogLevel() { - needUpdate := false - - if config != nil && config.DefaultLogLevel != "" { - if logLevel, ok := zerologLevels[config.DefaultLogLevel]; ok { - l.defaultLogLevelFromConfig = logLevel - } else { - l.l.Warn().Str("logLevel", config.DefaultLogLevel).Msg("invalid defaultLogLevel from config, using ERROR") - } - - if l.defaultLogLevelFromConfig != l.defaultLogLevel { - needUpdate = true - } - } - - l.updateLogLevel() - - if needUpdate { - for scope, logger := range l.scopeLoggers { - currentLevel := logger.GetLevel() - targetLevel := l.getScopeLoggerLevel(scope) - if currentLevel != targetLevel { - *logger = l.newScopeLogger(scope) - } - } - } -} - func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error { - if l == nil { - l = rootLogger.getLogger("jetkvm") - } - - l.Error().Err(err).Msgf(format, args...) - - if err == nil { - return fmt.Errorf(format, args...) - } - - err_msg := err.Error() + ": %v" - err_args := append(args, err) - - return fmt.Errorf(err_msg, err_args...) + return logging.ErrorfL(l, format, err, args...) } var ( - logger = rootLogger.getLogger("jetkvm") - cloudLogger = rootLogger.getLogger("cloud") - websocketLogger = rootLogger.getLogger("websocket") - webrtcLogger = rootLogger.getLogger("webrtc") - nativeLogger = rootLogger.getLogger("native") - nbdLogger = rootLogger.getLogger("nbd") - ntpLogger = rootLogger.getLogger("ntp") - jsonRpcLogger = rootLogger.getLogger("jsonrpc") - watchdogLogger = rootLogger.getLogger("watchdog") - websecureLogger = rootLogger.getLogger("websecure") - otaLogger = rootLogger.getLogger("ota") - serialLogger = rootLogger.getLogger("serial") - terminalLogger = rootLogger.getLogger("terminal") - displayLogger = rootLogger.getLogger("display") - wolLogger = rootLogger.getLogger("wol") - usbLogger = rootLogger.getLogger("usb") + logger = logging.GetSubsystemLogger("jetkvm") + networkLogger = logging.GetSubsystemLogger("network") + cloudLogger = logging.GetSubsystemLogger("cloud") + websocketLogger = logging.GetSubsystemLogger("websocket") + webrtcLogger = logging.GetSubsystemLogger("webrtc") + nativeLogger = logging.GetSubsystemLogger("native") + nbdLogger = logging.GetSubsystemLogger("nbd") + timesyncLogger = logging.GetSubsystemLogger("timesync") + jsonRpcLogger = logging.GetSubsystemLogger("jsonrpc") + watchdogLogger = logging.GetSubsystemLogger("watchdog") + websecureLogger = logging.GetSubsystemLogger("websecure") + otaLogger = logging.GetSubsystemLogger("ota") + serialLogger = logging.GetSubsystemLogger("serial") + terminalLogger = logging.GetSubsystemLogger("terminal") + displayLogger = logging.GetSubsystemLogger("display") + wolLogger = logging.GetSubsystemLogger("wol") + usbLogger = logging.GetSubsystemLogger("usb") // external components - ginLogger = rootLogger.getLogger("gin") + ginLogger = logging.GetSubsystemLogger("gin") ) - -type pionLogger struct { - logger *zerolog.Logger -} - -// Print all messages except trace. -func (c pionLogger) Trace(msg string) { - c.logger.Trace().Msg(msg) -} -func (c pionLogger) Tracef(format string, args ...interface{}) { - c.logger.Trace().Msgf(format, args...) -} - -func (c pionLogger) Debug(msg string) { - c.logger.Debug().Msg(msg) -} -func (c pionLogger) Debugf(format string, args ...interface{}) { - c.logger.Debug().Msgf(format, args...) -} -func (c pionLogger) Info(msg string) { - c.logger.Info().Msg(msg) -} -func (c pionLogger) Infof(format string, args ...interface{}) { - c.logger.Info().Msgf(format, args...) -} -func (c pionLogger) Warn(msg string) { - c.logger.Warn().Msg(msg) -} -func (c pionLogger) Warnf(format string, args ...interface{}) { - c.logger.Warn().Msgf(format, args...) -} -func (c pionLogger) Error(msg string) { - c.logger.Error().Msg(msg) -} -func (c pionLogger) Errorf(format string, args ...interface{}) { - c.logger.Error().Msgf(format, args...) -} - -// customLoggerFactory satisfies the interface logging.LoggerFactory -// This allows us to create different loggers per subsystem. So we can -// add custom behavior. -type pionLoggerFactory struct{} - -func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { - logger := rootLogger.getLogger(subsystem).With(). - Str("scope", "pion"). - Str("component", subsystem). - Logger() - - return pionLogger{logger: &logger} -} - -var defaultLoggerFactory = &pionLoggerFactory{} diff --git a/main.go b/main.go index 9eab708..25fbb3a 100644 --- a/main.go +++ b/main.go @@ -15,28 +15,54 @@ var appCtx context.Context func Main() { LoadConfig() - logger.Debug().Msg("config loaded") var cancel context.CancelFunc appCtx, cancel = context.WithCancel(context.Background()) defer cancel() - logger.Info().Msg("starting JetKvm") + + systemVersionLocal, appVersionLocal, err := GetLocalVersion() + if err != nil { + logger.Warn().Err(err).Msg("failed to get local version") + } + + logger.Info(). + Interface("system_version", systemVersionLocal). + Interface("app_version", appVersionLocal). + Msg("starting JetKVM") go runWatchdog() go confirmCurrentSystem() http.DefaultClient.Timeout = 1 * time.Minute - err := rootcerts.UpdateDefaultTransport() + err = rootcerts.UpdateDefaultTransport() if err != nil { - logger.Warn().Err(err).Msg("failed to load CA certs") + logger.Warn().Err(err).Msg("failed to load Root CA certificates") + } + logger.Info(). + Int("ca_certs_loaded", len(rootcerts.Certs())). + Msg("loaded Root CA certificates") + + // Initialize network + if err := initNetwork(); err != nil { + logger.Error().Err(err).Msg("failed to initialize network") + os.Exit(1) } - initNetwork() + // Initialize time sync + initTimeSync() + timeSync.Start() - go TimeSyncLoop() + // Initialize mDNS + if err := initMdns(); err != nil { + logger.Error().Err(err).Msg("failed to initialize mDNS") + os.Exit(1) + } + // Initialize native ctrl socket server StartNativeCtrlSocketServer() + + // Initialize native video socket server StartNativeVideoSocketServer() initPrometheus() diff --git a/mdns.go b/mdns.go new file mode 100644 index 0000000..d7a3b55 --- /dev/null +++ b/mdns.go @@ -0,0 +1,29 @@ +package kvm + +import ( + "github.com/jetkvm/kvm/internal/mdns" +) + +var mDNS *mdns.MDNS + +func initMdns() error { + m, err := mdns.NewMDNS(&mdns.MDNSOptions{ + Logger: logger, + LocalNames: []string{ + networkState.GetHostname(), + networkState.GetFQDN(), + }, + ListenOptions: &mdns.MDNSListenOptions{ + IPv4: true, + IPv6: true, + }, + }) + if err != nil { + return err + } + + // do not start the server yet, as we need to wait for the network state to be set + mDNS = m + + return nil +} diff --git a/native.go b/native.go index b61598c..496f580 100644 --- a/native.go +++ b/native.go @@ -8,13 +8,10 @@ import ( "io" "net" "os" - "os/exec" "sync" - "syscall" "time" "github.com/jetkvm/kvm/resource" - "github.com/rs/zerolog" "github.com/pion/webrtc/v4/pkg/media" ) @@ -36,19 +33,6 @@ type CtrlResponse struct { Data json.RawMessage `json:"data,omitempty"` } -type nativeOutput struct { - mu *sync.Mutex - logger *zerolog.Event -} - -func (w *nativeOutput) Write(p []byte) (n int, err error) { - w.mu.Lock() - defer w.mu.Unlock() - - w.logger.Msg(string(p)) - return len(p), nil -} - type EventHandler func(event CtrlResponse) var seq int32 = 1 @@ -262,30 +246,8 @@ func ExtractAndRunNativeBin() error { return fmt.Errorf("failed to make binary executable: %w", err) } // Run the binary in the background - cmd := exec.Command(binaryPath) - - nativeOutputLock := sync.Mutex{} - nativeStdout := &nativeOutput{ - mu: &nativeOutputLock, - logger: nativeLogger.Info().Str("pipe", "stdout"), - } - nativeStderr := &nativeOutput{ - mu: &nativeOutputLock, - logger: nativeLogger.Info().Str("pipe", "stderr"), - } - - // Redirect stdout and stderr to the current process - cmd.Stdout = nativeStdout - cmd.Stderr = nativeStderr - - // Set the process group ID so we can kill the process and its children when this process exits - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - Pdeathsig: syscall.SIGKILL, - } - - // Start the command - if err := cmd.Start(); err != nil { + cmd, err := startNativeBinary(binaryPath) + if err != nil { return fmt.Errorf("failed to start binary: %w", err) } @@ -335,7 +297,10 @@ func ensureBinaryUpdated(destPath string) error { _, err = os.Stat(destPath) if shouldOverwrite(destPath, srcHash) || err != nil { - nativeLogger.Info().Msg("writing jetkvm_native") + nativeLogger.Info(). + Interface("hash", srcHash). + Msg("writing jetkvm_native") + _ = os.Remove(destPath) destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755) if err != nil { diff --git a/native_linux.go b/native_linux.go new file mode 100644 index 0000000..54d2150 --- /dev/null +++ b/native_linux.go @@ -0,0 +1,57 @@ +//go:build linux + +package kvm + +import ( + "fmt" + "os/exec" + "sync" + "syscall" + + "github.com/rs/zerolog" +) + +type nativeOutput struct { + mu *sync.Mutex + logger *zerolog.Event +} + +func (w *nativeOutput) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + w.logger.Msg(string(p)) + return len(p), nil +} + +func startNativeBinary(binaryPath string) (*exec.Cmd, error) { + // Run the binary in the background + cmd := exec.Command(binaryPath) + + nativeOutputLock := sync.Mutex{} + nativeStdout := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stdout"), + } + nativeStderr := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stderr"), + } + + // Redirect stdout and stderr to the current process + cmd.Stdout = nativeStdout + cmd.Stderr = nativeStderr + + // Set the process group ID so we can kill the process and its children when this process exits + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pdeathsig: syscall.SIGKILL, + } + + // Start the command + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start binary: %w", err) + } + + return cmd, nil +} diff --git a/native_notlinux.go b/native_notlinux.go new file mode 100644 index 0000000..df6df74 --- /dev/null +++ b/native_notlinux.go @@ -0,0 +1,12 @@ +//go:build !linux + +package kvm + +import ( + "fmt" + "os/exec" +) + +func startNativeBinary(binaryPath string) (*exec.Cmd, error) { + return nil, fmt.Errorf("not supported") +} diff --git a/network.go b/network.go index 6948d9a..8d9261b 100644 --- a/network.go +++ b/network.go @@ -1,237 +1,107 @@ package kvm import ( - "bytes" "fmt" - "net" - "os" - "strings" - "time" - "os/exec" - - "github.com/hashicorp/go-envparse" - "github.com/pion/mdns/v2" - "golang.org/x/net/ipv4" - "golang.org/x/net/ipv6" - - "github.com/vishvananda/netlink" - "github.com/vishvananda/netlink/nl" + "github.com/jetkvm/kvm/internal/network" + "github.com/jetkvm/kvm/internal/udhcpc" ) -var mDNSConn *mdns.Conn - -var networkState NetworkState - -type NetworkState struct { - Up bool - IPv4 string - IPv6 string - MAC string - - checked bool -} - -type LocalIpInfo struct { - IPv4 string - IPv6 string - MAC string -} - const ( - NetIfName = "eth0" - DHCPLeaseFile = "/run/udhcpc.%s.info" + NetIfName = "eth0" ) -// setDhcpClientState sends signals to udhcpc to change it's current mode -// of operation. Setting active to true will force udhcpc to renew the DHCP lease. -// Setting active to false will put udhcpc into idle mode. -func setDhcpClientState(active bool) { - var signal string - if active { - signal = "-SIGUSR1" - } else { - signal = "-SIGUSR2" - } +var ( + networkState *network.NetworkInterfaceState +) - cmd := exec.Command("/usr/bin/killall", signal, "udhcpc") - if err := cmd.Run(); err != nil { - logger.Warn().Err(err).Msg("network: setDhcpClientState: failed to change udhcpc state") +func networkStateChanged() { + // do not block the main thread + go waitCtrlAndRequestDisplayUpdate(true) + + // always restart mDNS when the network state changes + if mDNS != nil { + _ = mDNS.SetLocalNames([]string{ + networkState.GetHostname(), + networkState.GetFQDN(), + }, true) } } -func checkNetworkState() { - iface, err := netlink.LinkByName(NetIfName) - if err != nil { - logger.Warn().Err(err).Str("interface", NetIfName).Msg("failed to get interface") - return - } +func initNetwork() error { + ensureConfigLoaded() - newState := NetworkState{ - Up: iface.Attrs().OperState == netlink.OperUp, - MAC: iface.Attrs().HardwareAddr.String(), + state, err := network.NewNetworkInterfaceState(&network.NetworkInterfaceOptions{ + DefaultHostname: GetDefaultHostname(), + InterfaceName: NetIfName, + NetworkConfig: config.NetworkConfig, + Logger: networkLogger, + OnStateChange: func(state *network.NetworkInterfaceState) { + networkStateChanged() + }, + OnInitialCheck: func(state *network.NetworkInterfaceState) { + networkStateChanged() + }, + OnDhcpLeaseChange: func(lease *udhcpc.Lease) { + networkStateChanged() - checked: true, - } - - addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL) - if err != nil { - logger.Warn().Err(err).Str("interface", NetIfName).Msg("failed to get addresses") - } - - // If the link is going down, put udhcpc into idle mode. - // If the link is coming back up, activate udhcpc and force it to renew the lease. - if newState.Up != networkState.Up { - setDhcpClientState(newState.Up) - } - - for _, addr := range addrs { - if addr.IP.To4() != nil { - if !newState.Up && networkState.Up { - // If the network is going down, remove all IPv4 addresses from the interface. - logger.Info().Str("address", addr.IP.String()).Msg("network: state transitioned to down, removing IPv4 address") - err := netlink.AddrDel(iface, &addr) - if err != nil { - logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("network: failed to delete address") - } - - newState.IPv4 = "..." - } else { - newState.IPv4 = addr.IP.String() + if currentSession == nil { + return } - } else if addr.IP.To16() != nil && newState.IPv6 == "" { - newState.IPv6 = addr.IP.String() - } - } - if newState != networkState { - logger.Info(). - Interface("newState", newState). - Interface("oldState", networkState). - Msg("network state changed") + writeJSONRPCEvent("networkState", networkState.RpcGetNetworkState(), currentSession) + }, + OnConfigChange: func(networkConfig *network.NetworkConfig) { + config.NetworkConfig = networkConfig + networkStateChanged() - // restart MDNS - _ = startMDNS() - networkState = newState - requestDisplayUpdate() - } -} - -func startMDNS() error { - // If server was previously running, stop it - if mDNSConn != nil { - logger.Info().Msg("stopping mDNS server") - err := mDNSConn.Close() - if err != nil { - logger.Warn().Err(err).Msg("failed to stop mDNS server") - } - } - - // Start a new server - hostname := "jetkvm.local" - - scopedLogger := logger.With().Str("hostname", hostname).Logger() - scopedLogger.Info().Msg("starting mDNS server") - - addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) - if err != nil { - return err - } - - addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) - if err != nil { - return err - } - - l4, err := net.ListenUDP("udp4", addr4) - if err != nil { - return err - } - - l6, err := net.ListenUDP("udp6", addr6) - if err != nil { - return err - } - - mDNSConn, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ - LocalNames: []string{hostname}, //TODO: make it configurable - LoggerFactory: defaultLoggerFactory, + if mDNS != nil { + _ = mDNS.SetListenOptions(networkConfig.GetMDNSMode()) + _ = mDNS.SetLocalNames([]string{ + networkState.GetHostname(), + networkState.GetFQDN(), + }, true) + } + }, }) - if err != nil { - scopedLogger.Warn().Err(err).Msg("failed to start mDNS server") - mDNSConn = nil + + if state == nil { + if err == nil { + return fmt.Errorf("failed to create NetworkInterfaceState") + } return err } - //defer server.Close() + + if err := state.Run(); err != nil { + return err + } + + networkState = state + return nil } -func getNTPServersFromDHCPInfo() ([]string, error) { - buf, err := os.ReadFile(fmt.Sprintf(DHCPLeaseFile, NetIfName)) - if err != nil { - // do not return error if file does not exist - if os.IsNotExist(err) { - return nil, nil - } - return nil, fmt.Errorf("failed to load udhcpc info: %w", err) - } - - // parse udhcpc info - env, err := envparse.Parse(bytes.NewReader(buf)) - if err != nil { - return nil, fmt.Errorf("failed to parse udhcpc info: %w", err) - } - - val, ok := env["ntpsrv"] - if !ok { - return nil, nil - } - - var servers []string - - for _, server := range strings.Fields(val) { - if net.ParseIP(server) == nil { - logger.Info().Str("server", server).Msg("invalid NTP server IP, ignoring") - } - servers = append(servers, server) - } - - return servers, nil +func rpcGetNetworkState() network.RpcNetworkState { + return networkState.RpcGetNetworkState() } -func initNetwork() { - ensureConfigLoaded() - - updates := make(chan netlink.LinkUpdate) - done := make(chan struct{}) - - if err := netlink.LinkSubscribe(updates, done); err != nil { - logger.Warn().Err(err).Msg("failed to subscribe to link updates") - return - } - - go func() { - waitCtrlClientConnected() - checkNetworkState() - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case update := <-updates: - if update.Link.Attrs().Name == NetIfName { - logger.Info().Interface("update", update).Msg("link update") - checkNetworkState() - } - case <-ticker.C: - checkNetworkState() - case <-done: - return - } - } - }() - err := startMDNS() - if err != nil { - logger.Warn().Err(err).Msg("failed to run mDNS") - } +func rpcGetNetworkSettings() network.RpcNetworkSettings { + return networkState.RpcGetNetworkSettings() +} + +func rpcSetNetworkSettings(settings network.RpcNetworkSettings) (*network.RpcNetworkSettings, error) { + s := networkState.RpcSetNetworkSettings(settings) + if s != nil { + return nil, s + } + + if err := SaveConfig(); err != nil { + return nil, err + } + + return &network.RpcNetworkSettings{NetworkConfig: *config.NetworkConfig}, nil +} + +func rpcRenewDHCPLease() error { + return networkState.RpcRenewDHCPLease() } diff --git a/ntp.go b/ntp.go deleted file mode 100644 index a104c56..0000000 --- a/ntp.go +++ /dev/null @@ -1,197 +0,0 @@ -package kvm - -import ( - "fmt" - "net/http" - "os/exec" - "strconv" - "time" - - "github.com/beevik/ntp" -) - -const ( - timeSyncRetryStep = 5 * time.Second - timeSyncRetryMaxInt = 1 * time.Minute - timeSyncWaitNetChkInt = 100 * time.Millisecond - timeSyncWaitNetUpInt = 3 * time.Second - timeSyncInterval = 1 * time.Hour - timeSyncTimeout = 2 * time.Second -) - -var ( - builtTimestamp string - timeSyncRetryInterval = 0 * time.Second - timeSyncSuccess = false - defaultNTPServers = []string{ - "time.cloudflare.com", - "time.apple.com", - } -) - -func isTimeSyncNeeded() bool { - if builtTimestamp == "" { - ntpLogger.Warn().Msg("Built timestamp is not set, time sync is needed") - return true - } - - ts, err := strconv.Atoi(builtTimestamp) - if err != nil { - ntpLogger.Warn().Str("error", err.Error()).Msg("Failed to parse built timestamp") - return true - } - - // builtTimestamp is UNIX timestamp in seconds - builtTime := time.Unix(int64(ts), 0) - now := time.Now() - - ntpLogger.Debug().Str("built_time", builtTime.Format(time.RFC3339)).Str("now", now.Format(time.RFC3339)).Msg("Built time and now") - - if now.Sub(builtTime) < 0 { - ntpLogger.Warn().Msg("System time is behind the built time, time sync is needed") - return true - } - - return false -} - -func TimeSyncLoop() { - for { - if !networkState.checked { - time.Sleep(timeSyncWaitNetChkInt) - continue - } - - if !networkState.Up { - ntpLogger.Info().Msg("Waiting for network to come up") - time.Sleep(timeSyncWaitNetUpInt) - continue - } - - // check if time sync is needed, but do nothing for now - isTimeSyncNeeded() - - ntpLogger.Info().Msg("Syncing system time") - start := time.Now() - err := SyncSystemTime() - if err != nil { - ntpLogger.Error().Str("error", err.Error()).Msg("Failed to sync system time") - - // retry after a delay - timeSyncRetryInterval += timeSyncRetryStep - time.Sleep(timeSyncRetryInterval) - // reset the retry interval if it exceeds the max interval - if timeSyncRetryInterval > timeSyncRetryMaxInt { - timeSyncRetryInterval = 0 - } - - continue - } - timeSyncSuccess = true - ntpLogger.Info().Str("now", time.Now().Format(time.RFC3339)). - Str("time_taken", time.Since(start).String()). - Msg("Time sync successful") - time.Sleep(timeSyncInterval) // after the first sync is done - } -} - -func SyncSystemTime() (err error) { - now, err := queryNetworkTime() - if err != nil { - return fmt.Errorf("failed to query network time: %w", err) - } - err = setSystemTime(*now) - if err != nil { - return fmt.Errorf("failed to set system time: %w", err) - } - return nil -} - -func queryNetworkTime() (*time.Time, error) { - ntpServers, err := getNTPServersFromDHCPInfo() - if err != nil { - ntpLogger.Info().Err(err).Msg("failed to get NTP servers from DHCP info") - } - - if ntpServers == nil { - ntpServers = defaultNTPServers - ntpLogger.Info(). - Interface("ntp_servers", ntpServers). - Msg("Using default NTP servers") - } else { - ntpLogger.Info(). - Interface("ntp_servers", ntpServers). - Msg("Using NTP servers from DHCP") - } - - for _, server := range ntpServers { - now, err := queryNtpServer(server, timeSyncTimeout) - if err == nil { - ntpLogger.Info(). - Str("ntp_server", server). - Str("time", now.Format(time.RFC3339)). - Msg("NTP server returned time") - return now, nil - } else { - ntpLogger.Error(). - Str("ntp_server", server). - Str("error", err.Error()). - Msg("failed to query NTP server") - } - } - - httpUrls := []string{ - "http://apple.com", - "http://cloudflare.com", - } - for _, url := range httpUrls { - now, err := queryHttpTime(url, timeSyncTimeout) - if err == nil { - ntpLogger.Info(). - Str("http_url", url). - Str("time", now.Format(time.RFC3339)). - Msg("HTTP server returned time") - return now, nil - } else { - ntpLogger.Error(). - Str("http_url", url). - Str("error", err.Error()). - Msg("failed to query HTTP server") - } - } - - return nil, ErrorfL(ntpLogger, "failed to query network time, all NTP servers and HTTP servers failed", nil) -} - -func queryNtpServer(server string, timeout time.Duration) (now *time.Time, err error) { - resp, err := ntp.QueryWithOptions(server, ntp.QueryOptions{Timeout: timeout}) - if err != nil { - return nil, err - } - return &resp.Time, nil -} - -func queryHttpTime(url string, timeout time.Duration) (*time.Time, error) { - client := http.Client{ - Timeout: timeout, - } - resp, err := client.Head(url) - if err != nil { - return nil, err - } - dateStr := resp.Header.Get("Date") - now, err := time.Parse(time.RFC1123, dateStr) - if err != nil { - return nil, err - } - return &now, nil -} - -func setSystemTime(now time.Time) error { - nowStr := now.Format("2006-01-02 15:04:05") - output, err := exec.Command("date", "-s", nowStr).CombinedOutput() - if err != nil { - return fmt.Errorf("failed to run date -s: %w, %s", err, string(output)) - } - return nil -} diff --git a/ota.go b/ota.go index a5da772..0559978 100644 --- a/ota.go +++ b/ota.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/sha256" + "crypto/tls" "encoding/hex" "encoding/json" "fmt" @@ -16,6 +17,7 @@ import ( "time" "github.com/Masterminds/semver/v3" + "github.com/gwatts/rootcerts" "github.com/rs/zerolog" ) @@ -127,10 +129,14 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress return fmt.Errorf("error creating request: %w", err) } - // TODO: set a separate timeout for the download but keep the TLS handshake short - // use Transport here will cause CA certificate validation failure so we temporarily removed it client := http.Client{ Timeout: 10 * time.Minute, + Transport: &http.Transport{ + TLSHandshakeTimeout: 30 * time.Second, + TLSClientConfig: &tls.Config{ + RootCAs: rootcerts.ServerCertPool(), + }, + }, } resp, err := client.Do(req) diff --git a/resource/jetkvm_native b/resource/jetkvm_native index 0d0719c796e876a7b4277cacc1fbac0772cf10e7..084ce14970c8ef559f9c2e2bcdfd05d5a8993952 100644 GIT binary patch delta 217848 zcmb5Xdw`AA_dkA~=bRa1%#1VUelar!gTXikQUm3X)rImkfcwuam zJ$35F=$b3yBBxG`^+eq&UPxJIFaJ}FN-gj$2fEmwljD7Eu}2JSm=q^|wyHdV(sHk- zw0z?*kyVsiocETeD#}x<((lQrY>wv`JV)hZRK^4{D!rb;Xpg6~!WUz^q@JA8 z3a`JkJPP>NZT3`&{l_zdN6w!Q@f9~qCcA-{8}F%z0b=q4gk@B|4LqN})P7h_IOURKIc-q$`1hwL0bYm3RMHNR+@bN7kCr)#;tZ$q7rm-d|a)HSbuRvA}X5P zCbBx@*FK(VRagL#=jgXQ;=wM>z1@`!3JezmWxvGj5EFlakvO8y|OKXwM_dDn*X<|l)OuJ>WSlpq|``!Zt)fLA(_`R>Z z;5t(5b3cV0={@m7-4ro?a@4gmT6!u{0>u@cfT*-#H=oCI)P&_!gL%=oC+E(}MzG6E zzge!1bJ~Y_cxZc%Ctbtnm&BhV9`Bgl%j9h3+<78Y2F7g7#Um<1a7cv~%aP7!>GRxhw#O zgyT+w)O17)D%X*FK$9q#bySPeiif@S@fqTwP6@VEN38C2hkbvN$nQKQ&6I~4ag@D! z{`{61;-$_R-kE3J4w%-<7*Yv0N&jRAwF0~^xcAMLez5dk(hvIHg7FFMLYn`7w893^ z%@%{YWaKWd8+L=Q-vn3477uS*5(BuWxZ=TPVsV!OYq$6zCnavv*{}l~9KuL)X&=(i z7Ro~#Zj-j0ZFmLk|BT_GH?ei2p8Ysm+*j0MaKAo(_xV;z*EgkYDr&%g#`tY_sM9vf zT?sNOPt1{HVU5w!sW~FLZIN}8EB%#*^mhyq0~Y2bZv_pd-q@0Y-qZjpz^5Q75XMJvD`e?hVq@DP2eQwhS@z|*#wjwpB zxMHpv)iy>ZDaLn*tDr|+_Q(0EPF-R}*A3m0pMpTuxy~6SmTlfpk;LzU3DFZpL3~y*Yk#IqQ=~iQJ8a_WGe>_{J_`Kf|5ych0k_GU4>8&XeC;38MJznDnWQ;g_(rAF{X0 zMub$pF!GF;^!68d0SvJQpeWAid%~?kH#9&~gU?{_E3VJY6;pQQ`8GZzvjeeyS4^4ST8Ux0tzb!#F zGAr=r*X++kD9DD=Zx<`xDG=j(rdn5t@@6S<1Ahp+()A@`-eq~gLpRI$Oe;+9uew*p z>SIutzCqYg;8CG%kryh&x^|G4b2=4zRn=QL;n5OM3P%GL1KCLP@3~eL%E1ue6A1{F z_f;!@P_6u4wes85%5S2aWiR*QL5f}C*@kXE#reTZ(OnNa&UGJlwk4inoRqw>xKi?*neV!X#?D4wO zuGQF`c00Ic%QMW@d!Vy;pp`6&UyF%*eG3!gT9s?i&duAzsMqG@mVf4{D#hUCbj(yH z;i$(o)oU;dgU%)Y62HGTJ#XF@o~qUc+V@8!+7_VAR%5@#X|cF$Sz+;M$&uXN0ymQJ zc{`qeZaLarRosN<-_`T1qhk2xo4kn;_Qrji>ssE|eYW`i^)&B0_*-jB^ZJuJB8oCy zEEbhh^>c}+WalTsDA&Z>;^i$#U3bB7vvoa_xPbQrQ^PofIg`f>jRPK?z@ssE43<2S zU#`Z(O*A@+qgx7+=iLY&!srgMLgqBNqsV$AIk@<4PgMxamZ9yl=gwaMqk9m3GuW88 zOim(H-K}t?P#scIx&+EzunuIVDBSm-Lg}&uO|OY5A9OKj(QDJ-d_jT31CLdK~9X2&hh=rIynap zJ{=AhH}n@@q$P{FhhpL?u>fV##>wvKH^j0-^YUWhfO8Fh?utYO=5`}QiE`NBDbeB6 zWtmG(NmTY;Jly^q8t3!-AaRNhS*gzPha-JXNpek}Qyld9g#X`rlY7pVmY$=X4VIoG zy=5rosSsFsOH%{~6Q;eTGuW7L(_4z+NufH!TfRz@-tru@HtQ$Co6z~SDmc{lxs1ip z4|B!(Pol)u!!eOdU4G)D!|A~gnewF{?7#ccRK_s)lE(G98eZs26&Il!Ta7NN8Xab_ z?21DdAuI_IihGV^HGQzQl&LjhB;zVW!TkyrFkpArw#B|9GriB%^okxw+k2PeZ}8~V z$y#m?OqUtRsI&uDotV>9{C>2+erS%!|Ew?}grPd=gPSbTwO5+RY3>sx^)gfPU;4Wz zUjCBw#EqxIp7=>$k(FEMeZIZz1YfO{6#*~wbP)62%}Z`kCLNPLdPd{PhOeF$)dX15*U5+^9P}RUt6+u8VbQ z-JIhUHDSLAa7&=AdM`clky#cWx4)NO6v9@X@UHcDPw0l|!&&9qR-TGPSe_c}PA_cX za#0Qe6V48$j9ko?mOqZ5a=jtge~gd?}z_HZ&r2IP2BJMzXR=w9eG8J?kETi%UsG15@xruaamgs$;zlY^>MWLT`u zL76GAE`KmfY&nqYjf?V%FAqFWd$R|IEGaE#$bHpgi`zaPSzHU;38T6U;a9~O_PBJ-5IOaf|_OhZAPQ zA>cReEOR(fnvU?A&^w|rT2y-ffS`+D>u#Ri2i{`k;T*B~6TkiI4PwtHg@dc$bGyNZ zylsV72e99}N|@#o3s|A5-vK_fd4FmiYV~gOyBzxga zV%fpmT1z4!HMSkD&ILHw+*;+tM_MVd_n!=R=?mhAJ}IJjTTJ>FYAi*fsp6!#&@Shl^;>MY*h-pUGyzqYpu z)~E5Xz+v^rAY-%4`5!$mb=3xBy@k-gXrggmQ#g87C(f@Jpd7c^u!hcnpTH#0-6%hX zvRXF;-Gze#z(@l@pxo6cKZtTWqx>(FbByx6DAza2ccR?hCV zsC~W+uznJ(+2s8gTC{oubkIJ?M0@*f;@v&NyO=F7d!6U;Y+0na=ZMNB`WX^%wAhHG zZ^8szu%hKrALP~Es@#`r^H#~Wc<_UU(M$8}iUh3JvvS4u4^~7s_z%Xnr?jHyIdSFQ z+_(}H7pbhLRv1+zT+&gj`zX1-NfWYliCk@P2{tv_ z&28=XXCYrr?-D|XxVIxjQ{! zS*g|l%X&q;H7v!!ep9MFV3o5f&FW%zS>=3@X7x^K`262fjh4^LRO7n;NNcd^fAw}# zjh|cD&YOL$miExrqRaAR$KT8Ab8!3+W4F26Ih}4@mYnj3B+{0kOxe#n12U|vVDv@m z>OcSs9ZS5pd*N;vFib|bW=6QPHQ_B^!DIpx)ztzOBdDX?L6>vTRVu*0emf^t9J?^E zD!!3-yOV1TclsQ6uk&Mum0xg)3(Tl&i@)51nggHlVULYl>)dOj4;@qvf86g}mTBci ze-$p@gmS^zP+4|BCK~?Og0QOL19!=(n*!zE>%5z3J)hQk9jd23?t*D5r0P{N&o{UIvO6ptw0e_5> zCrO#G3$wAv@pe@XoSl2vs@ttPO;sHOo9G*+gVrNDv0kq z3AR@JzFiArIS=i#n>wittjs#?=1X2Q7-FXoOunb%j(A5BkR)wqbp|x z&VNm(Af5xBQI0j&%63nKb_;7lO6;bueuv!rruuvFOgCYZ=O(L@XoDX!u?EUlXqszbisc4RNHV7U4TH&d|G=cD!Q^r< zF$HsPG{AmT>n?P%wpH4L9_NY7m?WoJvgHqsLMzM|LX@U@GS#TKRvV~9&Vl#nGU&Ku zW6Bb7o9BYo+3I4t`m8oZY&-g6AzrIWE|4dzbQ6e}s%5=IKOlp`+W1~r+Tt^&si#`@`Sk?qcm zJR##+kdf<6WO-JKeC~0zr;52`O>9g=s``|<7n+Kx#~6QPUpMz4gG>xj<}&BAYq$*C zB41&7z7m}{vsZi2a7N`fZ>b@VdU`ow|6U`epGxLdqnLYDYiH#^E6O=u2c7-)0%>^0 zr^6M>>x*Tl{-nz}GO?jw&W0xZdk7lyZ$2!f`zlY_^De`l0l*fiGVxzSd(0asROJFI z=n5lI0elZ~Ycna64M09$JiKL=ta4vkUwDW16>LE5N`(AEKLlR>*2_)Hv{0sp-a zZ4lGoHGgl|3u2C9N_;<%nLPyX{CbEG$5@JPt0C}DfMw=ja|DBowD%z>lI}%=?sG-A zpLAo9FH_V10NB_dyiE~4ZV>KMgkKpA9s#VGK{t(bs`29sEN&Ti$KK{9(y^E6?u64$ z5l>!~;;i>uF1;ys%A5PLU=YC^tuxrZxse>LqrZ; zJyXR%1m3Bk5BriMT^FV;T5wi{mt|-%l$W*BWy8zLZjoO0l1iUR??XG<(^}z~Q2{as zE3bwP=u)XlIJ(yLfawnWd4AYa8iolgM<}|0K{o*~dQ^si*I(haGw?`Dy!Zv3Ly}bX zP>96_B8d}oGTd!4YEh18p}k|M>Y$Hrl`Ea?C&IJN7v0=rEhnco2I*7KfF`E-$~2NP z-$7kA%r``tkAqZOp%gr{`S#&inXl4PLJu{Bt_7H;dw9MvNZ&FD|2s*VeX2qD7hp6? znSuAM!kca2k(PKb8F*hRytxJ*X%lnO!sff(FyEQ^(!ZxG^NrG$@RXL{+6~t~avRbo z8THp9gQt)0hEH<$ku&m7bLjUOm7j_Dw%KCI1Br!~CfV-M3xg(WhE^-ruU0Nd;+dqk zK((K4wBNz)zVu*eEo=v1`)<5nW;43nA=!2>jy(8_>T)!x>k?7NC4w$*MK2y_PX^SL z#l_Mac=w4ewHb=&=Yx1AY&Y?F{#}{@0T5&eoVH9Za``;(K{Tu)+qiH7GYN-PQa_&O{xZUWvC{6Dw;|G+y;GRipRx$1w762ag^7n1Qk~oE7s?{U9QSO zm(b36yHX=?L)vSUNw6tQ;NnLb^|gRs!zh0Z{LR(yqm6p*pw>beS#-skjQY~$smT+r zB{C|%TQ2u`wKf|>|Ar}X35La6xfbuf<;AeYpT;vS&YAIjJTnAyZ59XXM{p z1-tb$8g2(yq9?KJbL^@T;4DHPvV6yC&0@T0E0)M$5mQLK9`&uKQziV8L@l{z$r5XDL`ntCwcv=$wTbN_6A4R`LR zkk6pio(66U;F7+d0qg!h=$C@N6YxtCV+Vy!HH!WR!9oyFsFA>7x3O#XO_ckTLsg>; zV@Cknm@}fEwQ>Big_iY(GpL7^68GGfVGF$1MZ_&Huv<05PGjz<@44zEKlD^A-3`#I*t3p8Pi~13 zTb38v&!lcVy?m18JG2XD?=!l4Q^kxG{_diwTuRfj8L+~RL}^(nn88K?)3e$OH%i5O zz$N;rE${vnV&{r{?~=qC;`9gXs`g`V*nRx6*yv~AX(_q#Lx`5T$A$-CeuAC}qmm!F5NcwRg? zHzT-JS*#%}Mk`!ySV7l)js`g&=<=^9b5W?v>!Flc{t1RL`g~ z_UOQj4RN~Kv&yJ!2u>8Cx9apb;P7;xFg=^i zh3lJn(t!zb$7in^?+iqrO%P+3rU&l<>V8Apubapn8M32OGF%LQI;;)L=VygQVEG%+ zWCg+#s!IdDTMWK0DiKZ@BD^}m&aCoLcD23F$&M#{ly5vdi?3;Pa6K$3HqWq7Z`Z5# zs@|Sj%dR?%8`pl!3mW#B$})910S?d)>gaMg>Q5NrJpvUt9kC<#8Xb8j?yS=K-Qa2L z?3bWA|Gszrd{ft5S?G=~puKocjpc{HjAcD8cQ$cv?41?m^HXAv9}ABT5A;Fbz9{%Zpzc z#@&q#Pa2n_jP`j1zOYO^XF$=IBzIB|xRrZ>+&B<+CK8NOKA)Ny&S&;6fq%??Tm*h= z5SwAB(h;ig?Gs-xM~OHABC_vw`78KBsP7S@HT)yMD-E$HE3s++9HZsimUyZ@Gw{X& zkJEiXs^R7$puGe@+{J*dn5{x_^-3Uf_99abSK|Bx8`y+l81Sz-=S0n>nvXc+pQ<$< z7xiPD`6p|n;yQSYSolZ4S|T>wof0?qv#?98?7(z6y*_rpbh;daXPvwit`_NkrrWna zFN*%m&1s;hJ9QHC{!Gu(Rg>RE{?Q5DfT{dU;1l;AQr5dp z)_Zok*#BpYT?f zbEpvK?Hi;XFB^L7Yww(%U=;*UJ|=?#?ZHH)NL?N_JLUeBGQcfB0MUZ*<%~hqa9w$T zi!E;V<&F9msqX`Y>B|u^wZj#32zVDdp_5XARyhu#m`C&rRy>I90bX!HPbhi$Dzzw>&>xp2Xvxh$PLN& z0VB?4RX4+^+X)!gL>pAyT%+y{z_4|_o++yCk`(ELteXuO%+s*)DpfhusGJ5EhYAr_;3i(#jP3Rq%Q-W` z%JDumQ+n#+BjFLZu3c!a5BF>@x1!?eJtLPHG}f1^ok^Elx%t!X2**_JuvkCKaw?Y= zSdO&A(G11C>JI0X%dHOfSErrRms|JPJ5M_|jkJabZ@FF0N8{T<4a6~ef*#xpP&e6t z>w$h7;4=(3#}G|?Bj9rkINgr$>j9r{z;PYIgyJ>Z)S zxK^MQ;9Cv2R-ifHI}CV@HcEp=sL+eGC$E=br=E<3?lr};nKC?6+k;P+%^PjSS#dWW z4tvWZZKb!YA7jPDMIlM0Uyld&yU#j%##jYiZdxE6BMGy~PD?&`DWKjnNe)=Exp zhX%I$aYa3V4nQYR)F0 z9;h`}rr})P9Z0d=G<^JYdCQeOP50hDPuh>q>~;1upTEa*4(J$mHSU^4?u=f+_e^3S z6!%wV`OiC5^yHxK`12R}V*NGl)oP;oZ z1>i%KnXiCift-xWerU)W=zg5ng&AH}LRp5FKa6WrhetcB##_nt_om3O!l>C}jP0Hc zQYLyBVKc9nOMd8+PJ z&tIov2fCiewH533q0F1aeD0b?+KfuB$KA^a;ISlGs!VtfngXy)i62oplvxUHbNASS z2TDSN9mg{zpnJRqjxefw;n0^Fp3F}|@-%SLn+*-pZE-&1H{t)+Z11+|QsL)64J-V9 z8|+UM+G|p|Cn0|QSIl+qV7Htt#>IPFN9+JS_};OO4E0g=s;j<~WZ}P%c0z7GM_(?- z-#gG#ZxtNCOv7j1=ivM^UDYj2ttqZwG}8X~sJlsaxgpe?Hf}G>93gc#-%md~)#I*H zJL0fcuTuxYqYuW$y5|B*@HPsquFD6JwX@@O`7f3|r4?`JE8NH;bzKBjfFFl@JWxIe z0?^>>nuB%G0G5XAu4nJ}xGPoepy}+p0LJA)Gglco&d{fz4YTROCrhP1cXX9(`CcXC zM+RUrY+*qAQw$beRnk#SH3XC{;jF_=!M1Jq3&KrTJV_a(FI<3 zI(kr-uVW7x<@qQxX6gEg=tZ9D>+&F!P5iz!?W(U7?~99o#Jb`BK96L%K49f&n0Sk?^Hj}O&yVFJnLr(dV%;cJ)>G_o zl&L-IJgH4lXeM`W2sqh<(q>c^!C6w`-aQ!Z(XU&vM;lH<%ZwXL(uU*Nja2Y(9<9{m z=04!(#D`C1M0mkEQEYe&mw@nQOw`jc!RE>orG_cm0!DAchrCunKF^t)3g3Rb$hdHu^Fe1TY1!@GyBz;z))Hrb*T@py zUGg@aD*ZRb(6((WH(OX%Z282PXQN`buaXJFC1CrUnE7l@S^2Ha==BGb}OXizAyKte$b21eeeq@;pNLPI;a#v5cV~Ld)$#vC@--_+4lkK8! z#iy$;tMexYjC;=`%(d{jqUfb|6!MJXigrgg-tb(WRp-d7o~l?l*;c$*&*jFwO~eap z3Zs`60u4Fp=dU``r`O1CVz}YDJn4qocKhpu?bLdeD0n_AF8-5nZ?`ODZx7jw^_TAL zC-H3dHeaA*Z#U~IvkyLJ4Rie!@2Sg|EN#TR+3jTFY{3+B+r^5}U$hK9w!y0U2-4ZA zCpoh`57_&L#4mM`5-(J-j6GTgnAYH%mcT%~B27_@5uQ+^+;Tsl+^EV_)z z=m;2@ukkKYjFuaWmI0;*+a3SF=RNQ-_aAOmd=|KF+!`w(5SAKD7Xaa6n!O4})V%E! zF<|EO*ts8v`)H+T`9l4|b}oV(kjZ#v7jbab#4}S|_i&82xYV>$xk4;Ho>i|0+NU*` zaWug*|E50%qvT+}1nT^0&BO_LSirz0fdMq11sR z;N$h;mGbS%#p(6?Dd>{$RagIf{?941bx2u1M#35p~f@Ae~!I2Gh`fD;kUcAM}s04qZZC z&9OhY+Za#1QO0$_psTH3bq)e^^9=cSgf51+iwSoZm}{Yu!ECR=yamd%WHdZXFR?p- zw|nA4M}XiJF7Hb(&U_k}TT1ft-R!FU2J=+KoEIIncJ;4yr>NN4hK*a$#!j;vYeb2_ zSH8H{FnFm;0LFOYF{hxjmFpeXovX(*G5@QW9&41YXN;!a11zyx2mAo1)?RWZD;#2f zeN4`h|K1<=$6s2wuTL%iIwoU^YW@;rxWzDw?ql?eE5hNgV}ds+5!mN6owmT+XtH&h zT-~rQmjG{}QN9`Fn50_nnGNfogT=Uu!2lDL;&J=`9L`>J#ADKyV0uc%gTR;}FDE0f@iYVbU-n*`@grg`MqX8hgKRFQ_M@>+vA77`9>{I#Y{a2F@AJi0CmiR zbvZtgo^?D_=46(m_~t9VD9gOJC5UTyN-IWIsy+o{jhwC+>+&B6z`iV*u4>pVXsoef zo}ifPGU>^?yTM!6dA&h-j7o%A7eROv#2kE@;HVNtmk&c2=Cisy99nAOG;A=03n<}E z{UxtwvAq#&FUU~}Z8gG|wq13vHZMl+!6`B_(A{n%JO`0qX-`-r_9uD#mt(*zz;b{` zw+tBYKkzm!m(q<5So+TZ64_e~`OKYV>n^okv%LOmq!a!5QFy#%7P#+m-J2obse> z70Q#}cvMar11ESy*X2o0$tN|VdS4C>m!N_<1fN^tnG-IraPTuh*#1iAJ#E&F!jAY= zNbgw3mm0^Tj_}IIoKgL)4spwy$SIfWrcHl{#UJF=(r+F2Dpd4{UT+53mZn z-){7{*KZq7lKLka`nPT_R`txar)S_b`7E*bv6yRmTpRAEPi6nP!+w26SiRbi+&qFC zF&N{@@|T_?>O{QUUZ5uDkb71|s8yeF=fn+aR-ms?b8J`8vD?q=={qJhhuWrMA( zM0}u1e(i*r9OU}w5&0beyTd;q{4Npj6^g-#HXW9NGgJ<-fy$w0uv}9wvLZFJHtxv} zbQ7Qtfp6B^@KjEsYIKRH4~qw$b=_O#Q9B^*!ma9&b-(}CgM|*oy9|}WN$={ z?PL$NlCFY3g^~PtIV6C@1xKC{_{+0zCNE>~N&vK~SgSgWuooW;PhX2Nr4ta|b2&cO zVGQv9WXpzXd?*`oIu5n!w&zzSU|G zrVw*RbE=gmRV&Xt?|xIsJ9DDP$qv-0=j`~rW{e$sr<1kIE^y{Iw`1&cckqSNytpog zZeyG3r7=FCvP=euV#xQ=GAF&*O1HbOaEgkp!S>6OocoKdJp0OPoL7piE)kWdan|N6 z=w>B2QQfS>^hdYI8JmYSwXmlS-Q0c7<1L+{ZdPv|z|8Ar9 zyg`|zf=`;>hz)WBjDc{!AsL<_=A8q1H3VKA8?n4BC&d^H+MRp40T6(v4F4CbrVatD}pG!*1_vvRS`12midE%FdWbyizS{XATd58h0BP`?TMrZD| z@ExTzQQ9yn{+dT*oN~Jf&oR!18W^XGCR@nfZm!`mccKGy?=QwPJBeZy;(4Kgmj}->zZ~q~^>a9P zc|XH$>bY43KN5DGA=pg)48vWmpP|Dd!*H!?z;GdMoOdA!6@3ydnQ(2;`C)^`RVsT{IRocqLrSI0{*}| zsjt;t?@E~#e8ce1cW_|Hbte0T?JipI_eJ~{upBp$_z98C>EQrUN&bq?>FcR_*5Es+ zZusDh2fduMr;c~u`Rf&0#1r_QVopaou9y@45twt%yR znRVFm{x)7_T$Q`SgDIjh@9^3sU7Wbq$VAeX;q?L}!)FRCf1klBe=0GYEA&j)fLm~5 z!G!TNmWr_M8iX;_i8tR%Dk!_%eZC+i>IMGnybwM7l?M<7UpV)-uyP}(p&o+0(!%Nx zd5e7hv4xeF{DAwcQbW*i`zGQGryyWurT>IFChR89nyCNfb7vCk|ATsNYMJ%iqW<=C zXBFz}9zi{~qQ~>h3OcMs4#4N-+2~N`NWi++es-^OWlP+dShm+$g}=Ugf#NJ#f&16R z<9Apm#SaHl;y!&ZY~r8u;S{yA^6k08xw4&=o4gJ@Ck9F@_?g1jx{WU9M`rUrcUH8svI>)xK5UO~SCf7% zV2zFPL6k`s1G+kj&IXKkgovkKL0b+OUkA|T9g40t=*}H;+iP?hvhdAJ;x$n1-37df zMj7pwWj|9R!V><|4a)p^R*B_3G*9ttmNF4?(>Cw4&zFI z2T_|m4}Ioz$hY$He)vCl4iEE8zO5Qhj+inj+t}wW!iT-5W$R$lLT>h?t{Z38sgd}Y zl9g>Nm$C*o{U2?lnS83X(Z#G&9daw%$SEy%KeHpRgqHgt7ehF&JJnY0sT(bE<()C( zu+SL$fR^)JzICs+2(w;xs+F{{+5L%@Q<`V(@D97eb;8(g?}VLjf_Uq*6mh&?jD5NC z!y6wC$H?HiB~p^hwJRZU2|YuTW02~Yiif*Ld#cPj_WUl4>FTgG;7KF}&fZ<#cUlB{}`tSTT_sB$u&mtfI*6 z@_9uYye&4yj$m-(#eYbp%bKw{nFJ|uwch%DRyjsyc@Tfc`;5^MM{(R4? zL7nh9bX(N(?l$X3;`w}bZTESKONTGWJnE)&gAC3y>oZA-|8;RVVJydUj6UdfmJdTn z`tB(#nb4pO1KDVlll~uoncFZ^uU4xT8WA|(;P(!wFoo%cHMjYq!32e?laAB9!r8b< z*9vqy&!4|8lh;DY zD&|RS^-er{0=RpKpM*db>3*57nBPxd7HRpdBKN7NM$DB{X-UvpETal#yi!;IX`;?*>bOFY6j-6bq?&|+3}I^>~$PVC}rM>%gCv}4{! z^D@u80cGZI7GAsLB`_|g+AVVI8dl-pW~%2|Pr2gF_8H6ptA!wjzX^lykpIm<>y(^V@tcHV`&_bVO;l@Xv{uzW!^MoSDve<6&J;f89A4l72-!;H$Wug0fe za#)}$#pj7?e7?%Z^)6mDIAt)pq8cM#N|@0R#pr=*jLuZEPQPkA)*3u^D;{&nqnBa& zUp12qx%TV0N9KmoMuw+98L&! zTR4C?x=ap~$yy-zS?C#l#^i95@<~7Z$E>5Ee85=5GD>dcxJOT2W3=$QC%rJv;Hu6vOgR0TzDEh`B`e14KJO6`0w`vTUDF&@W3`)yi z{U7u#tI@Yp^u?M!2opa0KN!@j#-OJr1l>H)nbsMqn2$Gb7XJ_4(cm348BA0RW*7|E z$Um9)cgzMR{cJ@)$Dp77Kj=^X%M-kS!Cb}QE`!0?|H0sBH31eHbZtRr3b#b@UT)y@ zSvLBgwR^i7^OcH$+4pSZpRBWy*&}~V#Jl2hIXdsz5gwhZv$1iI9-OL&zA+fvyzy?Iz8|}sS)NQbkQ?mC#w8&mYb_otTbT10LCsl)bPiT{rZd4la_;1<6njHBw3bMdwlE;uV)N0*7SNm{2esObT; z#Bku!q+c248>>uu%7BdoOo!x}{`QjigtTjAdu2{vrp$~P-+KV&HG&dhpAM5ZKK9@* z@9Vs_Eo|RUGx-~E!Pa`mEg;t|TF%aZ>)nAGVD0YgsOA{?<6Ri0GT0#!v=(&Iv<0Ab zSI@v3f=^L!Wy}TMPq^ktKB-|o36;D}s(pt@t%f>VuERD)DwuGakt^MA9+oE>6LV~Q z@4?2B8kcy7;wD5hnK-jy!fFb~lR1Nk!U_HaIX)eQ`0PGhAy09~V zl~jXO0d{#c*w=uKuLdgzY;rZ&F~D%tH`Izdy*1oDahb@23cWkp@$O9{vFCKO{YQKE zWwaGtZ!fxIAG!aNMJi*0 z(r4vx8t#s}J~KR_&!})5t_I>nAn6y?rVWcb`1T?()ojUW5+5%%pRHySb{M`DaT^m)YoTQEayWGGimJy8ji!M#!w$#B;vkF zx0X7Co~wZ$lUlhjuBRcu#B}%0pBnnCi(WVVPeIp-2Ok6$VJe`DlC=U4O51`yX`F+ET^K}En)ZTp633BWXFzj zLz-JF{LUq3uk}kyNKA8;6<@H0rmzjiaS|}oo$GHS6FOsiHppD&TF z_{iq4E54e7>>yt>>mTDC=6;p7jrVf-by`ud(@e@CC+!i?E@K6kO$bc7ZW|Up=Byff5eqFDAT6|qZh)TfXUpxN2{ha2sWQ4T<|HdO|FhlS zO%((at~O~duWL$Vw1(N18)kdbh2uBvMaxkM4_}(NvDt|6mUqTLX^1gp;qIQB9O~}O zTUg!3o9+Hh675F%i$+2a<_OxUDe$rW;r^bVi|s0`LFUv6UodN5D&~zG?k#HP5$8*q z=N034gI#!SM>#xWb4FLj;X5KLjUoDJN0EjFweq%oLw4#r1Eek+UJL7TAW4iJo!4bgTPd>*OY_AT%Dm?Zt7?gob)g~aTEI=+ zdh{2U4Ik@$x~-J`B~(-PPdi*F`!j~@JvxeW!wW>|uzI$VX@DWq^dyGO!Ph`028U85 z!*V4913`a72A03-CxZsx;b8f4rH@|9P@m8Gi7tclJB$S>i}yl44z0CHF8atrfRViG zMaXyd6HkG>mm;68$Tu0}a};^rMaZA;CnAUBcW$Q0hbi){2KhKe9(xh;B~Z{3_3~51 zT|@jG5)_wCipws8OK-*HY?16z(*!s76CVx9&-#6pG(mkuzTY5k-obqz?e+fBSDt+} z>?`ji(pP$KSBy;`=mzmzKUbqw2j0cEd8*c{{##@C(27;AzdJ?JXP$&W^qDG=Z=8=T#zoyv|e>&zy01 z^qqt=dUB~>D5g(HPX2T?Tn<$}WVzgKV@|VlwOBDB-B($_-$T3&8)RHMl1HScv|=9a zF<~|g{eB}msPaZ>ppRV z@I3{8v3n=xDZu7BR;HCOeyt%MO}7Q*9I#BouQ0Ow`)rw%@EN~YB~Rf|zp#$&=JABT z4RiB!KB^jg8{m$Atv6BhyUH(C&R!^%6wmfv)0dto-!2{OzG<2i*JoqcE3c`887<2h zeJGXYgRbC@zi}0+%S}*b-lof~Q4YNYVKn~UH@EbVmLc&>E4p^dH!C`+*Z){E5l`PvfayEhDq2szj| zJZ^?Ce3w~&uVi{3d=miWIZ%%EvoDpdoT`K*tlKrxUvdmsC%{-&58F;0LVkQWL4Uiv zsSCpkG-&IZ^XF^1T6AS_AY@ttCp zKe$;Yt3RualT<@4kCe-wafKBYXw?rdPs#xh$S>CRi6&-gTUb#IuewybFXS zXL`G?Y7->ziF@a4R?hcdhhX?VyPGflkB@_kDhfwCJkLQx+VqC+T zVo}$a2{ly5nH~03?+ZKZ>$ z*r}J{3lVw2X8ol*?p7}MG&G>ZuSzEF7Q#2_VNHHQqvpp_I)f>+Ee{ESU6;@^!hI(t z06ngo79fBq+4QtPL6lMrE=<=wjt~c5BS|Z*u>s^ z%Q#ImOOG1xU!)h*e>}uZgIHYVGM=y_=eBY?0>2gMUiymG-{gB7Nduqt&Fed%CWH3L zzT%Gbf`~bFp{aN^y-}T2K&+35t_kP9Do&>-*Lfe)v}@a9dB5t~ej+(T(_Na;$a~AH zyjYti7G=Z~U5Q;LdTz0@0qr^!FnZ8LW$$IUEjeFz7;Myhu=pq=Iel{>x&aYB6R2w` z6ebc*R)5_VHq(=rs8=sbcH(Wz&e!m5%Y6My;$7?C@b{nhY;h6TFZk3ePcz*sp$MK7 z(tCr4`;~RwpCsQo4uyFT1Q*VX1;r`1io^*O)V6jNh4-kXQ5@EW9x4Ovp2#m`s4NX!Jo#-p^Rl~Y z%vTJmS6~%VjWMVugDMjP2a~<)B9FGRt7e@)FJJkO_eG^KFXVFnHR}2pb$)cV72tm71EO~0x)C18 zutv0O++xswv4O)@|G4x*X=0Q#KUIS#8q&njB2DGd5Uny6mnGMT4UIF{t zo0cyIGGJV}p?QsR`lsL!x<2tD^($B}dNpYgu?}*s5w|pH5qwff+>oym8}`dIB+FF1 zcz<-VYU>80txQNxiC-_2N%?GLH;&u4T^ROM{(?5j?&N1H`!&k1TlZEa!@kyVb3+Cn zWe^kcX@T4GGlH6)D+uPBZ1Y{vgy6-}V^*uX^rpwuYj+{h`0KsS_g0*VS0Fxf?cNuE}a{w#d8)KiM2@!if3{ zrW@Jdmq-naJ0PJu#k1L9k*N3T4j4dE^5D*=Gy=i_tPh0Mk{}C^L^oU zHP2({*Y5(xzgv+qa9-2lZE_MEM5R7)u!o&0cPAS*LtCI_N|cLdzC?c&UZU5#`9F7; zdj>y3*s< z(eR71>0u9k3DVPf7|n^dd%7%Zx|f;oQD$~@qWWIq{?&)Ygh25aOOSQ+&COkjzTZJq(DT^%*z(M3#oXQBk2aaavl6m50TfD#&(m_z-7;edtyqI zd+2%<%)o)J%|J)^x~p^up4+iJN|}o4>ZU&F?5XOFG7nd&))CO~?-EGML0TFfq?IdN znEXve^6Rbm;f-4SzM^9ACK&P++Y(iNM=|{OA96tJ18Azg9d-PBqiSr8(O6J5#v2Z7 ztf^|O6ucWN-p?rEC;E}6QR){|V-pm^^#;T16vIc|CH+rg!_WBL*PeE&{m?l1Nt^>~ z!dCsE2Co^{Z;Os_7XaWrTXl+l4UK);QZBiilKqy?DP9!qtJ4n9YuqcW7!Y_anI*PQ zP98oBAe!@3p}ZK)$)`24`OPYu&w)l2hj0;#LjxmxmmzrTdD3zwEq`_URH4YfHr-oq zh{w6UtVWs`e{D?g$J-gR^&V&ej6w@O0845f7F__*1FxE$)G*4_teQ-(K&EEZ zWFjvwWHRZbJf0pEDJqnhl{RLnN-VcXm<>1JWI{LKhAn$75&~K&^7;NIqF_qF=n)t+ z?!Gn9n)xF_^UYX}62SCmy=9s#?^gCAyVs^j7tX&&jUwQ4p~AW}eAyab`?lJ!`{U{v zJ>IPb$I*ZeO=OQV%2JKKt1>TRG!)D@`)qz?yD|Ge|BN{2ws!s?4#=wzp$!aG0Gi@@4>vg zP5H7PZ%k?A2B>8CBEr%zLa6>Y>P_ZG^i%_$GY0SN-sFD% zc~MsxCg(gC9?;7pxo;VljVXxZgL72W4dSD#liN21h(l8c{KfD9H~7M9y~wTRLm-az z#f_15V7Tb=mrl1Kc8c(C_lb6sVn)nFH642nWaG%Ax1B|N((}c$IB)iV72&_!jS(H- z2n>IR{!$DAH0xg4$wf27`xP1eW0UF*J_bG%fxmyqq=+NKE0e`^-V^aF4`VDn$M1iq zOOaoIOh^w;5~qQI^HWn1eNFzP7K1%i4PoPp_`C~8FN&Eet*v{2 zu-6r9zhd3fV4d#5=<-aCNu#_1W%K69A?zn}gi+Yp#`xSZ3S$AVGj-%03n=QV zJW_;j_E}aNtD}^$ks)IrDdSuv<21-v(}N#_He}Rw6A(8z&RGBAMe6G!YO-GAw?o;z z%%mJYzmbdzlzFZp^Sn`>T=&IOA@xq>goe7jaX$3x@0lr$Q4JwoaxRbLbJ+@t7ZXsql0 zIxKrlmyan65&zmS{*~n#;Z-0kSB+`dP&8I%IQv_80abVHTEk39=-M1uW|IN)14ipS z=2~Zovd&z?IxBsy4{lZ3?EFN|bh^CiGdUON@;lIoe#e8$^ZITix1*b5!|;>$9V_1E!?Y9G>`dpidlxHTMez=8!0|oP~^n`cei6&8R`s?>g-bL+@jRc zLpsG9syZYu%nk>jy@v7yP+ng*re{9j;$h9;dgu$C zWS1^8tnr$ci`?|!L1lttA4?b2<;_Q>i|X>1kmOibdGJ{?=My)hh|k&ipPD%E)Nnfo zF`ngtz$-3xFUZn_u~2}i_h zj)pSDVfZ?zaG6hhiq|3e7kB#IA6_3|C_mkc;hEdw8621hgjkav19J8koRBJ%`na&B~x~Jzc*9 z^&DQiFH+YZCXt(N(%h3#X7B-RZ`c0A0N_1)&Fp%VL!UfF4n4Sm^iPhB28N2Op*l_Q zDxyuc`^IWxh+)35+NxUFu2zoyRNfhlRTlaUUtiKOFHKsgnX*tyE_VlO zIsz>;*RX=F&sX)jOvj~)lx_3DOpq0Fw+ z<$Wldj+NCY9NU{2)@=#v&W}%!>uj32zKfVq`IH;nU+Ulvuh>TolfCX(*I6lIMmOKp zt(Don+%LyKoy}pV(RI0ygd5|!>_Pb}!{s-fkWqyF-rA7t*Wt3?KXhN>ynS25%kC1j zzmllEA<;-F5r4b5laeS1iFozym9nrzx^5OyMMeVFS6rmNh3Z6&f0^niUA_bwu;X-j zE$ZnsD9es~7XbRcMmU3VC>_Lj((n)A9bB^M@>ygKeCJ)4>mpC>sTAr9g<>G^JVRhz z*HtML;|Z2lynGP^e$#+`KnBlUr2a(+@{G|ZzWU*@*vaUV%h4woITifsxIVBeZ!i0) zT^^!+5V32C5$?Ma2(RKjwM9k)r*gyds&0VuX)zlB^PZ|v!aAx(xv*yLN!Ttl%D)*d z|5}yHRhzMFtf6XjB0T42MvOYQ!;oggSa^^x*Tv3%G8_m>o${&DtAqWN$=}^4`&E~p zf|a;3)#dT3yL97_!|Yva$|B#kevGzjtS9bOqvK#KTf6uh1%qW}wjJ?jX&s|@Zwc7sKYkRIQOMbylx9wY|mldwH3tqDS=w5c^ zNbf5(?6_`a$rHTOYkHSr@w#t{x1qCkiZ@4>dg+ITWu30`?yck9T`$h5HP8D9AIhGd z=WSAp7-jqJ^OmREBLmJW&v=XMz5(atGv3SWb^)j8S(NhwWz(Pa&TC;`)}ZY4L2qHI zOH=520lOmGclUMni$9l5`Iqlx4SU|%vf;wFJ;wVdSf-Tv>UuqeaZX!4#Nc5BA2JK$ z%5E<8O^CB2PnR8C=Ns&^e>hcEu+cZkwsTH7%U<;r)=t2C;`9Vw;>m0kSN88$eYe!~ z7UOBio4!Zd+kHQCPX6j!VK?}!?4I9zms$3Kqt3^_`yRH}9xa>nhi|8CzkkHZKL?D1 zN6Owk=POLK-#b|Ld!2|IY&-v;Gd(FH*RFT4?BS${qlxy(kDTfG5xwohAN@bdzCEz# z>i>W5*L$z?+GdycHoMPkW6Ur!j9g}fg-)H9GSV8?CU>I^dU-=tAz)KEIOQ;k(_T%fBvp_3CAC~%h=t$b8ODZ3CpRYchwzF9;=IjmixnY+|< zW!nYv+pR{2-1wq=IhI_Y>x}<&kxWlLXHPFGua8JeIk)IEA>Y_&zAmB(yCE|^(*&7w zi`3AN_iZRoT%c1BM{UULb(A?altD!lw+AQ_JSY=wD07Nvz#bH?-32-e)YSKE>9tt^ zOJycQcph4CCH-|#(uB;i)1XkFLm~GU>FE<}={<|6`{yWJ?IM=^$PHA;JD;mvm7`E3 z=t4T#@ZY{b(Rz3vx zR`{6GTSnQ;I6g>akK;o_X50rgob=QeY87WeU>Uvsg&L)JJweO9P&1XOFVLkg)Edg` z11WHy8khOXMW#)xfHt*0%>=Cyujj8s4y03Gq6r1* z;cyK+e^_6U45ghLt;fxB1<6Su z3tpVYg2&OpuT}rVGhp;J7%_u`2C`LYCoIU~0FbvRbQaXg?g6y*Yc($XQ^b7)2CrHq zai{6UTQPvnegmAPz^{|{j50DF0>npvn2eLjykx*kpG>2b2f(!V06Km^O$_e}oJ`=%vM|dwgv&BAYyzAb zz{$eHe!OqSd@|B##Mj`HJV4s060hf}L=K=+D74}lK#Pt-SRc^da$xtw!e1EtZ|72< zgTN`tEh|0?oURr*uh*lQ2i27D0;C;6+AvGn*?M#cY0BYTJ@5Uw6b&URyK<@NH)>*n z8#Es>jYZ*~)bmt*6WV=Pt*U&KON*FnaW3uPU&@trp}dhxr5v7`EA_49tw!4jwi%_1Mtd?p8h#vUdy$qL?MeGCmF-e@Daw~Q6n6y5+L=R{N5E_|2tEKo6Qen? zk~XF4#=cC?IDHXtrUPfB1?QbqPyM_CzwnSHZJrcHZ`a{dLHs<3&GyO}ykbcE0@9v9 z+G&e^o;NaoGDjL@!ehYc3>+W4a%NUSKTGC@-|MQ&8r~l0a1~dpThQAfyJN7;CP?G^ zOxF6$FtSU|p#{faH1WXUZ$a#`h!mH~J~_qPg|xX@bt9?rcWPwNxs%v*XTeaQqbIRD z_uTHe1s~MmVO0IDXUh+h`r5)Nws0fL%}^ueu3r%w`mil2=dAt(vRzriMI+CsmDMmG zui{;FWTzS-h5&h2eG;uiMs^1Ydj?-F^n*OvV+VQlG~;>FIPO=id|a7<+g|564Ln7y zyKpoWv=Xs-^9#xonHk(VQ*nHmE#ADL@V$ zj>}Fv?iU$}YbESav{j0II-X>j)<(pN?=2C-IAXpo%gi<+T;0w&(!||rXkt~HQsP-z zMs2mzNzQ`Av5Q{X1EUzEt0ucGR8yxd*gkMKB18Or7mlV6II)>7gv2i3h#{7U*mj7R zsz+qD6B$ioY+6n;>TQ)MU;i9$4$zM3S*~p-!ZWN!*aSFcE!>ni9=%!9qR!Hx8)`VY z9uVF`tqiijz^sI`HU@(Y!CtZn#_=Z22R54a2F*xGLlYhl(b3lYx*F*N_4Gpzh;ZS; zF-TNC;OKIN>t*0}vE|S4reenEij8Y8!poH_2RM8_ha1FG633&Mkp|yJHohFsyUa{e z-$paWph?!{UWELbHn8F38#pmGz8r5h@@{%XUT-fldR4aRj`2*r)~O)PEOU38Ct^Bt zw9bac-%6=WvmO-fRi1!H(UG~8LaF3I;TUd7C^``6^xLB!Ge2;on4 zB1L2?b2!gKe5`E9xu_Y(LwtK25An^o6Y=T=K{Gmtn!Y`J(7(mw92w32LJd`)?O#@W zg?@?@wdZ#fuHe5$dV0Sg^nSZ&_dc||(nqB+WOfwMPu>Fjyjp$|@LP(944tN&1e^sc z`y}A11#h7070q4DcSe@hp=?1)8GW55qA0C}@TT?OsQ%)pF2Z$)AP#uMX}kw;D~QA0 zuQJLbhB>BRZLqh_2eyFy(;4O1uk^5Q{2t&0gXMXHoTmvmd?Tj`(<@;4*1sj5=3yx& z=`btMcO2k9z0niX(r?rlG0Kw^F$C}xBv~gDPlEHe<%CV_ElE6WCmyyI598rw2-ZWl zN9j;K5uR#>AGG1K6U(>Yx6(zp-W9sNPlx0Bi0B9F16~httW%eImMX+xO;&Z3p%8N@ z#BDXng4d9OV?JT)b0H z>(y+vm`kaeWhY(RyE--cIXLlob4w%EH$u3houM~ z=rD`myAW`+r{3mRNT%tq%x{to%lyV#^Bbur$o!tvVVOsN9p*eD9tQj!W~kOFS`QSh zcDcsj-4JXmd@GC^qVGEJMO7z$uWyI>%gLkpbt3*ajEsBnKXXD06lA z>O-)MX?=9dh<{l42Tzm<9=YPFRV@)J#_0I6_#<^#W<412W!uzskR|J`l+Z$iN;g)$ zmyLA}itJ@+id~Ve*KB(|hY76UtO}^i7sOamO?2EHTuEs_*;y9$uX@-gXDowd>7hA_Sib#>RGnl38%_-!Uc_*m4)0(%T!+sx9H_%GJE6mI zh)*c(RaX3xZ9(Ag8lKwo@f11XI&g;r*SbJ(&4OFYgDcJg*Yq66b@*&w5m)0I341KB z2`oUdG@~zcTq)aUfSV&f>$1W&OMcBP`K{6E|+7?k!(8;#oH@ z>oEKAgmHlX#2nhXTruVzc@EX-xJ8KpI?Q=0y?a?YtFC&2RD63K=E4PMN!X)^!E~gW z2#sir=*`IAy86+m#=k~AHFZvGT@9)N#tW0>7DK97L?+u}NI2M)@0M!#*o3h+7%x3L zE<~%_k8sUe1V;_}SZc5Y=}$u(b4^e^!a+pa+J{>2s|NvzNjS%o#00>(FM%B|5Xw#w}32NPxk7vC}} zw;uC`1=rVOmEIG9OI^o1;vH8gprwci%s%2-qG@#m$J+S*UHpWyy&-2{H`#@|swqfTLKWjkP{qni$# zR1=k0H(fuers`=C)bNz*svDWE8y0`QSwP7=(c}F(FSp!&FSMpho>d$Dx>--cwx*m~ z*-3*9Qfs;-jiA%WQi)He>!;N4fOrdi2?hVC)@)nd62;Q+bdsg{+{n_Jj?LJ+yNrin zpFEU47wJ`ejb8duZIIW=ILVT%0L?4w!pXx@ix<4j+i>{pnuc#kr+8iP#aSv7zqx_` z4e;$*B!_?(R?|7FwLg?kKLHV%d(z_ym)Q`w%V)w>kkP1}oYSg*!h2jc?CPw?*Ew-l z;wqQ-aZB7ansHi;B%5|{c~zHbr5r<*}K%xTIVCm>m?GEVin6HHpB|0<>zE$F1!eM z|2fsA-pA#pGw0NZe*J8+Vr}F6-9}}osZ9kqcUzHxQ)b7JcG9f?g1guj=*BKJqH|4~ zS#WOlN_iXZ%v$O9F5HbvC{RhkJ?kX6P}g3eS~KbY#!=SttipS8+@NzjQixh!XEXnM z%PG00oxdm7Zs(<(qt2_5xG&NF<%G)|-47u1MG*4kKZ z5#wV+jHhgDmV3mAzldzI?dg_m^mfwiq8d@I(&NFR;xElRq((&>gqh4SwaCJa&;%(^k39*ow!p^P%7qi)m4jn&|v? zu@}{Q#fh&J<3p)xnAB^)B{iaHp9%=gD)#THzXGymv2IDt;JYfJ`)9_>7gPUBYP-(< zc6t=S-aaeaRd7$mqJ37qhr2}645cQQrC*Zz?{!(N{C})3G^cy9mG16@&K9mXlx~A< zx&Dh)SwdB=sMW%1qb{t5s3$#)q$dKAG%Qx&!B<=tH9B*;gO^&2xL!uw9K=~!@RAC5 zTyKPxXU*|lJso>tBi13ExuQn2Xl_$yj=^G_jRnW|vapDkEb9NHMyS%OQfGkZOeb>8 zIcX9SEaFnqyNE5P-u(ZnwNSeD(>vehFt{$q?PI99oQEsnXT&W;rSw@F4`lUYiWY;w(} zu6wf`b?v{|Zn3aE0k)|ApRcJA4C_HxWsc4Jy=Blnz;txxXEmbA`!_d+p)O=0B-rYGn>@T^ z!%eKJO`ehWtemCS@2SP53RViQT$h!Du}UcU{9>z|d$Voz%YUfeN^N_6TBFwaWW9-Qr+{|X!D_7(v@wx@yK z-t&gp3fR@qlhE~q+XB3;iD^q$5Zedw(`*?qv8kQ8_OuEzzh!fh>y6AWhnVW30l%st zRl3`R++yIJVmzAtD^%82YHtm|a-M2`7I)2Zta;^Ec&c&Vn4I-eS<>TwgWG=5mR@4y zwCb)zI$a4$V-AAMac(^>F^&37jRcV1ej^?_18)Pelqt3tW={{SPI6AQc=lza3SAZ1m zGvv@a%!bvvrJh4;TMn)3{aa0T+R#j$wt-%NTWUnzRR_!0QntaqhK+r87o&E|uP5ly z()Vt^rMmLQ*=i%Z0;#%JfMYlJS8T??VwvVX)1C&=jHyAOY(G4I#?TnkemS5V_Jrr-Pv{;_j z@v6o5HoWeJHa`k3ujTGio5OFbjnQR(cw4PnCCXN)Fr%ubFeMeuD1~R%MP&xORqu1ErdF{{@FI*2cM=EFW{8 ze@Cr6#HyBdD4N~1yi`FBrsYksrG*%kgj-1E`kWA{2h8R}Y1SRpb)Vfwg;Y>;PuX;( z^N=m;8svtZIQ~{c>*lR3zmV{mjV#&_X)TLHt^SsSGWX)d>h=$0xG2Hl06YKrb$OkQ zMqSvwKtTmK_FlDzWO)QE%LzI3o9SU{ZRawUBp4Uo5s5K;A zKYoB%*k{K9Jzw!-U;Hzt zQ7e=L4WYL~#FXpTM_An378aFe1GAhxJoDf7P~Z^vn$$WJ!ArvliYUJfd2f!=^z)T_ za>U?-Iqv?ybJT6tc~6e5oRVX%Q`Br>b#PS-Z*V-Yd}FJEnya`8OW9A{J158TozWD3#k9Ls5n4BBb$NGtgp(}ey>UkWlgSDuhk$h7 z*-`Fkd98%F*GiaF#jC=j=H)`F;xpt>t__CEHdrJ?wfp+p+D~`|YME`vk>ss3QQnW} zT*qpnnp#sTW2q*}Pmubc;O0=ena#KKslc~%wrNGu8;!fCJq;Q&yJdbpI!MQ(#y%np zb=TR)rh}f0uY0{$eT1u)X0wl;kjFlPIVX#?8?02p=yV2PxtTyj1?v0CCTWCJpKJ%s zeT6IRRE6oezM?^k75mFuP6Q0jJW_kuPKGq|J>6>3kdOG<%G_U;S#I$9`w9PAjaM0l zitR)0%6Zel-;BfPlrLO1=E|p5em2)AI^W9~NVhW!?C;TFH~2}nbHvZ)8Y|+c56C6Q zv`QlOUmW%H+)APzJj?7#qH!&2I~!oMvwi;MwKzbk;YKAr0|g>lJ%XS-gY(_f!}Ukmd)-RxJnI@c}mH`9D|>1MR;wcXtO9 z+aMO+y8_S@#(!B}U-#xO9lw9zJ^4$Il!bKD@#h4B|G@t8>@-HU&QH66O?mez2|ad3KxY2g~N%Em&0RcGRZDnT8ho*vdQ;t<3b* z=IYFQw(`!DnwDPjM6ifg*U2*f6D%U?x^1Y{4DF}evZ3t702TfT*^5Z=}Q2P3#$cZCYqko#;Un*!b1`>kL*4tSgSHnFFHzuhP-$00gZzNZFcW6Vc7`(oaR zTTWFP4GK3IEqD6}1W%RqK9i6VMmECO7s=AC+(HUrLf0;2Ji?)^M4m zggy<&sH%Wn3KfEJSg5VmbgZxlQBAqE4_``-=Fi?xuLw~+yoPzUB_5}<;y*8%6SU;Q z(oOR2i)Zy3H(3Jbn1cbFhcRF^K)!K*z^pWWMA8emxVUmvvEcg{aNR(OztY54417MH zWX3s+I5+@(^_pKu<&T1x9$|ZY-@^3?pBI|p^>~l`V@udOeqj+`(J|9G%-3_QVZNGU zhTp8?hnL#0E{wPUd^Qw?FnH72NZc05yHP6d>%eMpUngqu1GwsG=uDUObDb3(8S0Fn z%26U72Hq%2^bM|rQ`=rRf@Pe#!`43E6d); ztbQ`sa%YmleG#&sTR~QfEm`64C$f@io+Aq|ipw%hg0fhXB*?|8KNjF-BQHMX&Dt`L zDD(Qja~8gL|hkA2YHzFzsoG?VSsbM%zAOChlRl~DOjuhYD`jFrn2ZD*`|XQ zl50RJ1aBn4ghb4bb5ezmT33eaa#E56cP9X>BEewXkf>{3I11w^OCG%O#nx)32S^e} zZBdy5QiPVLBFy^z9S4M24_U^=bk>8zQVd_C9*PaT>K42|LOj-5&!?Pq74f2hwVr=! zQ%=6TBAzS9TLdfwLLBs2VmZQp&XNItjKf+$UfZ^2a0K*$Om8`U|E-0-twX;#^{FgK z)g^`IF=sCE7gj`BasR|Y&u$i)5|RO~lVFRg06)@W6Gc^Zkpw4J1vpQFN9v*G_^y*W zU4}#3RcmB?npek4dmw0_PJC3wcC_ zVKG(JY#l2eU|R_mG1y#!ORE8FAi+YW%}RibDKZRRRqH1J>~9F>G6b`>jaZ9dZKPmJ z*%AXK2Pv2^na8N5_p{y{i9om|K`GwP5|qliC_yRHj}p8qlkZW4gCK~}_`Z_KGK1YF zW{mo&1Z8b(kf7wcT!J#M#S-+?#(Wu;8P1U4-3*f^BfMLbzW_EV*kjpgvb4U{QJCi> zX)OkaNl?o5xXCa^Jjx(0d*ckd`jCw7mm*@4IstqQSy-=zb+Ba7-IGOgNhPyrC_$M; z9SK6_7*U-;Ba2uW%~`0GCAgG9e+ja8pwmgHWv2}PoP?x1t+`?XYKW>yHv!gzOx7D@ zB`!~~b^j$D;CV?kssWQ(Z^#irUgaG~>SA*HDdq9&@U^(EMd z!DI>ct_8!02iOm?TAyHvv&hQN`{*61>XKB_FnlC<5)4!?3Ep+<>g`rs!Lmdp67-09 zS%#&toRc6IRy{63X*Y)?xSLIAp9BjT+#x~aP&N1y1}%y?K^u}qsJdEWtU(`@^Z`Ju z5|z8%_Fj<3s2-H%kw*2Vr24rovY0NxuWAFFEJ4|EkC&h{pJydlw+;}Vl3-^B2TE`_ zgMB4917K1QfU_Wi^;TOqiwI3TBHS;jer5*EBpATbG>~9j25U=DYO|UIrHRK#a0U>P zLIHL~X4Y$M!IsRjJefHql`QX_EUqNk-~TE>*}oS{kZnir`Y*_^%=||Q%FK^RP%8Ut zfIi6Fdg<+}K##IJdNSW8sRHVuOdEBOi?>RGojLY>2}->$l%Ul6935nYX%d7rRaGZS zPzvgkCj67eAp8mLfm$!Vjkd_457F*R1DC;)v^Rq}5**85FA1^@CUpfk1H7yc?PObc z@m-c{Bza|dcs04pD=7`&a4`7W^5{zHzZgghjF(ihMMX;R?fQ_w1+X(1SntXOS{QWj zXw^$n$tu0wlB?9y4ohTMR^Al}%E~K{pw#jS2}&)0E5Xmwq1t^qZaU=H0q_oPOq!iA z57xE^cxqQ4W6Of9l~nAXla>Lz0IClx?+krlp^~F0n%DrS^MQH{sLdlh@B6%2G3pec zehyUY-MYyZ)DcoAJP40^0jTE8&b&S0jWOgks^3tAmOCn8p9(wyQp0!klc3bu!xEHT zeP;>EuD*=~xkFK#OVGCwz=jf(eM4;tcIM$%q6FngEJ}hr5=(Lc?2Iy5@9PCx%9LQK zXG!%Z=WsidWt25oB0;Xfq{{%SgV#vQ%SA>0_PXNvkotq9dKX~Qw*dV?Wqt4H8w-^_ z@Q~xbos#MwX5f}!mBs+qNw5WjA4#wugNr42*N-F-!r$RCW{%|vuX&biEQT~ylAZyV zq&$GzL3PbS^^%23&L*i!6J+?bq>?&$Qi4(^{Q)in10RbeKKd^POPN8ohe5l$4Dwn4 z90vw(StMv?VUWxfWa*8DqMNo(2>EGkS! zvGQ950PFzq%!w1X&q@~YaP2JGoGfvgwyzYfBqlzyjf{cbB&@;z~Cm< zbIkb^7j~nCfych`K4h^iRl>{U7G6c~A{}=TuI{(AqlFe$_OkFAxRrhe=nsJ2dMovH z552e`ZAPEfzr?bTbl%TPP&(`35@g5I_6Y_p!t$)$dUv%S(ib2z>mAj;p3LO6OJijs zTXK~xq@4t13uysx92i*N7i(r=5G*~Aq^j$ostzz0ROYOa)tzXe!cv^MAQC32WWNvq z5IxsbbHzqltA$FpNFE@x{ik_ZaZ{jLpEkSW=dnoHom=$6TPoH{_!;!rBIg-Ekpy`} znR|vo3l$IRotB3QzDGKLblLh?+A&M6*x4)B+IfN1n(G(T?L}TQ#sG_;mNO*=ir*^lB97cD|(+DN3zH?yC#bXPj}~H-uL* z{@UO#AAfuC=W|nd4a47eH{IhFseV36>xDFBnL0qZHpP8)nVP98fm7TmtJU+0^2THe zT&MO_R!^ou>(u_frzc|xGYuccq*h^mq3(ipYL-K3F_{uKAaQuPM0#g~8n3LF?B20K z?e0)~CsW8qHAA`cGIiJpjzurK-`l8;Rvz2sqj`OYzer!r>x%KqunT{GBkXE~VFb<| z^Xjcqf9_>i@6}jaoIm1{ZE@I3xY`g3d115K$}w+=Hir0X?ar^#%IL8@N(<%FH0qxx zGL;o+w1|J-PNTE@`&t^s<>PlO()o7?((&u;&4fux3+ICX7Bg@kfG)MbsuoZtiu9-~ z6{6xQZ>m#YS>?;m-!e=`o~cxod?ty9l<($I|4AZT`C%cAo`k0XuFRwPlSE3~wt4!e z#lj)B3*X`TxCuUxUSI!a*Q$kdc9LkVd@+v_Ul#8vxuy`C={&eO*jamdvH=uZtSWpt%%2U1TWZ=2G@_ zsC&#@nmJvhEBDQ%Jshqzmy{VO>zyjJXol#o^q5PfGsGUHcnkG(~KvqT4_(_7SMmT2ww;%xnMlpWu4FPkOO9cn2d zf*cD(1pPK!#Kx5%%->$C4o6}>1sG)z{Y386Iij7TY91nZbyG!c*^%eZ_L`fwFM;y+ z`FPWoxgypXJj;t(7WpK(|DG%MDoWyPT1z64F3uPGmCLi-2;6cgmFCm3w?(4!nNU3K z(b17aC2xy1%H?^Ku|Om$*`c(fg>MbYUm&)JC(P7EF&Nz2#3<{4eIlb zNXhE~gEB?)ck#F6mK2T$AwN6c$w*7a`4!$%z=s>7OCxOd`(AePFQ0Ga`cSW#s-ApeEif~qG+T4ZDxIqfDiClwQW(5Y|i^{xL!b|hJ zF|uh6PL0r~@E7nHj%z_9-WAc|fBjfm%wOM4Gd?TJU*Elbnijn)($(ux;6+Oh`Qp0G zyJDpBTyyHPNOW-a#v{ldEfPa&7Ga#-1I4Kf(d!^1jD2KAhaf~UnDTe-o(U+U#*0NQ z=L`S?7mNFpCtK2@#Ue#nC~p{FQb-AMS?ScF_(&@^b|a-B){|> zk1ZP1$6MgHN+~OT6T-e@sQwQ80DI0Hyq})ch_)_8dw#tcmEt$_DB2e*V{3ZHLZ)pK zS?2EXo~Y$ew$Gpm@1r$<;^X&4(>g)T%8L2Ry3O(R>rO6k2lirYq;O8W-4tWwt^#j< z6*bIV_X8YRQQFL){vV3;fGopD&VmpMefGYnSBIMZn`6xiIL-hbbEJS}qOmfzDYaXM zRyF%YnzKwSbB?N1M$JBgPG$ml>?3iXb9_V@E&NE-cNT@?5u%SoT_xj1D*Z^*$aon$ zB!dlkRea&u%Yi|xqpWxd$HCB2M;I{5!(V{r^cAJlXSvuHP;EN5UVL~C)0@h)p`#l5 zQPv6=eWmGCW3@;RxeXrP!Mb7Y15fB}%1W_JIs7`Mu7dZu{W=X@CAKPaUUvh3v_omq z%e`m~TC{h!CU76KR*1;?>qM6F?IYBBoyZQKGpwwbd78RqSrVGKm#q_-j+7&f@unIo zLJID}DJCQ~ses_i#uT^#kN(_0jZ!vo^(u+K5RbNMb;0|lQrtGtM|os4joT&$INOUdI=2nVdjf!S zyQrnSIh9hk17qw|nzmhZSJJ1_+3ljK6FUqP^BG$9)L;ozN!-z(W8o}Affoj2SQu_u35!}hg>LQ?Yg5iQ zzy#hWw0^&}_$MBxcSKKN4 zL{o=zy0eU){1Q#&XE^tDUy2PKp6ljuz5+Tr0p*1i;42~?u-}h43W+BD5SGa zP8;l7K&Q~%%Kgw~&B^5Vm6)&8o=n+C@X+p-uf!(h!PjWS0ns$@(IkxUu*J{4?ua*O z^8xe^-Rsl&1MtzEm$<{fhAUB~bfW$TMXGWcZ;~GrPjkn8BQhM$ zl@94SqiEzI;otaF$Fkxao3IORmBM=Pq1pP!3ZIQHyR;*{Z=o}7Qo*xXWtW)M=72Ky z#zP{(5l&tHUDU`w=<2t4>gsV68AS{{4975H5^X+=B4$nEB4#K)liX8|h*XEtcq(l^ ziqXc#iFE3y$PBg?w>yqrEd+k?c@9=TqP*kx64tidZE>24ZRL97Ur~ zPl~F}wlJI@Pm20Vq)h6mDE~qU2Fj{aB3C()M}a?zamt(j(6k@L0M{<`<$SV)g}4Nm zmVD_KOZneBy(#vz=%loNg)&>?ZU0wKW5kw|M~6;}%}Tob&7ibz;4Al@5iX@{G95ny zgYkKVl(XVg$Xi1-ul4x*0Dt@Ow|yvGKP$3nmC#Heef^hUf^ ztMzyzahD}A_ZkpkCjM|%Jka4L%!@ocBkwX)PTL9{##nCTWV)HCbx;b1Qf4(RBl0bS z-y0TwY2Zg)t7+9EHyNqaJ5u^rjXTA75e59Fq{Y#J|3@Jh;u12-0KfdTK|?`?5{JCr%*Q>>)4-_A%so;yvikvsL0&{Z?`2d(J zX-h6BklKuUnByQA6LWz%bB4DT(u8C!x^dP7Rg@QyVQC_-wQ* zAT0A_IJu=4eVeRBWn}*7RU8S)9yL~fBazr1am=g@>WFP%KR&<1pQvW-#3Fr8A;s0w zQk{+QXkwRIT8q${c)^hgni~YuYm~Vc*V4K~MZqj6rv9C3dE#ieR-N{0fhW}KGZ@uZ3 zbozIXB^9)QC4n(p&l-jet4c2LSk*YdTt>DkybvI*s=ATKM&w~y)kDYwMwNHhs%9Z0 z(}L!Mx!tNh;5cN?mXwQ3Osn!WGISwBJPsf~o1SLW>l+*HZE1zI*wXM3IfK_uS7BaVDzkNH(+nJ~a)`5#Y0 zVGOfPw<(X)vk+%CtL5h?E=^0N_`SZ7${loq^|g#@el{Ym;z*7&s{8wMrTC_5LZn&! ze>C@`eTf26$#;J7w5z`M=>4-H3(J=U`52w`RtEGi!}D@0<%;I`DHdEa?ziS%^jNwU z*5O4n>Td@bB4_Hx)C= zKw=+mWfz~OasgY$Ji|orG&l?T2u)KPfL{r#zA&;L?Fg5^F&gvFiGj5JWcGFlE z8nSYREIS!tGra{`1(y|JO(G=yNhuArxY#exqA(R?9L|-Iv)?zK(uBi1C;=S1<4!$%fLkkPqie&zBF6U5y$OakO6CwFy^>z~JLOVjjg0ZwR zLyPv?HiqM4ieAUqC<7Ul}uu;^HVreTuD(rQ%2RP+cl;t3Hk#x^Z5FM&=mKtZxx&#!$=O zfN57Xr8;F@5PQ{rci^5VxSLW{UjwXj5$ln+pQh}KVuVuf4(+&zd86Oc?wLiHoP+P% zmqfL|Q&`|~#_H~<&S^SuNu;D?z>3)2O0H667{qLFEVoY((e>^SxgD9 zf|GL1joAZRD5otdrmdI7=n${=WyKF6&9vOd+feH(;=Pb#50n)@XNhmzhHhQK?0NV& z>hKe$q@Bmn0sftOKXte&(rbQ?JQ$y?^(f%Kcs2rf8sNVTcxoH@scFi&=;zx4GPJI) zn@}&nl?{7gt9D_8*WTb&hKErl91r`$G>*+}+!ZRliZ$FSy(qg_H1+QBC-jUZ-gsKy z#p&%{Uo5se(#;C#Q%zq)b74i%WvnPZtawEa1Ecqm#>++wPp?Lceil2F7pqbK>moDo zDWs(tiv8nPT6rDwpTB;kL)W2%4%OU=zhIW8j7W6%DZ%GUl;lKu=7va233?Qc2%_@9 zm3xWd+;ohvd}0QkS%F%H?x7twL`w9dNN<8%ngE|i<}Gk)v5mJ+$&BmSr~drNCdsGs^Cp?n%G06c6>K1;2@&feUtd8f?owRQek@ zJvfF^e;1|7pYfD>3u_54B~Z^>SQB`4AdS9-1&o0SwBwf8<2?bx1d8{=qW%YeU~;nQ z25tUB^i;Nwroh|cZ1}>R`hdXjKGOL#?L*3+Vzd&!o5uYqYB*!#F?T)g8zrz*aMiny zd(WSkRMzro;B_Ga8#e3Hj^Q{xTSs^NdHMMn@m}~iD&G(Ap^1Ns`yGDt1^?ud??0lM zV*qt4#XTQCdg32ZEg;vtQs6pPG=OH8ifXjwA6yplqa&rFrQ>l5E4@pT;Nkyy(c=c; zdN0UOzi;8Qu3s)m1}#0b!DXP$mH9>8i#FH&N|~tbpj}g)!{!$JS80QAty0Vos$@-3;>%=^(j9Uj>uaN)umm3IPeJ%eT?pV#^Zoi0;Z8!m6GV2QCcM$ z=~u~{T*I`=&OIaK&O;Ps4by!5p23&Wd0@6AgSrpXCMfw&)6rpCin474l@8M~qVt~D zhcZ6KG_eKnxgCyup0bC--;RBn1`XFHg~asL&CZ-oHh-S}8m`5K2U+9M0Qg$x!_QOd z2rX4v)|dK^&?YIBpQrO9v_a0-us+<TwC>8B;iNo|8FB7#%6?uOp|l-NJD%4L zD86lI+GxaA8BWJXYdw{phEeJZ+Dzq(VYK4~m_R`+++6Lv$PxQk&zoTrf#s&2( zEgnqWbj%?WwQ3-kF;Q!YpKtWgjfq-ECHE!DoCMvpdx=&~f=*LjqUe{k{z}M8H11`D zZ;hv`{CjFV^_i><^;wbu%ZB5itZTm2-6fMXA4l{Ps8X-`@UiGB8d~&|HiSZ^UC!Z9 zk+P`=O?h2wr>vghKJ>a4=Tu5s(Dj+xR%Le!TKNW)*R=sXGYeK*uL0efrOi<$HK1v; zwe!lO4QRw1ZGqCL0r}0<{;Jdr(-~IiBSjc(j2bSNaH2ji>h8o*PfHzp3^1>ZRGU9q z_zZuR&*Z{7d78ShrrcA!JD29T-H78~_d?y8S zR~QEJ?-ZUL;WzUl)HrwcjL^I#NP!_;;FD%lSlRtGPZaQI{hX3=v0mq)%eTO!#Up9SGaJU0}c)%XK8 zC!uwmUbN~3QAKeD(}5Sn$~J#HJTu($s2U!8ZAbUP@}9!4BmVyL{zf;(XAy7_rm&$= zNOnZ^pfHO6z!8ff?4YX<$dq6!n3)osQgp4d=2+W%kj5J0HE-@SIWvZj0{*WISvdjU zQ07>X7|~qQJB1rZJf=41Xes4YcSO;wu_9Ud!bMxhin#FU$c@Wyp3Y>eeeD2UA1ivQ z?{Sgov$2?e=8VJFtcDo4<|$;x9VBs6$Ti5@hgOdhvEhBTU}?dH(Hv4&;aCjM1irgW zXUB>3yn5ikIxua#E}vmGDr|TS-L{_w4eKV$93Svp2s+@}nvOIcfpF!|3ezi}F0o@BuB7Sa!Gq?cfq zY$CiS5zm8luQ?b+V*LPgQ@7CB@nXJXyX1NwVzwGwIdvKoZW`W7aA5^;1#sFNq)D!T zJ8q#vFNtY^@459B!0AJRXwU?VhUdHSS%ML|PrK-=8``UfJ%(0`jgBS`(`wEiB)U46 zT`I-5+LbEqk%L4Nhf?(&TKOc#p}iK;(I>@}z}OUUz!nNSn_kaS>JTjP=MNU+%O%u% zmi&ewYsW%H=wB|al96_10qq!qJ>9Z}^w$uPk-m6=7k2YwidJJb#=qThxSAU0{7%FOb?|>R8{weQx_iC)cIh0oy(($K6pO8`TWL$rY&m)(nwDN+JM60witwYQKPN3R7me|YyChIs?+8DMBd*euLMgl}z{WZ0E!ynT;?l<p+Wu({s9kb02X2 zf`#?6+z&X|L3Uk*EC&{A{Jyi$e_=_Bz<`b&Ogt)(W%Bc<#1O5ia;FmY57EL?E?Vf8 zRX|tOpewFKi5adrPbdai z8!OR-P%S*~goUQRg&(GW(dHTr(*@q~D$a*43M}~D{{#QVUtYzyk#u#W1;5?D@tes- zAkJXoEwAFiAUGZ8so16#0^D#cCy>`)lwBDUm_2{epvqc}%uoI(U(j6eGYe4LXhroc z%GiQwECxMtp<~$}bbxuhW8=}@!lSOiBk~?RX8b{^VOn_jR0|K2M*E%mglW;yFCvXy zmMNEc7VY)lX=WHk(WCaz^)Rh<-HRtY&NmzJ> zKZ?_LWGZ82hMzq`g3YY?IuF;?bJTB{R#RDb zKTacSsg2&UaCySu!W`d4u6^q1hW!xAwmaXe7%yyIVED+~rQIUte}n~sK60rVkoRJ& zE6*)C9!axqIG>|}kFD*o<$Bb(Uv1I!UQZ>z8Y;;4y|~g5&!_ zQ=b=&qa(|;(8zZTUm7OqX$P>5Zj#n#@01vFl;WlQ90;NH6N_t>>Y z^)T#$BDrcZFmoNaLTjbm$)RN{&;cH7M~7Cx>t4yBn=7=G^qdE9^f3(@#%W;Q<#NVP zD%jEzZ3t6kue3=;xchjO(_c1D3m?g$?3G$Z$f9<-L7J;HJ9B9MN-Zw1Dfn@d_Quo2 zTUyedm0DuRb_>6T5Nc98x`lXUQ4U3~!Xgatny%7PQUgKDLlc&xI;@ASg^hW^Y<=uC zFUPAm1w$N;+dP-%tkSZ5yyjw@4;m^+bS9H)wbsk;QK0c~xmr7VW;L>D3DH(-DgJJV z%sSa5*RxtXBk6P<)kRsGRByVyT5GS={+?Q|!G!(X37WPB%QibP&sd|iuD=$mJiNig z4TYQf&{DmvZpI2ZhkwwWWZ@QsZ^0McrVVSf+RDbuG+{0B?>dLxTdO^zyroiLj*{Y> zp>n&AqOQ9&m&#+uF*ItORy%KendUVefA8S$QRA0k{u_orZhBm|@5SinXW6T8coD*U z9-YPG@B)PSKnXt}$>I43^R|}NR$j$Q0jCX`rF%8N4T|o3rf;EgMqI8kxI7FAFFf)X z7{F=p_?GKM|D9Weddps~Mdv*RCSGaC46g2>P%o_HWJ&rTG~NZ(d0hu@#$k83+`wT+ zb_h5uIKw{}hX(4T9V>FA-G5%g$i{hNF;zoh_Imkv@!#M0n{NCvj6WeG#?jB~wTM0s zqF~%0m|t(eTo#7MLkR)kGtOvt>QAjd#)|2_{Xa%NQ7>47BT{db_L7$+%<=%+va1kP3*r;`jehrc4nm})F&s?$o z96G;IYZ~($chVqA1JNsh*`ISK(8?R9lQ(H?m6jjUgiTmZ?R=5eZbD&$exL)Jv~1;( zUKIU_)-=3*Fa1~*PdfRg1QQ4RfD@lst8CPZ-upy*#(S!Y36HCW8tX2J(5gElcNrcn zd#}f%{Q{V`wBq$GtgbOyr1Ih^ePsT2jMhLIaLRosMpGP0Oe7tR1^4aVZIu4@9#84t{#;H>++*IjBO;H06Ue^a9 ztlik_wDKJ-qms$z2^{Dven*SyxfbV-xdUheBm&H3(NDF)C{P9;w4HTO$g@j$zN#+* zj0Ma}^F|u{dNh}z_NRIrm*000^?4WTk#F~+v+rU#`eiJCF48gsBY*KKt^*!*fM1qL zlNO;LsarxT7h!e(pKEk{5lrUBH4=-pn=!i~vZa~^gt zY69bQ_AEw+ipw_6b^v5Q`zUX$W+2ohMY)egR@2nVhW?xE@aX>}IjUHV06SXGoYK7Zl7qhfY&YM`g<3$wO zC7gkDDogXD!>zP2&V816e|oO9Hd$G4R6o#>wh$j!uXDfV6G5HzRWsg9;N?-aoH?E4 zF%s+{y{LK9;km*)^wn$~jgMI~%H(W$kBye!uetjFZB)x{oPlPGRMCu8qZ#8`cJym6j^>?zZSx7E27qE)hI{p%YFZ=)>=pOWTjoqW=t(;U0$ zKpSn3G8Kns+hQ);(M1Q^YEx48<47UP5d=BLfY)ThBV@r*)j_xyyn)hW$%Quf{ktVi zXa{3$l1+=+VKskJ59;#(HgTTrLCYS%)aj)=B-(2k>ASJe!n{oHkdOR_-a$TaoZKCA zS*)j!FLid0Xs-!J&-bHo$lD;~GsPou47xh}M;9o_mG<(5E{2e-)mLyY$;`yWZHu9L z>y{nQKWAK37=ptV5c8}U1t7_*sY^YN}M-3OpI%>1jcp2W!;q3^A{8|NH zU4;znH#wh2qN#r;ZLJd4odP>!Z{xX6w6-(0Ft&B0(b?d;t{W}Oh7#I#qMO-TrgDEL zO7Eh*r*zDs<6YR_MpIx{t$+8VEU)4*u8!yXLcVwm1(BDdkSf{8t5kuCSd!^%q-lb7e3tA8%u84TT}_O@P$~|AQFxqoOk(f`8uEfd)Njno_{68t zCV1vxZ4>7oqzS#SxAxV3I@Js9_6FKqZyup%)1}_pGoiUyhLD;>+iwD1^A48IZP_-A z^80A9O0WI2sE@W;c{!VU_Js9n?= zwpj$%#~qypwBrbFQ{?y8-gFNAx^!b%jOw8A14O?tFPN!U)`i=fJl)INpV0LIV!rp- zdf2WR>FZaAHVwof|HOJU^l{uLdobE?w&lM-E5?R*vf)<9xyLsFyiI4`rwq|W$W~bI>U8#8=E%q)f}!oK0o!4 z!KtQ&(|*9_kzA(ohCgA`W7yAEpKkG@BZj1n7c-v2GBo(8LeRNdaylcMaf8>ebkxNWM*cZk^X~f%ZN0Iv{COLgZ**x{*;O{d zI?x_>Hw?F3g~^C9L+9`lXrN7?fmsHhmo^gaFNf3m91MWNHqn6`EiUN0mHIS35(GVb zyy%Y{?P2BTHI$vJrPR1+l!7}u1+L=fHF|yU>OW6M*nO;AON(-`-`owvJ9D+L@FZmG zo%W-wkX)#WIVWIyJXec$E=a-I=jBQj=Ntg*R^m{|?IP9Hx)YYqco>;%>>%)l?yI4y zoZ9vH%}C=sk>5hdP#v{%#d!aKr{^E8p_K!0D*6eNeLn!dEXSzq;71B>cMa4cYkbTu zElr;i@?4hb0(hAT8d_~==!=hO+(0z=M>f))ftZpvTSMWG<1k2TquhLy(d?tj)=&pd z%LiX>(rg)BjpDW+PZKz;%XnR1eCF2NEqQ7Jef+p~NU6D#COv_9Ln%%LaM>A$r>|Vb zB`e%pk>*gIo4IYBh4eqrLt*Ij<8Cml_i#eDISmFLl(E=TfUSwYhr8Gx> z7HJsTXd{mAQ57Q5iA3lyVIFEX3K9$U@@4rz>PkUn;I~|Xfa>K3zp`S-2 zd29Wg;{p8Ptu=DK^&3Xusd-VF zZJILJhr)feDatRnd+&=0NwYe%$5;DUb%E|T3f}G=>YnbWc{`LZYtx)cT6*Jj*zIO? z{m0U;UVb*M?9#JoJ1?;gd5U#m8$1G6H8PWG!EztRYhRR7&@sPMs_WD=O7qw1guMBf zkC%UrkJoDa?!{mA9QO!+3~Q9nmeI@rZCRv`L+@}h)_N-M2f+Gldii7#Nz*rL{vF2~ z%N(J`@**!Y<{NRmFdKsKWJ_2swHdraEW9_awe%R_yxMRcGbuLg%~%8AaMDgnbZZ&m z!{IS*u+wv8qZq*V4V^KPm zHw#&)*Qx(jOn?ig)3~kJh}&vtj1TOx#!egI^|NM8k7?rC{4 z!#(YH=xKQ(XXe@Xt5P~Q#F^yIYN_}*oPAH4E8Avv9|ZALe5h3FVF zG@e71?uJ?`{*F5|d#KicwliF;!?W`6y7rF-9OdwLe67RdAHk=Ywi)sHz*%L$NuXV5 z;9!~F@j3-R1)e9;)kx=<6LLsUm*N{BG{_7M=FmfCXg!Boo1xnXIntO%O-k#hdgtwR zXc#$VU8rv?w6HZ*Z|Qv@4r3ACwDR&=a^V!l*sR4!Bc59*FT8$d#K+s>4;k^Xw)p)< zd}U9(CK>JNLVFsjJ)OyyjRlOXMrtQ#rOTyrtG<#z>l>-DfiD@83htbyZQ)6O3VCOs zXmh&CxqU5a&!LHCXgY^RnxVrSdJLhKU-{If$i@hD<}_Yp=-OCKcJBGXv*h5S<&3c2 zj4EJ+MP>+V5-w`e1PId+wN7)^r^y@|`$MUFaT9f{BeWkHD7zqbs;|Nb^R|^z?WXF( z&M)_s(l0@NQS?qzwTbgxgmxn2E@`T^S3W*ove&Pt;+Zdh<7QsUT zR};Wg*iNnb?`U`M18QG~QvE)x6{vX5=YDr7z1%^q8`I+=JFiMbD{zv)6o&m#!MmmI zuREy29nNvkQnOBKtD0}%2n#2Di(8JYM!q|g2>H3@Up3Nlww1cy@1#EH2<^IA$J&kw zGgI~2T35TPPcD&@`6TrC zUa+trgfIiBOM5x=&mnN^rM7hz1CY@V^+xnbgr4q==q~|Cv956RCNtEq4?;^3>e5Gj z&^gnLKFHB8nxV+P2o2UlN%Munvc-1U4wH;7`#?@Kvy1vH;tnD%^dR5T4kz}g9sl>#7K_#gL3a; zY~qMZdPKoCpGf+fBfhgl#6GV2Qs-PX$g#%~J3SX@>n#!Aal|4^MC<@WOt(aIdIF4w z4*;WadaUaoC6ZPH&H2kgtGoM+qt29zYCaHAUzdxT%u$~rYVkl=&N4j>w}l=@^jtFp zF@0%BV>QT`Z$@omgyBYL{u2oFF_R!+r1Jq@6nQHQN9?fCng4|P5I+rb{0ViOa|c`k zJY*OZ{Natepo7#{=ND$wl0oXO(1kn`F+5@~H2$NQL_GVXS~oi4j9yvn|1kTG=7nG$ zFN8Fstxu}Qojcc+(%ix7XUKBlYdAD=lhlg!K+WF$sJ5E>Gyn!43}DXO8` zqA03HRf$VcbqQ5eJ!uG~qAsl}D#`D=_CAvwQ_u6h@Bbg4ZPwX)?RDF0uYEcDoYRd7 z47W6Ba3oH4g)U+m{pJ<^xb& zp}%9A%i)h_Z@g>A(-)|6gprES|9n#|Elh3Uo_k)mv{jTjjYt`3nWm&Pq8TGCQ!1=` zs$3j1-5F`|S5`emQKKvas)qbjA|Cqju#Ex54U0->`6$Z^rhjiNp`uZiYNj&)1dp(v z&BE=2=VEdjK7j3r6w)R0_bEdxwnEG%(P7;6bP znIbooxR;N&v@n`3eOf|iCs-PU9>LCoN4a~NayXb3ln7By0I6oWB{pd6<`TUAMc(h? z`_`Uqy$3-wGTq{8WyO6=t}q06o)uLcG&JYUIFGvcJ*eILP7Es_L^97qp>#_F|9P4e z{7Mu?UivA->J*`g>E7GiZ!?prvObi-{4%k2N9)xfFO6OyOQ^9#H3D;dQmz8_??e=OGv z+qGpouZ%c|_tru6JqImoX(N`qJ{ktFzXY6qR6=R)STUxM_l5i=|o- ze|>}(%QsN@=O)v{5djXp{gowLX%Iu}zq0H$;aHh?5gN_gWwBa*ZX`~i zU9tMp=3N#)n+4sIKN7RTD?c7vSc|kg z&uO8>bn>dNzq5=4))AsA{dv{*nF=e*7WVtEtfXhJ`8IKtWoc_m-aC9Xl;&RZjR?>2 zFa(uy`1^}yOj&t_veJF>ns0TpbDoDR+AR>v+j&%c!}o;p;V3%(8@Bou#VJ(!8@3xO zkHWoq1XbS5w_+uOpn84ED5`tY_ei9NhUY+-=bHV&GnUMASG(o=v&rTuvo4%pFVr~C z9sY-Jrk`>+e_=>m2<4ir+0G3fiNGbsY$#ewea+S);~IL_VjX2%N5y{DQ1@wzwZF-@ zfnsge{mL1a`<~4@!v{E%{j621tn*M1M^0*98XI=&mvyub^^9xjTl_qR!4DgbM{}%m zgYr*Cp!`_b+ReDeUB}=0H(y3pvDOBw7phpR8aKFyRIz?vlQLQ$(zI`3ZEoB~@3pYj zc5W-SS%Hr24vhce45zO`Fj`uRLdc%XBVEp{vhX z7b{DfxuW+BLdfuk&e&jA0V~tmorp+mSoVA1Y z>!KCotWEi%;&^KW-5qBQ#DyCds}-g8-?TN|+EA(fxBF7MwYN!`@Q_+&ST`xpJak{r zu&xYJmj6LB$vR%?`3EUWt@)OWyEuACa|p*rt?yYAl>v9%{ob=~_c!h!=SJ&G_=(*P z+hlCCCL4FSuWYoM%*wzz?(18uB?>#9yJ(xWf>F8AoF0B*jROkMe+o3_QiT)N>Q!HsoKzs3owO;oFx^^+*s7Ki~ysHd`pC-vU&$6;SLH5ln zyFK+aHhbvg3Ao`NYMW>EH}0kv^QeYt84%vX~cWEijo1 zcu$-I;UAY}yk$TA+CvH|mBH|1mJh*X=GV0Gq_uivSrO36NNXa$epGhVqcs1774NA$ zW&IiQUOWY{ce}fPZ+*pxCqK5Fw$@g@ZbmEb86%cF@C_ipAFOvRLfdj9QrH>meM?dc zrO==Los1(qNiEzx&RVwyD=9X&&s}TpiVO@;Y$Hud!CCili*2Q%3_t50X|*-%rDqAJ_s{FQ7}inR5!D|C!@HnKHTW*wvJjcntU5y$A&7+bvZ@-bQ+ zgZ%Et=v0htjMDU&yF;uk-l$wY>YmZqHWZIsw4j?!Y|HTLp4rs)s)cFZY+>tQR@T_u zaq+f>crwrC9@^G+(5RfNMh%~_O;M5$)23%^{gm$FH;xk8+g2#Shbh0kt*c@^>~8q1 z?H6BQyIn8XdMKz*!@Jq$DsO+|e%Q_SYJhUP1r10-2{5)M*%k=Y4k@-M2j`4Tw?!z5 z!|q-%$u><@j=RYQm|sye1~Oi!IolyTfL-D=Bt5xX(5~ znb(q{_SinP7)+Z2e5O6g%o;i>sJMe~Z^-?mBGSfLiY1-`e8+ly_xuGmV|b>B<3&rp>Os(Z?G`);RlxD~}OvG=eYZY3@6Lv5GZtGl-? zu}`-!R?8Lk)xOGxR_^={?Ij_~QPrKl&)&~hxmMlX@`!!7&6=n6ecJqrHJS>Zw*|Pb zeQ)1jv|iIFQtlYv1o5IDH{R^yF#6xn3V4UK0yX<WirR|UgJ_+R!W7^`07pVRctU-nS^tozH}6FXeh*+%}neBb`BDyOx!3WG;%($X98nBFly-KOTgTW@4)aLeJyDD z1R}g$FNypLB4zYh0zNp{Ku9q8;gcocd7Z4h_o034W0m;<@*$$~bAQ`wL%7j@dx_8I zD7o@!_j`Za-!KBDO0lrWQ~0UlF-0E;!qY*#uiEa(W8W(g!=X}enkE<9tCykoN0D6{ zDY*f!_LZX~FYQ@c1I3=U)^vI(@yovb(5R=~e-_(6G%6qTbuTNmpTet5gWV&I>TruP zEtt;xs56zr!8FuYooT;TA08Q&Q1~dA9{Q@Qs~qsSh9;-oU^st(wpi6rwO0k{yse)p zrcrL+$n7>Y2k$U!K&$O)KYq!3W_k@^XnVz%+o4Nf{lvEzDQA_jRsgpquSa>M=F+ezc)D-ck~4}qGcb$~ zxnf@jmg5t^ypCgUH~aA>JB}MQl+DG<#KJ1l7udpBwVxNRkD1ggJoF*%m$QGM3@25@ zQ(ZjEngoveCLXBK(`I3r7iZ%MfEv(-j~;VnEOG4o{sI>F^PR==cCU7A{;F6!{>6lo zya@9$C{;5Em-hx;#MbM2<{6JOzc`|VUlMuygcA5fJMaSqYzHgsut;Zz_jPs%g&jPi zyMGjmCn~f| zk+%(bdg9(q`4gV2^=``BUIP3M97@Q;38c;WI3uy^Bp&baQ`NvsYa{8vG4Q8wopyK* z^gM5$aAQ5d?i`<;@LYe@R{EKyd5~VXV2xfhe3I)P0pq&3Qxp>_dn|9=u10+=6!{k7 zdmKUU7SkqGb;T_JvYuG9m(wmpxj%@t))AZ4mL95VNM@o=5j<<)VZijy%b`F%im!ex zkJz(tnGoc^@f!_(cf2}DvX_F{z`15HRKh2@$~7F=mOgf~P1f8Ps4xKy#9=QCe%lnng`vA&)9IC^Gj@F#+ZZH|Mi>#DxE^m(ibHySCJaW_1abGT zVq3adK@F+sCo*N8sVxOnL`#~%+@ult`^)08Q$lQ&0$l&M7Q6MRv}n}h8ea(u17i%S z9+&ii!Z_BAFI%xd3F2--enOc`W)`9|1vN$U7-Ht&j3~bV66d)wM6dwQq9?zE%AItT zPn1`FVqG3yxNaQkc0%3$y1KdlsGIpXV^8G~sN4dTQ*@QL{G;;Ybv;54K;62iJ6l(G z^?$0H>7_A`Ks+w5&{baepDJs0Jwi`LT?gvwn{0zub9{&<?vb1{V@F1mAjWErIRWFsOxrm>g(w?l(W9fLI*gN-`DFij2O{rCh#p3Pub>Qw8**N0mCC{lvPbQLOy zF!O}rm`!q6XXed+)6mLl)IXZyoDSy$2=?CupMY|N#>ZUSo6lU}G}9?}LsM>uRE|`C zwa!0;xUUmp90=3_fm*N{KbYXTZx#jR#j{qLm3d0}n6OaT5mD|}gFz=67VOB3@kB^K zx!}VJ4LEtr-69&q>Q|OAv6)t(6)FgiMzb~te5j=KI{O7TEl*y$D6-rA9$#F9RmJr^ zC}#$GVb~?k#Zyp#+BoDs@H}&2UW9{2P_jnjOgxR8ss<@X<7wtpHRRd7z~v`SMrw0J zzE{!_c^Fji{;;w9DvF>it?n*CxB8W}qP&3V%9f$urm8ic<#8ytma))*VSr=PJfo#B z1kvJMjU20kGd}hljod2EhZ=_Ludf+8HTc!MdCsK+)V+%8|FT{)G=~_?SSycIAQP0U zk(rHrJw97voEPpj4R-$>5stbkn7lC`0h(u&UtQ zw+)2^>Uj75FT8(jLjwX;Jn`LzW(2A&pBm%EyS>JHr=Woc1w?rPTU_v7&DXT|1*IxE zs;R-59({w5&-dnZ4p*k{`+`PJ;c(FC;IfF5K%He?nRE2f_%0B1@bHON=VjF41=}QX zT_9#Q_LlhK3Z4WGj(LVYsisy?>a?Nl)znujruoYr!2|fRHWU@44$Yjdm>Oj?<@uZ3qr%lXih!2aRG*blVWhgz(hj9!^2ldZenPP1t(2yQ=P&V8 z{&Y>0A<_gOJmfZ)(|bP3$}b^lih#z=xTQIrs;4%tyawflM7(}Y%g<>}uKH?(Le1SR z>#IAZ7=JZTzcpsYG;Q zy#G+1`ycXI@I;jHP&pMtEH8R_$I1 z1mMY4;K@HW{wLmd07+&k*&zteRo=w# z77h9gbC*hvrA?hMbbS{~`TUz0OBJ6}XxIzti~gpV@{JX~@sWap6ECPWDyC{8@u=mFp@%Q1W0bh|G_tE2WJ&|ltgh;0 zrCto#yQxDJXAF($rml_(eoDGTkD7+UbdA7t%_FunDlV!Ojz=J&-*~*a{Pqf475cZ) z*Xo2Z&4^WEEd*{pF*Gev9n)|#Dm;TTGG+sw#m0+Fd2Jks-yNEV81c^UWDTcR6h(Ab zr&P*qDh1#n?B%?&3zG8uilU@y@>pu8_=oVYQ8d~0d0LrU7%0{>d;3$ zqI79MmHVpGlxYoUNndrDGN=JH?xzl|T3TODqFDC_G}{L6?H?dwzTm({w;v-N2j z|N7R)lc(rdmFv@`m(_hWuW3%g;jbnb=)+$`Qc2O0WV<{ z$kng>R*%NKqK?VDp%LOm+xNiL6N&{P1v7fj7J*%}FUMG(g7aJ{7j#iYKBpnRnI;0RTWP~+3V5V!D@(-QJXdmM#CUR(AmN2NdbJCq}Gu+ zN$QKH+bC)@M19tD7r=xe>dU4_j66h*t!Su%4uKsfj#g=P$u(3RP%lmEB)nqD(?w9T z)_eKx1D~6>Rx!~BLqV2VUmdD$F*%U4e3&|4a!giZO@7E3oUFD`rqrR8$!eS_47k-_ z!*FvqS*_){3O5A+@9Edn?h*G>9R}~TYm~!KANlf^w!Wsetdy(?$)P>)DHHwun);UD z_U3T)xk?@txD;a3Ps7#jf?1>2)xH9l^}0Gj>EDPRzOMFo%+0=xe%w3SYi>3wMU7Ol zqG?Hr+OXYHAnAQ`nO2E6$vtj#8hJb&e+|Ff!k$*=yy$518KDlSH3((A=HQ2y*=?r_ zI(VW|t9u;3{Q9e4O1y(N$!#8?_E$cOrivp~JP%%*PK`td%&$!kN5T=tL{rcx{0@(H zw;ZMJHY&qVkg7H`W!3U^*Bh-?Gb#bKslynxrIH^-)5f5?9g3n&W7OV0^=lUw1vV%9 zSaoOJ`WAQuM(F2}*Z&%}2es=&Tz}_-FO_N6U24YN*N?R8uW$|B9EE)U9xqHR z3iCzS8?RQ$Z0##0+95Cs3VF9?NsdKpW)Ol2k8pRN^Pg zU(u|0yBTO=VwYB1TF?hCSqcnni^!EzScmHv3JOQzH@li|W}yRJoeP%B3~^lv*Mw^n zl&#M>3N6)yn!`k53JUoxVB563Y~(e9GqCR_C}!N!NGSX?vbgAO*&BktzcM!F)-NpV zJuEj5i+WWP3MZlB3?N?62#sgrVxjHRV`XuXPd*KufJxAs)k}*mSHOc}pxdcBEtsHA z3V8#0M+)#p0E8N=if-+AJbs3MQvnyfA#@JG$ZcURhNv=QGu~hTXs~DPBHu$lkzuM(Oidf0&scMXXp$(qG|4f7cV}e_oTOWTDY> zYtr^CwCDPobUjOLSm9A6*aKc$igwa=kXlbw147)ocJpjA`Wetpz+GHZZZpoRDYhBo zvViNo&FI7QU^mY;W3*QH4M7(V3)H0isp=ThZxO}#Ms|&UowV|eo?F)FiQKC_^*$8z z3JSZy50W(8mB?$Z3#{CG!{J}NshFkVY>S{H)6}S!)|S7fQx+C9)3puN_>1z{ieE; z8*&hc@Ux?zM$qav)hNG5QMNM2D+E5p1mPj2+i$8dA+yTlnSP}WJ>Dc-3MY~Lg-^Iro#DIl|Iz|7gAO26Va$uk7YU7uCJPp4>pY-7Ne**4I z$Y25QJ=MI-31f6XKCtNYVFM1;l)KQ@d;>d^0gZK zOUU!QVvSt__pjHVMV{!!TJvubBFg4ptBJEstG^y)oP3LQ2d6;8Sp}S0jV)Ar zhT1@RCW3m;P}?=U>R|$=e4fFe$YyQ?4h><{5yIj2Pz3Fsp*HS)LRW`(nfm*uGxB zRx5u#oLbJrmTCtYG*hjOXXD?QsaB85_mVuf=07F3h0~Fl>h{d9!M#0H_q=Qc)PwG7 z)Sm;_Md;UD#{OVK*~>a<`5lA?1?6w!+S|V_AfNlFxe1Oo;rCo)^~%Yo zpQqAUxW)XYhbRcLdSM!|?~p$N#J0d$6LrqIPvRGLf-|;mBKN3TBd-$_=>%c9cVl)P zl&<;8n|K*;(*OvF;L=*7J(cl+d!VsOAIw&(M2g8t%>Ke1kSv-t<_Qe0x8a&swe#?s zNk7h3pHcdTQOF!MXlR|rGLU+FvQw>p8afGl5NWPA@WM@h0&ZX68eyC&G0$IlSV?x` zJE634jv5sdgfbDSpc^J)>o;h6d>KahbJT`4tX|~@wDLscx1u_8A@-0k8aY>us8$nM z<8-l_&9$w#G)NogbLXl7_6u6|i6B2$ZC7yulefq=2AziQs!xRtMnjH2h7WbP4q<9;jD7$ND;yD$i3pH;x3Y z^fGzn+~-lKE{|OUbDtEAR)sK{I}gjkwV||g9&SeW1TLG7eRw#bg>wZ46rP*@4)cp|zlaXH^`;xqW8KafI zhw^AGuf3MH2zkNS0OdVQtkEu-cseqa1}#uuviH$&CWO+y1?m9vhu}vsi`0Oqbgg6{ zYB#}L&j)D(=Ji=Hk!LI`4WR)G)sU_mJnF$T2p9GhTHU+I_lJ}l&|6u`9a?^C(BwP| z@DZ=@nH6jZX=U-DbYvmsy@x}nWTD!@eof1-5lUSbsV;2aDt?r>PBnMSEs!%3o`tLf z%v*0fB04}7T!Y@ul&#J)^Q~DPkL!u^Nw}G=D~p^8?`mVBZda$%t<~$GtJg?Z&%-FOmDy8o_BzxXs|naZSI_l?dg*a? z)~E{I=34!IR#Xpxfh=*Io(+WMk9(KTnI2*a<-$3FyCAZ*ZT?O_Fwgb0H=0;SA zmKC~P(pC6QSHaWd>!y@9`RUBZn|!sdtYemJazWfIiAV0%5&uL-+#|lXk&7BiSNTX+ zZ?3MMN2nAXW6G35d0b_PuFS3%>X0N}bE=N`a2;`vP-Q0Cq)C&ktI%Cn!6Qv09mjr| zk4qD+D?5g=GU78eY1-?EH_#FPhcu%#Y3k`J1n4Swr1^bvd4tu+d|a9$UD-U8u{4!7 z!5d!Cr18}ezn%Y2e=8HesV2=$AbI@ltgeDbnlE%53p87JT56N7tfwZ8a34QSnj<>m zpX-SK!#004klw#VS7F%`D&)mhke#gHGjy_iS*zPpU9G9QS{@x<(aAY;inJ&{LBdh5 zkFKm3Wo#f3^>#|)5otQ&FYAbVbeJAXyEkJ4W|bz)zjXNRb@(1(B6JL2nDV$V)pcdL zDDwzon%sb2YFw@B4BhwOLXzY=?RYNEyIqgQ6c_C=;T5*{0GQKaD5`@;=dk-y;7}eG5o;b%QYNTl@Cu-!zDP3 zv{CyVD}ODM1$L|pDC`-eO({l1WnFHt*K-dI;$bdHS6RexpCI%-I& zD`?0k$kL&Q`SB5^oQqRFp#dnjtX|JmSX!m$9hysOkmVIfeU7Gn0|lxuBUsuowUqvnjTP!(XX%hgL2Hi|Sy3 z*R_1C`n!Huh|;Ey>th{4zbwTLNAX&!_#P;@Ky)pQ-Up&F>nZ*{P#Aocv-Wm$)V4Sw z&RXhjaa4^AW>B|*8ir{`j>}qD8rn^0{hX+E@-jxabML7E%3t5pO;B?wUiDa`qq8Yw znHu6;fw#g|!91};|vEJMz4 zHTI{tR0eg`cpAFF--c5)Quv7|_x7A^3+Ui-tZ3W7(kmcEZ%A=eS~_(d#dB6T-spIg zR_%kwHV|O=M^NQ3V6N|q_;_f1qo2-+7!BO z(+Rynr|XA{@fk9j-*u>srECOS*Uwu2qjg9lE!< zo1)xXfX$2Od=AWA{BdznFLJJc04@m7OL$jE3`TFa{b6P2t|A1VhA$aq~ak6a(3QSq{j z_z&UdgYX_l1BL$z!;cW)$;E0-%j}aFZ)ChC0dl;yv=m@0n#0n*9JH5%0TSfEnIJ$B zI6E`w)lTosqcLmX7nNbA6gt_~5nvV&M?1^2Fjxw;W!v4uYaUZXH^fua6!H-y-ZhU) zN2B9e9_2A8`k)V(r07h=&hYF!n)VTx9RCRWm=s>D`ag9O^lIsv)Zb6uW!do|P;~ib zZNNCm`NYm+`4a+4DwdC#3avt}5AVrt{f!2Eth!t&kW5cR#DRBc=EsPq{r$@0=@;*m z7HyWkF$bb);Ku?K;Xe0cDgi;ut#`R}^g(cs2W^9F+QUKEKu2^t^rLLrG5clH&VHA+ z=E8yIqt+T}z6ua@4Q)Ax4pi+os<;=W&0(`O!o7T+VX!d`sNrxaznq|vdodQ@)_%9k zUrX)@MxXj4DBL(f`9Lw=qwYsACO!2oRon;b?p{+|gtua#wUo1KXyiV~eFv)S5E3Y> zZ_vtp=(n5je$yQ!f@m`2+aVipEvWAh4QSj-qYr_^ow*dUA5pDPh0jAES`~zxBO9hZWAmeY8 z07VE)0G$Ig=qJ#*gEyDrk=-f|e;?i>z>Se9-@uk`&^#bwpyjXQ0{lR&+Rg~W9q{T? zH{X`;&baMS0=r0MFm`V;5Ntd{`tNU3#e;a<u;Hc4t-zvY1|wJJxW0l4uk1_#BH)QCFu@MKDJ zkG$ZJWXLJxryO|;jki^>rFXF(gl#UHW~>0k#IXG;Dmet*UtYzfqP3<%mF9B5YK+!u zE(ff50rG(5x<3a99&V77CCQsda}TT4gEi(~crmX5j;YdUaRL-ZO9UtV{qnmyqwSf8 zZ_0Vp)r|HT8INFWg&|Uf&AL`#(1WDeEH9{n*c@Z2T>5g2$P!I7V_gJA%{IRxMx-J} zoK#)C?+Unl(#m*xn#Eq_idXrcDEGF`=y^2hTlmgi^sVNyzn>Q%_xISl+#I?dZ4NA- z6Ap$M%E~kideWfx(Um@CMsf__!k~_Vx9{vk`5K*_*fxL8rlCjF2unBEBU_9c@i~ww z+e1ccNk6ZaOxNSUTYlR*zRO_HXwY4|ZnG8bH6(*Fr0kf3V|gC2&OFe~4p zNyjnROb;#}Y*xR86ETwMBrw&$g#r}(H-djtIh0(O#h@5U>M>I<@}_sG@d@;mo~WEG z8!|>OBd48s*-t^3`1Gkq1dth}T;7>RJNiUL6|GZs6%mpct%^Y2(~dYIIVqVq75w zxrH$g&5!AL#6t#k)x5j1=>#=Im~u_~Tl&2|jQwz&`khpR8W-qkK;x^PS8rD=b`l>`u=9wWV-|?nvA`0 zvlvgp-^Mo{%7_^7>KWWoF=CwKslY-mlUoxnz(Y3EDog<^bG{QGZ%q(`9tkvGJTsFn zoq}{Rki_b-8l>X^@UMrDM}}7*l9ih&z$_p!*IEGK@Y+($wJgs-04ue%JN#`%$kR<+ z@;-*e26?PB-|ndD^Bjb*Hl?{(J-6#A;PR1zqiVu*Fb&JOIL{*trYB`Egl-+|D4U+N zpH3mCF+B;+DW=fVn4UZZx#o%38PpAO>FXKsZ4;(HWhPBQ9oMm$EXU)#Kbk}9Ps1Po z{Gb?LnG!Rbmkm*a?`yxG$Y0C%-*Oc(RQ#Jko$1*@ zg5RMdKVsMCqm@+oJcjqLR^rH`^nf2=;tO&#n(<2sKrsd^5TFzz-S640*z1?L6 z*D^G%Ul+`x-9N#2F`$|(L+f(_Xq8oIghAw5MiK;|r)+1tV9o2&tAZ0dn`X zv@8ST+B2vl!v3P)Qmc+~Z?R|=U1H)_-^Vcv>9+=mg*S{s@9-dqwaB9{6iLvD_0FtP zpo#silz(Ht>pR4bMRMpl!=SF3w;(^G+}r*^*>vjPu>Xe;f0u&xt{5B&DQqWXwAdLiF9U0V7 zX2E8>O%VkUtP%vfA)UGr=-iNu+JVsx8SMVREBbGU-2qFf00pBj!3YSvzw+P^gUr)t zqWOhz;QT(+_#(EqZmq!aGcv|vQH+{la#m{s=`iYEjJ<-m?`Z2qoLabii1II@%icOf z4=-Zd>BA%Pdeae#zl09&KnsRZY-MbArd~qh?*zv%YFh%Ef5V{83t6>@nY8^9ylf2E zCrRg@B|vum_>Z7)lCDN4otFj7EPbrZ+m$koQ0GFmu1{BlousC8^9sUTU%)rKjN7&k zeOw3w^Z?szXSDVj$g(`WDQZ_&U-LLG=pA(y!8&%y#gXXz=zUghr202?r`x|2$H6A z*e*I#Ou+V&zRcScKjYiyS0KO^M7K2Reis7l0_bgtH7`-nFR;WH;2JDD+ID=vAy{-Y z<=YiB!MP-d%v)C~aRyQ*F>$FHvWU^VkZ~ zmwDTw9i-Sm^B+O{PS+qsz_&Eu8l(tCzIVTS{Vlw{82#?5hb*&g@%fi0d6j209FF}gg(!!|%6c(NdDQ4>|+)HQS2iTR;mw8)w5CoYmEqt^Hf;17= zgRVi4c!0IM1c~ZRm2bct&A`2rSesj#!yKJt1bQBVbdnKh%3)e?1Lmj?Z90k3PHFZH zoxcGkiqM^8mgSxR+g^gh^#Zp}G885L20;{2QG}uj4C(?4``qxiX~}OOw(~u)B;-2{ z-wRNLqTe81w5~=UokjY?BKk6KiySzFS5qStErD#&S}0lt*xMBEyht-{!W7HEcAgAH z%P*GjG?_!uCP*+(hN6q8JWqzA;9Jn_-2;3rrdG=iP}f_~qN_j=p{TC_MJRF~qD{A8 z!y%%A2t}z3>YAEOaclrAm{M(JZ^-lnL1h*sW) z01wdV%`z03A)a^7OMH_a-bT;+acObUW!Xm_6qEpD0W1y(0FcN0uSKr7+wrXc#hCx* zn~WgF{QdhG9F5x{u44@9y-i zXv$q!B^aFHhr1@h3vWR~S8 z0Sarh0JnK0tg-7)SmWwGfyLJNok5*)+&tahBIO>4eL#2;P)4a80u)i|6vVrxbKNAJ zIoiS;`Z8~GtbC_h6AsaBnK3R2IG%fV~~eQ!EQ1 zNK+Z54t_&p{(?o)piNU5r84)?&cC2U6VR1emUsb*D78X>V!{-IwS}d#s3@Y;iwx>w z4qIgFbQ*sj#1sg!Li$H_0dmi1`xFFOp|fGK&LV5DT-TR*Tf_lDR!9d4!wSxlkHCOE z8=fuv3edZYJ~fqwK0p`Uy#yylWw6T6$5%;2uu|?21vV1_p2t$y2$G zn31mC%OKB4U0WH{H7Bb(rw5IH2)nNYmu-^E2cWY}hQzJVe47l3S0Lgx84?|TL;8gv zwoQh_m3t}vZ{*4BEO;iveF^xelK~5qfF_CZx(nWe=O98Sc z#`l6CU34{4bavnWCcYM_EAzH{y#qA&5dHF1FaCx+Y-0C)AdI<3Tj+JA;qw) z0y7L0?x$2C>ROCBd8Y$3ycm@33Ex7ZYXj(lq|#Qp-rWtJsZfWr4ez-OQwA)`wW z6OQImdC^p`UkY>dg*Mn2 z;^35&yEw`bWeEgXnPaIbKoOg!3Xo&d;uVenOCwngq3PL{4C=ZSd!~7!-2Lk0$;a+j z8H^S2;n0d@_p_|P?Ib&E!6KfN@YyEI0|AN=%^y;C66bA{c9}HN=y18_XZ~}A8l6eY zjEgoCYY4Aj|SSj41Y0 zK4MVUXslLNBCS^(jV)t9`o6U80uZ?`t@{zU+?NC5IRvNsazIQl!CW&y?7pzFWxfE} zrplguG}q*)YZ)O+InJju=rNkM2huK`uA3Yo&eW>qH`P0&m*Uujuv7?y(ZFT`WTPqF z7t;W<`zeQ zV%bCuAo44NUdH`=0!8^i z&BN8p)pSoNElQLt-*+LQ2Cfw#4=y$KLE}VSXY_23-kHqUQ+b}60JZOnvji3LLgW_R zx0tWw(V#>}fKAKUdq{5DrEHO#b~7eZgcWMd0v*_e2YFw7R;s_eU*s=9adxD!P)aQ4 z+6Yjb9Vz<BJEC*UQ1p<={>U^4XDJ1jzF-*KG#9TEEd`nqqTwuUg& zw>exjR?kD0PI=F{zppr-rtZgP94v)OI18f8J5TX;w8{#6IVw%ujI;Ef%%yvsScWh8 zn&zU^l>-U%B-!hK&Y<0PNRSK(a;48r6`<&y%fTX7uJpQ`r>-hk^Z|=pxu;hdEONax zdbTSqQ5{iA%LTMsg^TtCvDTssDI*q&tng;2)>>DMqoN;aN0BM1GON^ayV;R&{^A;rI34*xapaGTOND1KHUK*sY0EHusg&^%I z_AtEmM|?qC_OuuQignBNH@IBPtQYQ~ct1GW`(V;uE@gsuc|cbbgI+c;zCa)QAKOKOxkFdZEVfLd@&1mMmNpOp zo0NgLGuB&xqP0>XK$z&`k#{En)Y+Sf>w|bcR1FiYi~x+= zrSEb}ilswy=^eQBrtF$T=L4Wrb*4bhW|$I3J%L#M^VGNs+BpwIW=j|QRe(Zh6X@Yc zGVe-lJ4>6Yz=d+p@+oDp8mg&7v-**=+ERiRfuNV!V{o4BzI>IO$oo6mvu z_4!M6ZT={OW-^(l!F`vQ*~fKeP)EkQ%{f?lOEY&ODS>EEYerUqMgrAoQp+?F>Z2+?E~-ta5Os0L3&b^lPdZgx+^dWR2#QILDw)18#|@ zGH7@ZO!yWgbO;IKRx+qV^R&bcNUqP%(X~YF49aIR!x4oLUNEta9LJ!J4BJb8hiMAR zTgw?rPGxzv^;6895@eRyUuJzMrc>g$tDlCoe4m`Pbl4zgEd$eOXE1mVN$0zgV%Bo; z2R;)i_JDp9pqRBdp#K`_+Eu@#el^e$cJAOTF=aVW4$dqG7fFz(EV%*{QU3jE z+S17tf;him4jwX<`vF_QJ3w~oZ(vj{cf82Oo%X7hX#x~?*;}VG0(Wss2B-&!8}6ZxGVN8I}fZw%^vJbeLoAo&SS1+Bp64dS^>oYi#Q zpMV-3G;Q|XW;B(A^0ZWUq_XX3KII~2-_^iRf;Ad5s56D_*- zEe-VX8IjY8WLP*lr!L2)JsqOZn+r0Y(N z?m!jqHhH%rjf{dIQ^9?MY?EaI6m7B%f^3j(JLLNkfZbK}$k77iS(xQKNN&(3;IGej-ckFnlD)iGkA*4RSq2kqFlV81!n=;hBfJF5$d1J9YcmV#CCzUm zDZUP(grQ2MvW<;9BTI`iq*=d3@6^Cc0u&a01bH)L2&gxXK@I^mCNZcZZ>?$&Q@*44 zt|xBJA$C-_uMG*sn6a!8S@vWt>(AYCNXS_$hlHJJ)T18s+mpr*@Q5DP>J&fhBZh>I z0u)2S&@UK4EM!LuP%LBv1Sm#@kaFLUW& zc!wY55|TK;eyyAVgu)ic#oYt*NzN1j;DP`}09XR~*GiUWz;dmePZluf#V0h4b~gZ@ zwcyiVPWAT+P)sR*g88vB=W|}OK^<#*9(|SyP&}*F1C09XTAxLy5wxVC*BG#T1nz6e z)*TD-609+sL7hOJG2rF58AXf%<0Z&rz%mGfTpk0y9m}8?0|Hr99SxS?v#~U#5tM2U z`qQQH`aot4i~z~$WIPO#tpqp~^H(#7Yvq0dRhTqbfNWCN8L*u0#b?A=nh^s&_pBz$#(iMg@!gpOJ!9pKVJ7q3ig<# zBV>Z91}+t#*hJb2{#|qudp40Sj$v%xL~_N9{ijma@f6YoU8@f$-IYp>7T`&&VQYZq zU7c_@bTQ}QI9k9J+aRpnm6P>O4C>Im$C2(R_crRWab#}_GBv<7NqS@+ z*)pW>ymO)iAa^mJ#SAv3M$`CaaNm~b1p`Hs1odQ4XW8eq6?Cn)=w>raoZ7s_sKO5i!FmJb z+@~_6#d$V98yzA*ackz>XhxWd$Ac_^;EsC+a8y?WgE|iEhl57b_T~`e`V4Vrl=mS` z5CnHX(PNYv+0?KFcppE`cT>@cFqjHZbf87x5~y=Wrgx|v&20hO?gG68a=r(8KL}6= zehZ`%s4Wi9SRxR46L_Dtwk2;1gJPEO(-!L762q-cl!`6cjFuQ<-2&wfDn7f9K`$+4 z50~5ci5|zmHhvk5`LTZEY#wkEr57ecG7X$6Krz}ahmeUf*d0k_Pz1YbPf+&GSen!d z%C<%2T-mYu2~bRs(m*FyC#YYw@~0#U$I{JKSn5S$T*#F@D~3TGy7y?_0_7QA)=o>M z;jJAVjKy@ZHAaUTXqzlKOvQkCmW;m_k8v)K$K=<>VYd7xuEk*W3G~a7k5pgX%%B() z?=a}Gq_(;k4OQD<3bq?wSYN7oR)9j)JJ7nmPWCXJssqQ;!!|I|hlqmpg{qcK0u;gX zumCws_Bh3$jtCoo&o#4K{q}@1{mPUBueZn(+tR^!Y~Rj72a3mj%@D|YT6Un7koUB> zU!a`)oH{&>mdJ$Cr==6l+)Q(yhQwXqX{YHTRn!0-7_V z+y+cOp%+)elzc-T3JmUz7?G3dd@k#K%InS0_H%w$hpt(k+6r+3SDAz`ubWL@)pW`AH38l+ zI|7Vv(v;^xb30U;E^hGRqrl?1!X%KM&h`j23rGa#$^sO@Iha8ioP92BD1$9G7$!bH zVwoz4vI`kMpwXAmnHq`u>_o1XqWZgoc%ciL^bOMXip#wg0=Md z{dGmt!ouev z<5Ch9zE3AzSV?A!LSf;BqR!(MzCDtLc7ufzAZ4Nu__@9e>U_v!;jys1K0irk;Y--u zC@a$}{0&V-OIso#Qld~RGKN7NWp4|&KzWuNx~^51_vU#Y0)EJ&+2&xGH2_(#9A^b+ z583oa7KW2g)Bu}Suj?0be0#7`j&J_2)0ytjGw^jjsU^;SEIM2QP;{ys0g6X&_IxUi za=~4FjxZ=+5nvf1vc-!RCX|D1%E68T6iqf(fZSxR$qec==E%5fI5qBx#lW*!tSxU< zSq2F3q7C~!jiw-1=Y^iZs1xVQb$1FH2o-tfw*~}9+{TK^Qb&Md%e-(nBk-MkOHBxX zX(W?52()0ke6<JA3Vil7gE~^I)5^he3;61aAe0z?Z?%plr9fG%=6^T)vw zfMPmRDnK!v`4Z}Nk&jwFSSLU}kYa+hyXXXAYrmO7L4Bd=_wdi#lF1fV>31mi7NBDa+52J5vOD8~F z3eD^X(IolW3O#J{MAVbb+%+N%S?rrJ{DEAiNo!3cu8KyonnS-*3T(<A-=%esp! z{-{q3ZCPckO7r$B4%77ai|Lg}rOv{z27#tNP4E$gFVqlz`CCv^LMc^w8#_E;dj9Ky-9w7cpsX>10EH(== z>NKI=+wm5@-i;~bFC|3e)BD@iCZ^M$@k`hv?AlxMj;=0YU7{$`jTOPHVHEGiQCG(> z92>zKSB27eMFyL6>rfi$Rzs?;A1Xh}t}lByR9Ga6zIChBmAS*{7hnd(g9^Xfl`+T0 zl>Wj`i8H0}VHC1Mji?@oa{kbdcX^#*EMg>O?NA+E>V1GO&vDD(1%*buppXgdTq*PT z#fzT2^I9Ii@6?mG7kRu%&2|2O*h_52FxMqaJv?uz_k5Kh=~JrsB|fe;a6Q{Q!XBti zs2BRulrPmD=H(Odo|;sfy%uf$QVlb&ef)ANyhT?!x9l=+Cy?)>f0vcIsNq+t-MnoO z-l1Dguirr3rGvJ8r6!r5dIfp6Pus)j_AdDD8<{kB7d&Bt_B&Sl#aT1t$5HFA)%K>2 zua?rgyYR)M;8$t)*ZBBZY7|B7RzpJj*T=Eju!O=e7{lT$Evm&|wnJ+~!!U8lrTkWY zO3X8wX>-2KO1n<_+Gx&hHT1E1MShRh3r^O2_BIalDTjwp$!^ueZv_}<~N*Q2PK7#eJyFoSiByX3k=Vsyf*mtun|MT4F-4B@y;*J zrfruhxK9oV7-w`B3<((efB2sgdsK*Ze|^=_=$<+CY6S!NJ@D)AestgO=nJOw>lJBq zgkQG1DBN#LnrTh*O741Noxdukf{m5_e{!q0*nK;*qIr<%=;li9Sq{IpM$_rf{m9v& zVrzFoZ)afz{(qI(VZbC_$;HAKce|OpMq`&F%AwT!1kx*LM@e|!y5>W5|s3cj>n3H;-Z(1RQl=zfx&sl<9Q0%CzMZM?H7HBxhy2X<&L~_n{1Doo7us(<;-a z8O|D%k%6!D`R#KC%l~Q}at6D%9diELm)ah3KGUd6US&ffQXiy&NJ&V^NGV8ZNa;vf zNYjzB^D5K3C!E#X{ZBg+jnw6Pr$1FX?R^1ZVT9XR7$>)UBa zO3EmMf&Tu{`Hm8qM6Vunwxb&-owX?Aq%)NY@|-K^+&O0w|5uG-&pAJ#w6o4Nl_HZO zBiZ%Zj~O%!UPq=w@XG@Soe8w|gtHeVW~yeyFp{2qrvdfpfMv~8#&A%+Q&Whyz@gN-T1dN#$D@z^RV%iwHGTJc0~Ca zPS?g)1Mqh=+Sia2W;W*HtZkYuJ!3%Nb|C!E8*xmpQoX8uEKhEH6t6J6Ssm zk&DaLPV}YFYtAaZd~sL12;~d!zmv63*5*8vabKur`p*F6UUQl|Gm(O~%h?9J>kFaG zMk5Bou=)*KwQTT;6z>aPL-uxw*TwB?5R8DMF*DphPSf(83xtH`(^6b_%+Jo*iVw5s z^fSfhJL7$C*YP*>x#dgMFFC)?Y=0dENL`Q;k$NHZLF$h*5Ge^M87T!R6)6oV9Vr7T z3u!u1HqvaQc}R|PY1j42jSRT}4j>&yI*OEs#Fb7X zokO~SRDg6D>DqOgQRv()guYQ!*>D@_F4BFZM@WVnl?`SjE0P1r56L5R0N_BRV5Bgl zT1e4I^^szbnj$qv@)o)^@)D5RBXvURasxsq0_cVGzZbd(wZGz=p4sO&m=(*SLRyNn0x1V+4boa9uCoEZn~}C5?LgXv zl#6uWx5}}G!vKyV9P2yn^T_^YFz=&);dlLHNhr_-H+UL!hg?tN`#dAWj8uPk1PZwi;sU zNSt)-Hy89#9%|TJ*WZxSv7CU1Nfxr!Uc^KC&LCw?9om~!F-SRDmw!WAve4dtW+UPX zh(4)|V-9N$dLh3$BO(OOOXWOMJurmkvde?%_^(cv=;4V;0(J*qcg933=K|ebD_4H# zXt^uM-;hzw-w=%7Hu$<|@*No8-`~&+c_~r;20w!moqe^M;n7~Jp%aoH8?@AD$cS?P zKeo;XpwcPtdS6G8~#&P+^9Mob7X&1y_T2rVIQXjg1Y`-+JT&(iL; zHt)7JJ1wn^&1z|FYeQ@ZAvT2AvD=#W`+d$i&pG#b@8QnrbAIRi&hPxr-{-l{z4w;A zmi{R*5w@JV&(htOvA)1TX<`m9hKXa+uu$RW*xdDV%PspXJ(YQ1Lf*vwgSLFk7`$ZP zrTa5JWM9dr?z?m)`A)@;?z?m!x18dS8N+`|@|K>utPLZezXm_#qe8<5`S|Re=IQ0-QedaM`PZL(`l_?mxk6he%d9_rt&~$=)T>+xSCy?e1t!E+R&nM@>(;F= zKYi7j*!0uSKIzPp%TGUN{n@9iJ4MlCEsxM#w(A_jqX=V>1CU*z8brwE6jBjY;4?k6*76suE1t2b?LlFv9nrv z8#iCJ5t(ac#hWj`W<&kv+FYjE1zbHlqtaNxmNWNXdeoNA{g*CA>#tdV#dj`QzoE8v zy=%4Ni|RLSxJssA+RHpwZ){u-gJSsV#_Q@gGIf3ZhU+$7wGrF8C%f zUB2SGjh>q>+t?VWyP{TXz3YIbALS-q`<<=Vlr3GkWVgcH4Hq?TzQ#Rc9Cpm%ha7fP z@gavDa@gTVY`OBC%tN=1eID*i*gCj&=}gPAu0Lgp$>%afG2+u#2r+VQrWkn^FQoOp zvE|9XVmuDMvGvuzF1`H7V=MlfDLNK2#XMf+5$sQIU?zTp7q}uLjG2~D*t&L^H}iJp z%=i!duV#IP>H+iJ`AZf8oW{Q(F%@I($cRVqIkGZ%e`lB_uVk&f4;m zRXZH!mfmvWiQ(ON>9PBkjuXSFThBTnyfrbYw=9bn6jfX1pF#!oE5cRDNfT?_v`6ll zM$#3l!)GKXwV#p2En}~(@0N#Oy(qEmoUKpxhtonyEl;^w2e(FlA5I7)wm-l1mS@6u zEKe#M%aWHP$9@vNGZ38kSC;6!{9n;6E8Y+P%UlHQx+S){f7I9wXGSP&%AKZp`4)PwvZHPw^hY`;qZh<^}Os1GdT#DF1 z+<>?aaVO%8b<3h+5Pw9Va0YQJ1i@o7MF3ocxC>l`xB_tt;zFeNA+AC^g}4XlbBG5K zi{o&tLV6hSBsd@O2n;DfJP)oxybWes zpcH@9p+G(2PH-pU1;m4hi;-~(@f>1tLZ-+?zCy&ch-(qIA-xlEBl3?T&Oy8laUaqP zPDEFMD-b(~TM)M+eE@M8%FiH<=Hrj-2rP!63~>M%n-R|-b`WPH9!Fe{IIsfw5Emhy zMgBU(MJV5exEtwXhzpTEk9Yua?n-PCh^r9SAYU8eQt&Y1MdY7fnHd#j_#=N6DukdM z@iuS^;!4Crh^vu4kGKK~3QAECID)tZT!nZP+<)*Hni*+}R^96`c3;tCYphIkm9TZWc` zixC%qs}N5hZb00KxC3!D! zcOw=^A4Xh@^cloK&i`OJMm-XW5RV|PLEMJ|ZHRMFU=VRLcn0wp;zh*0h_g>a%ONj9 zoDZ%*+={p!aRJiX5H~?z58`rGjCcV&jkp&1#9FKy>oEUw@JDqd3ug}uuCgpqf_N0W zWf|h;bFxGw;?~71QH{7{EKAf;{#jTwA#PZmC0faAu)87_Uxh^v;+E&L!~o+nvcwqT z#;3B}t&ZEBz(izyOk2I>wA8G-qm>(X5TjhNRmee{DE7y{@>WRB#XE>aEkrEk`0aBg z&)&3y;1!(?QjTM>l22Z;19mMFq`XVZSBvnaJK*4Eg3R!=&LHw2FXMKg!aj&`L5C1< zQ`vx=i?{qNEh~2kjm_S)Wek58&@fg$g`C`R$MH&Nk)>%+AmJ^jVUF;nTe8zpnwI2U zy5)rQtOFDCF5S90J?lRq{&8`3*7Zq=l^eIbyGPc;iKy%QIa$9-%-*#1=smM;O-P)) zWXsU9tUD6Jmu@ZFD{J2*7P)Mntjq-0x4ozezxt$3H3`i-hIVi{7ilz?<{5dSE2j%0 zc(DnCFfy%d$53n1_Y&_+NUBW0^;)))k8g&-_}j=&P1mx8%{y$_zHioUi8+^S**!07 z_r%&uwj7n0brj9LJTL1Yn7ifvysZ6c?X!7VC+vQBJ?{Q9Bnf#*$ae!I(>{Tdca|F` z37Outxb^T8v(gfx4Lh zlX-q%3u#a&#~Ab|xWr<5pdL&Eqk3I#ur`X)TX0_q96*KyY#hrgU@_KR$#Nqg%OJVy zUE{HzpM`xH6$D`;^*@UWo2>HmK-ZoLqAvl<1T*{%8Fb)SrU!z#riE+~*>-U;*miM` zWdJLvhMezGQvU}i-)J?+K{{D3Q)uD5&j4n?Lj?GjAtbiPk@NWg8A2i?@0GjA1~~W& z&kIN~%I_6Njt2--{m5`coD6ubz*7*3Bgex6p8T{pay&xd$uEo}$3q0}GL1|jaYdXA zc(%Y(&=yBN_zxq$H;x>S5P0Pui6h5@1>7bWLgIhpWWbXKQVjVEapZXPz>~idM~){D zJoy)K&S3oY`xKIG zh0{KTMV7*4|4$+Am*yD*iy$Yrp-Oh~O7J*X8+cw6eZGJMyTZKvOa=!9E3697fP!W$ zrNa{m7=&R_4K8^r8*k&ABQ)hQgvF(h>mXYUeLQ!&Ax%VYMg}G{AwdtqQLwh~PRP4Q zcfasQ`9Ot2L;fVDx%*k|fq+%0DNCF?H0wY#XdT*#ZCKLc`ookvFC`X3gw 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

jgn(2`Y77r9+`;oXtVnAPw3dq&38Ytr-y~6{=ZI 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 z zV=p^kjYz$C%EZZOB4#ehI`iV3f(y>gk>8hOPCR9b$d^lUr%s+iyYpRo*2F0xc~w$9 z&p!2lbz;Qqnz<#_*Vr8gtkDDWa&jl7q)xG4I$)ihQ&B#%e16qJ$*J18^rF56B^8LU zdE|nE*;NbW*SvZBT~xHV!s$LkWrvqySU$hN`Bgh{V&~M)t%c!XlTNXZI%tg?Ffrwn z$y27kBiHBG6c|>Oa|Bpp0$6;A%MOA6FR8Kt2s-(CtL9Ud}@0{_K73H_5MZ{Y6q%W*c_4f diff --git a/resource/jetkvm_native.sha256 b/resource/jetkvm_native.sha256 index 65da816..b540b94 100644 --- a/resource/jetkvm_native.sha256 +++ b/resource/jetkvm_native.sha256 @@ -1 +1 @@ -c0803a9185298398eff9a925de69bd0ca882cd5983b989a45b748648146475c6 +4b925c7aa73d2e35a227833e806658cb17e1d25900611f93ed70b11ac9f1716d diff --git a/timesync.go b/timesync.go new file mode 100644 index 0000000..7b25fe2 --- /dev/null +++ b/timesync.go @@ -0,0 +1,53 @@ +package kvm + +import ( + "strconv" + "time" + + "github.com/jetkvm/kvm/internal/timesync" +) + +var ( + timeSync *timesync.TimeSync + builtTimestamp string +) + +func isTimeSyncNeeded() bool { + if builtTimestamp == "" { + timesyncLogger.Warn().Msg("built timestamp is not set, time sync is needed") + return true + } + + ts, err := strconv.Atoi(builtTimestamp) + if err != nil { + timesyncLogger.Warn().Str("error", err.Error()).Msg("failed to parse built timestamp") + return true + } + + // builtTimestamp is UNIX timestamp in seconds + builtTime := time.Unix(int64(ts), 0) + now := time.Now() + + if now.Sub(builtTime) < 0 { + timesyncLogger.Warn(). + Str("built_time", builtTime.Format(time.RFC3339)). + Str("now", now.Format(time.RFC3339)). + Msg("system time is behind the built time, time sync is needed") + return true + } + + return false +} + +func initTimeSync() { + timeSync = timesync.NewTimeSync(×ync.TimeSyncOptions{ + Logger: timesyncLogger, + NetworkConfig: config.NetworkConfig, + PreCheckFunc: func() (bool, error) { + if !networkState.IsOnline() { + return false, nil + } + return true, nil + }, + }) +} diff --git a/ui/dev_device.sh b/ui/dev_device.sh index 650cadd..2c7b497 100755 --- a/ui/dev_device.sh +++ b/ui/dev_device.sh @@ -15,5 +15,15 @@ echo "└─────────────────────── # Set the environment variable and run Vite echo "Starting development server with JetKVM device at: $ip_address" + +# Check if pwd is the current directory of the script +if [ "$(pwd)" != "$(dirname "$0")" ]; then + pushd "$(dirname "$0")" > /dev/null + echo "Changed directory to: $(pwd)" +fi + sleep 1 + JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device + +popd > /dev/null diff --git a/ui/package-lock.json b/ui/package-lock.json index b51a2ea..9e77e10 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -19,6 +19,7 @@ "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "cva": "^1.0.0-beta.1", + "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^10.2.3", "framer-motion": "^11.15.0", @@ -2433,6 +2434,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/ui/package.json b/ui/package.json index 3160297..4dab092 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,6 +30,7 @@ "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "cva": "^1.0.0-beta.1", + "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^10.2.3", "framer-motion": "^11.15.0", diff --git a/ui/public/sse.html b/ui/public/sse.html new file mode 120000 index 0000000..0a8b4f3 --- /dev/null +++ b/ui/public/sse.html @@ -0,0 +1 @@ +../../internal/logging/sse.html \ No newline at end of file diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 0fa4121..db1fd04 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -663,6 +663,95 @@ export const useDeviceStore = create(set => ({ setSystemVersion: version => set({ systemVersion: version }), })); +export interface DhcpLease { + ip?: string; + netmask?: string; + broadcast?: string; + ttl?: string; + mtu?: string; + hostname?: string; + domain?: string; + bootp_next_server?: string; + bootp_server_name?: string; + bootp_file?: string; + timezone?: string; + routers?: string[]; + dns?: string[]; + ntp_servers?: string[]; + lpr_servers?: string[]; + _time_servers?: string[]; + _name_servers?: string[]; + _log_servers?: string[]; + _cookie_servers?: string[]; + _wins_servers?: string[]; + _swap_server?: string; + boot_size?: string; + root_path?: string; + lease?: string; + lease_expiry?: Date; + dhcp_type?: string; + server_id?: string; + message?: string; + tftp?: string; + bootfile?: string; +} + +export interface IPv6Address { + address: string; + prefix: string; + valid_lifetime: string; + preferred_lifetime: string; + scope: string; +} + +export interface NetworkState { + interface_name?: string; + mac_address?: string; + ipv4?: string; + ipv4_addresses?: string[]; + ipv6?: string; + ipv6_addresses?: IPv6Address[]; + ipv6_link_local?: string; + dhcp_lease?: DhcpLease; + + setNetworkState: (state: NetworkState) => void; + setDhcpLease: (lease: NetworkState["dhcp_lease"]) => void; + setDhcpLeaseExpiry: (expiry: Date) => void; +} + + +export type IPv6Mode = "disabled" | "slaac" | "dhcpv6" | "slaac_and_dhcpv6" | "static" | "link_local" | "unknown"; +export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown"; +export type LLDPMode = "disabled" | "basic" | "all" | "unknown"; +export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown"; +export type TimeSyncMode = "ntp_only" | "ntp_and_http" | "http_only" | "custom" | "unknown"; + +export interface NetworkSettings { + hostname: string; + domain: string; + ipv4_mode: IPv4Mode; + ipv6_mode: IPv6Mode; + lldp_mode: LLDPMode; + lldp_tx_tlvs: string[]; + mdns_mode: mDNSMode; + time_sync_mode: TimeSyncMode; +} + +export const useNetworkStateStore = create((set, get) => ({ + setNetworkState: (state: NetworkState) => set(state), + setDhcpLease: (lease: NetworkState["dhcp_lease"]) => set({ dhcp_lease: lease }), + setDhcpLeaseExpiry: (expiry: Date) => { + const lease = get().dhcp_lease; + if (!lease) { + console.warn("No lease found"); + return; + } + + lease.lease_expiry = expiry; + set({ dhcp_lease: lease }); + } +})); + export interface KeySequenceStep { keys: string[]; modifiers: string[]; @@ -767,8 +856,8 @@ export const useMacrosStore = create((set, get) => ({ for (let i = 0; i < macro.steps.length; i++) { const step = macro.steps[i]; if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) { - console.error(`Cannot save: macro "${macro.name}" step ${i+1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); - throw new Error(`Cannot save: macro "${macro.name}" step ${i+1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); + console.error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); + throw new Error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); } } } diff --git a/ui/src/main.tsx b/ui/src/main.tsx index e09a2a9..f4bdd34 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -42,6 +42,7 @@ import SettingsVideoRoute from "./routes/devices.$id.settings.video"; import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance"; import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index"; import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update"; +import SettingsNetworkRoute from "./routes/devices.$id.settings.network"; import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth"; import SettingsMacrosRoute from "./routes/devices.$id.settings.macros"; import SettingsMacrosAddRoute from "./routes/devices.$id.settings.macros.add"; @@ -156,6 +157,10 @@ if (isOnDevice) { path: "hardware", element: , }, + { + path: "network", + element: , + }, { path: "access", children: [ diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx new file mode 100644 index 0000000..59d52ef --- /dev/null +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -0,0 +1,408 @@ +import { useCallback, useEffect, useState } from "react"; + +import { SelectMenuBasic } from "../components/SelectMenuBasic"; +import { SettingsPageHeader } from "../components/SettingsPageheader"; + +import { IPv4Mode, IPv6Mode, LLDPMode, mDNSMode, NetworkSettings, NetworkState, TimeSyncMode, useNetworkStateStore } from "@/hooks/stores"; +import { useJsonRpc } from "@/hooks/useJsonRpc"; +import notifications from "@/notifications"; +import { Button } from "@components/Button"; +import { GridCard } from "@components/Card"; +import InputField from "@components/InputField"; +import { SettingsItem } from "./devices.$id.settings"; + +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; + +dayjs.extend(relativeTime); + +const defaultNetworkSettings: NetworkSettings = { + hostname: "", + domain: "", + ipv4_mode: "unknown", + ipv6_mode: "unknown", + lldp_mode: "unknown", + lldp_tx_tlvs: [], + mdns_mode: "unknown", + time_sync_mode: "unknown", +} + +export function LifeTimeLabel({ lifetime }: { lifetime: string }) { + if (lifetime == "") { + return N/A; + } + + const [remaining, setRemaining] = useState(null); + + useEffect(() => { + setRemaining(dayjs(lifetime).fromNow()); + + const interval = setInterval(() => { + setRemaining(dayjs(lifetime).fromNow()); + }, 1000 * 30); + return () => clearInterval(interval); + }, [lifetime]); + + return <> + {dayjs(lifetime).format()} + {remaining && <> + {" "} + ({remaining}) + + } + +} + +export default function SettingsNetworkRoute() { + const [send] = useJsonRpc(); + const [networkState, setNetworkState] = useNetworkStateStore(state => [state, state.setNetworkState]); + + const [networkSettings, setNetworkSettings] = useState(defaultNetworkSettings); + const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false); + + const getNetworkSettings = useCallback(() => { + setNetworkSettingsLoaded(false); + send("getNetworkSettings", {}, resp => { + if ("error" in resp) return; + console.log(resp.result); + setNetworkSettings(resp.result as NetworkSettings); + setNetworkSettingsLoaded(true); + }); + }, [send]); + + const setNetworkSettingsRemote = useCallback((settings: NetworkSettings) => { + setNetworkSettingsLoaded(false); + send("setNetworkSettings", { settings }, resp => { + if ("error" in resp) { + notifications.error("Failed to save network settings: " + (resp.error.data ? resp.error.data : resp.error.message)); + setNetworkSettingsLoaded(true); + return; + } + setNetworkSettings(resp.result as NetworkSettings); + setNetworkSettingsLoaded(true); + notifications.success("Network settings saved"); + }); + }, [send]); + + const getNetworkState = useCallback(() => { + send("getNetworkState", {}, resp => { + if ("error" in resp) return; + console.log(resp.result); + setNetworkState(resp.result as NetworkState); + }); + }, [send]); + + const handleRenewLease = useCallback(() => { + send("renewDHCPLease", {}, resp => { + if ("error" in resp) { + notifications.error("Failed to renew lease: " + resp.error.message); + } else { + notifications.success("DHCP lease renewed"); + } + }); + }, [send]); + + useEffect(() => { + getNetworkState(); + getNetworkSettings(); + }, [getNetworkState, getNetworkSettings]); + + const handleIpv4ModeChange = (value: IPv4Mode | string) => { + setNetworkSettings({ ...networkSettings, ipv4_mode: value as IPv4Mode }); + }; + + const handleIpv6ModeChange = (value: IPv6Mode | string) => { + setNetworkSettings({ ...networkSettings, ipv6_mode: value as IPv6Mode }); + }; + + const handleLldpModeChange = (value: LLDPMode | string) => { + setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode }); + }; + + // const handleLldpTxTlvsChange = (value: string[]) => { + // setNetworkSettings({ ...networkSettings, lldp_tx_tlvs: value }); + // }; + + const handleMdnsModeChange = (value: mDNSMode | string) => { + setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode }); + }; + + const handleTimeSyncModeChange = (value: TimeSyncMode | string) => { + setNetworkSettings({ ...networkSettings, time_sync_mode: value as TimeSyncMode }); + }; + + const filterUnknown = useCallback((options: { value: string; label: string; }[]) => { + if (!networkSettingsLoaded) return options; + return options.filter(option => option.value !== "unknown"); + }, [networkSettingsLoaded]); + + return ( +

+ +
+ } + > + + {networkState?.mac_address} + + +
+
+ + Hostname for the device +
+ + Leave blank for default + + + } + > + { + setNetworkSettings({ ...networkSettings, hostname: e.target.value }); + }} + disabled={!networkSettingsLoaded} + /> +
+
+
+ + Domain for the device +
+ + Leave blank to use DHCP provided domain, if there is no domain, use local + + + } + > + { + setNetworkSettings({ ...networkSettings, domain: e.target.value }); + }} + disabled={!networkSettingsLoaded} + /> +
+
+
+ + handleIpv4ModeChange(e.target.value)} + disabled={!networkSettingsLoaded} + options={filterUnknown([ + { value: "dhcp", label: "DHCP" }, + // { value: "static", label: "Static" }, + ])} + /> + + {networkState?.dhcp_lease && ( + +
+
+
+

+ Current DHCP Lease +

+
+
    + {networkState?.dhcp_lease?.ip &&
  • IP: {networkState?.dhcp_lease?.ip}
  • } + {networkState?.dhcp_lease?.netmask &&
  • Subnet: {networkState?.dhcp_lease?.netmask}
  • } + {networkState?.dhcp_lease?.broadcast &&
  • Broadcast: {networkState?.dhcp_lease?.broadcast}
  • } + {networkState?.dhcp_lease?.ttl &&
  • TTL: {networkState?.dhcp_lease?.ttl}
  • } + {networkState?.dhcp_lease?.mtu &&
  • MTU: {networkState?.dhcp_lease?.mtu}
  • } + {networkState?.dhcp_lease?.hostname &&
  • Hostname: {networkState?.dhcp_lease?.hostname}
  • } + {networkState?.dhcp_lease?.domain &&
  • Domain: {networkState?.dhcp_lease?.domain}
  • } + {networkState?.dhcp_lease?.routers &&
  • Gateway: {networkState?.dhcp_lease?.routers.join(", ")}
  • } + {networkState?.dhcp_lease?.dns &&
  • DNS: {networkState?.dhcp_lease?.dns.join(", ")}
  • } + {networkState?.dhcp_lease?.ntp_servers &&
  • NTP Servers: {networkState?.dhcp_lease?.ntp_servers.join(", ")}
  • } + {networkState?.dhcp_lease?.server_id &&
  • Server ID: {networkState?.dhcp_lease?.server_id}
  • } + {networkState?.dhcp_lease?.bootp_next_server &&
  • BootP Next Server: {networkState?.dhcp_lease?.bootp_next_server}
  • } + {networkState?.dhcp_lease?.bootp_server_name &&
  • BootP Server Name: {networkState?.dhcp_lease?.bootp_server_name}
  • } + {networkState?.dhcp_lease?.bootp_file &&
  • Boot File: {networkState?.dhcp_lease?.bootp_file}
  • } + {networkState?.dhcp_lease?.lease_expiry &&
  • + Lease Expiry: +
  • } + {/* {JSON.stringify(networkState?.dhcp_lease)} */} +
+
+
+
+
+
+
+
+
+ )} +
+
+ + handleIpv6ModeChange(e.target.value)} + disabled={!networkSettingsLoaded} + options={filterUnknown([ + // { value: "disabled", label: "Disabled" }, + { value: "slaac", label: "SLAAC" }, + // { value: "dhcpv6", label: "DHCPv6" }, + // { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" }, + // { value: "static", label: "Static" }, + // { value: "link_local", label: "Link-local only" }, + ])} + /> + + {networkState?.ipv6_addresses && ( + +
+
+
+

+ IPv6 Information +

+
+
+

+ IPv6 Link-local +

+

+ {networkState?.ipv6_link_local} +

+
+
+

+ IPv6 Addresses +

+
    + {networkState?.ipv6_addresses && networkState?.ipv6_addresses.map(addr => ( +
  • + {addr.address} + {addr.valid_lifetime && <> +
    + - valid_lft: {" "} + + + + } + {addr.preferred_lifetime && <> +
    + - pref_lft: {" "} + + + + } +
  • + ))} +
+
+
+
+
+
+
+ )} +
+
+ + handleLldpModeChange(e.target.value)} + disabled={!networkSettingsLoaded} + options={filterUnknown([ + { value: "disabled", label: "Disabled" }, + { value: "basic", label: "Basic" }, + { value: "all", label: "All" }, + ])} + /> + +
+
+ + handleMdnsModeChange(e.target.value)} + disabled={!networkSettingsLoaded} + options={filterUnknown([ + { value: "disabled", label: "Disabled" }, + { value: "auto", label: "Auto" }, + { value: "ipv4_only", label: "IPv4 only" }, + { value: "ipv6_only", label: "IPv6 only" }, + ])} + /> + +
+
+ + handleTimeSyncModeChange(e.target.value)} + disabled={!networkSettingsLoaded} + options={filterUnknown([ + { value: "unknown", label: "..." }, + // { value: "auto", label: "Auto" }, + { value: "ntp_only", label: "NTP only" }, + { value: "ntp_and_http", label: "NTP and HTTP" }, + { value: "http_only", label: "HTTP only" }, + // { value: "custom", label: "Custom" }, + ])} + /> + +
+
+
+
+ ); +} diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index c0b4181..f8e5262 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -9,6 +9,7 @@ import { LuArrowLeft, LuPalette, LuCommand, + LuNetwork, } from "react-icons/lu"; import React, { useEffect, useRef, useState } from "react"; @@ -207,6 +208,17 @@ export default function SettingsRoute() { +
+ (isActive ? "active" : "")} + > +
+ +

Network

+
+
+
state.setNetworkState); + const setUsbState = useHidStore(state => state.setUsbState); const setHdmiState = useVideoStore(state => state.setHdmiState); @@ -600,6 +604,11 @@ export default function KvmIdRoute() { setHdmiState(resp.params as Parameters[0]); } + if (resp.method === "networkState") { + console.log("Setting network state", resp.params); + setNetworkState(resp.params as NetworkState); + } + if (resp.method === "otaState") { const otaState = resp.params as UpdateState["otaState"]; setOtaState(otaState); diff --git a/ui/vite.config.ts b/ui/vite.config.ts index f8459cd..e47774f 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -35,6 +35,7 @@ export default defineConfig(({ mode, command }) => { "/auth": JETKVM_PROXY_URL, "/storage": JETKVM_PROXY_URL, "/cloud": JETKVM_PROXY_URL, + "/developer": JETKVM_PROXY_URL, } : undefined, }, diff --git a/usb.go b/usb.go index 3395db4..91674c9 100644 --- a/usb.go +++ b/usb.go @@ -66,6 +66,6 @@ func checkUSBState() { usbState = newState usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed") - requestDisplayUpdate() + requestDisplayUpdate(true) triggerUSBStateUpdate() } diff --git a/usb_mass_storage.go b/usb_mass_storage.go index 2b03f1f..79a05d1 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -62,7 +62,11 @@ func onDiskMessage(msg webrtc.DataChannelMessage) { func mountImage(imagePath string) error { err := setMassStorageImage("") if err != nil { - return fmt.Errorf("remove Mass Storage Image Error: %w", err) + return fmt.Errorf("remove mass storage image error: %w", err) + } + err = setMassStorageImage(imagePath) + if err != nil { + return fmt.Errorf("set mass storage image error: %w", err) } err = setMassStorageImage(imagePath) if err != nil { @@ -477,7 +481,6 @@ func handleUploadChannel(d *webrtc.DataChannel) { totalBytesWritten += int64(bytesWritten) sendProgress := time.Since(lastProgressTime) >= 200*time.Millisecond - if totalBytesWritten >= pendingUpload.Size { sendProgress = true close(uploadComplete) diff --git a/video.go b/video.go index d74add8..6fa77b9 100644 --- a/video.go +++ b/video.go @@ -43,7 +43,7 @@ func HandleVideoStateMessage(event CtrlResponse) { } lastVideoState = videoState triggerVideoStateUpdate() - requestDisplayUpdate() + requestDisplayUpdate(true) } func rpcGetVideoState() (VideoInputState, error) { diff --git a/web.go b/web.go index 6e74a13..766eaf5 100644 --- a/web.go +++ b/web.go @@ -9,6 +9,7 @@ import ( "fmt" "io/fs" "net/http" + "net/http/pprof" "path/filepath" "strings" "time" @@ -18,6 +19,7 @@ import ( gin_logger "github.com/gin-contrib/logger" "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/jetkvm/kvm/internal/logging" "github.com/pion/webrtc/v4" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -103,6 +105,27 @@ func setupRouter() *gin.Engine { // A Prometheus metrics endpoint. r.GET("/metrics", gin.WrapH(promhttp.Handler())) + // Developer mode protected routes + developerModeRouter := r.Group("/developer/") + developerModeRouter.Use(basicAuthProtectedMiddleware(true)) + { + // pprof + developerModeRouter.GET("/pprof/", gin.WrapF(pprof.Index)) + developerModeRouter.GET("/pprof/cmdline", gin.WrapF(pprof.Cmdline)) + developerModeRouter.GET("/pprof/profile", gin.WrapF(pprof.Profile)) + developerModeRouter.POST("/pprof/symbol", gin.WrapF(pprof.Symbol)) + developerModeRouter.GET("/pprof/symbol", gin.WrapF(pprof.Symbol)) + developerModeRouter.GET("/pprof/trace", gin.WrapF(pprof.Trace)) + developerModeRouter.GET("/pprof/allocs", gin.WrapH(pprof.Handler("allocs"))) + developerModeRouter.GET("/pprof/block", gin.WrapH(pprof.Handler("block"))) + developerModeRouter.GET("/pprof/goroutine", gin.WrapH(pprof.Handler("goroutine"))) + developerModeRouter.GET("/pprof/heap", gin.WrapH(pprof.Handler("heap"))) + developerModeRouter.GET("/pprof/mutex", gin.WrapH(pprof.Handler("mutex"))) + developerModeRouter.GET("/pprof/threadcreate", gin.WrapH(pprof.Handler("threadcreate"))) + + logging.AttachSSEHandler(developerModeRouter) + } + // Protected routes (allows both password and noPassword modes) protected := r.Group("/") protected.Use(protectedMiddleware()) @@ -203,7 +226,7 @@ func handleLocalWebRTCSignal(c *gin.Context) { wsOptions := &websocket.AcceptOptions{ InsecureSkipVerify: true, // Allow connections from any origin OnPingReceived: func(ctx context.Context, payload []byte) bool { - scopedLogger.Info().Bytes("payload", payload).Msg("ping frame received") + scopedLogger.Debug().Bytes("payload", payload).Msg("ping frame received") metricConnectionTotalPingReceivedCount.WithLabelValues("local", source).Inc() metricConnectionLastPingReceivedTimestamp.WithLabelValues("local", source).SetToCurrentTime() @@ -242,7 +265,12 @@ func handleWebRTCSignalWsMessages( scopedLogger *zerolog.Logger, ) error { runCtx, cancelRun := context.WithCancel(context.Background()) - defer cancelRun() + defer func() { + if isCloudConnection { + setCloudConnectionState(CloudConnectionStateDisconnected) + } + cancelRun() + }() // connection type var sourceType string @@ -459,11 +487,51 @@ func protectedMiddleware() gin.HandlerFunc { } } +func sendErrorJsonThenAbort(c *gin.Context, status int, message string) { + c.JSON(status, gin.H{"error": message}) + c.Abort() +} + +func basicAuthProtectedMiddleware(requireDeveloperMode bool) gin.HandlerFunc { + return func(c *gin.Context) { + if requireDeveloperMode { + devModeState, err := rpcGetDevModeState() + if err != nil { + sendErrorJsonThenAbort(c, http.StatusInternalServerError, "Failed to get developer mode state") + return + } + + if !devModeState.Enabled { + sendErrorJsonThenAbort(c, http.StatusUnauthorized, "Developer mode is not enabled") + return + } + } + + if config.LocalAuthMode == "noPassword" { + sendErrorJsonThenAbort(c, http.StatusForbidden, "The resource is not available in noPassword mode") + return + } + + // calculate basic auth credentials + _, password, ok := c.Request.BasicAuth() + if !ok { + c.Header("WWW-Authenticate", "Basic realm=\"JetKVM\"") + sendErrorJsonThenAbort(c, http.StatusUnauthorized, "Basic auth is required") + return + } + + err := bcrypt.CompareHashAndPassword([]byte(config.HashedPassword), []byte(password)) + if err != nil { + sendErrorJsonThenAbort(c, http.StatusUnauthorized, "Invalid password") + return + } + + c.Next() + } +} + func RunWebServer() { r := setupRouter() - //if strings.Contains(builtAppVersion, "-dev") { - // pprof.Register(r) - //} err := r.Run(":80") if err != nil { panic(err) diff --git a/web_tls.go b/web_tls.go index cbff56b..564f150 100644 --- a/web_tls.go +++ b/web_tls.go @@ -54,7 +54,7 @@ func initCertStore() { func getCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) { switch config.TLSMode { case "self-signed": - if isTimeSyncNeeded() || !timeSyncSuccess { + if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() { return nil, fmt.Errorf("time is not synced") } return certSigner.GetCertificate(info) @@ -174,7 +174,7 @@ func runWebSecureServer() { websecureLogger.Info().Msg("Shutting down websecure server") err := server.Shutdown(context.Background()) if err != nil { - websecureLogger.Error().Err(err).Msg("Failed to shutdown websecure server") + websecureLogger.Error().Err(err).Msg("failed to shutdown websecure server") } } }() diff --git a/webrtc.go b/webrtc.go index 1e093e2..f6c8529 100644 --- a/webrtc.go +++ b/webrtc.go @@ -10,6 +10,7 @@ import ( "github.com/coder/websocket" "github.com/coder/websocket/wsjson" "github.com/gin-gonic/gin" + "github.com/jetkvm/kvm/internal/logging" "github.com/pion/webrtc/v4" "github.com/rs/zerolog" ) @@ -68,7 +69,7 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) { func newSession(config SessionConfig) (*Session, error) { webrtcSettingEngine := webrtc.SettingEngine{ - LoggerFactory: defaultLoggerFactory, + LoggerFactory: logging.GetPionDefaultLoggerFactory(), } iceServer := webrtc.ICEServer{} @@ -205,7 +206,7 @@ func newSession(config SessionConfig) (*Session, error) { var actionSessions = 0 func onActiveSessionsChanged() { - requestDisplayUpdate() + requestDisplayUpdate(true) } func onFirstSessionConnected() {