mirror of https://github.com/jetkvm/kvm.git
release 0.3.9 (#349)
This commit is contained in:
commit
5452d7c721
43
cloud.go
43
cloud.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -59,6 +60,13 @@ var (
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
|
metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "jetkvm_connection_last_ping_received_timestamp",
|
||||||
|
Help: "The timestamp when the last ping request was received",
|
||||||
|
},
|
||||||
|
[]string{"type", "source"},
|
||||||
|
)
|
||||||
metricConnectionLastPingDuration = promauto.NewGaugeVec(
|
metricConnectionLastPingDuration = promauto.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_connection_last_ping_duration",
|
Name: "jetkvm_connection_last_ping_duration",
|
||||||
|
@ -76,16 +84,23 @@ var (
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionTotalPingCount = promauto.NewCounterVec(
|
metricConnectionTotalPingSentCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_connection_total_ping_count",
|
Name: "jetkvm_connection_total_ping_sent",
|
||||||
Help: "The total number of pings sent to the connection",
|
Help: "The total number of pings sent to the connection",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
|
metricConnectionTotalPingReceivedCount = promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "jetkvm_connection_total_ping_received",
|
||||||
|
Help: "The total number of pings received from the connection",
|
||||||
|
},
|
||||||
|
[]string{"type", "source"},
|
||||||
|
)
|
||||||
metricConnectionSessionRequestCount = promauto.NewCounterVec(
|
metricConnectionSessionRequestCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_connection_session_total_request_count",
|
Name: "jetkvm_connection_session_total_requests",
|
||||||
Help: "The total number of session requests received",
|
Help: "The total number of session requests received",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
|
@ -131,6 +146,8 @@ func wsResetMetrics(established bool, sourceType string, source string) {
|
||||||
metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1)
|
metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1)
|
||||||
metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1)
|
metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1)
|
||||||
|
|
||||||
|
metricConnectionLastPingReceivedTimestamp.WithLabelValues(sourceType, source).Set(-1)
|
||||||
|
|
||||||
metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).Set(-1)
|
metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).Set(-1)
|
||||||
metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(-1)
|
metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(-1)
|
||||||
|
|
||||||
|
@ -275,18 +292,31 @@ func runWebsocketClient() error {
|
||||||
defer cancelDial()
|
defer cancelDial()
|
||||||
c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{
|
c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{
|
||||||
HTTPHeader: header,
|
HTTPHeader: header,
|
||||||
|
OnPingReceived: func(ctx context.Context, payload []byte) bool {
|
||||||
|
websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: cloud", payload, wsURL.Host)
|
||||||
|
|
||||||
|
metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc()
|
||||||
|
metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime()
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
// if the context is canceled, we don't want to return an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
cloudLogger.Infof("websocket connection canceled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer c.CloseNow() //nolint:errcheck
|
defer c.CloseNow() //nolint:errcheck
|
||||||
cloudLogger.Infof("websocket connected to %s", wsURL)
|
cloudLogger.Infof("websocket connected to %s", wsURL)
|
||||||
|
|
||||||
// set the metrics when we successfully connect to the cloud.
|
// set the metrics when we successfully connect to the cloud.
|
||||||
wsResetMetrics(true, "cloud", "")
|
wsResetMetrics(true, "cloud", wsURL.Host)
|
||||||
|
|
||||||
// we don't have a source for the cloud connection
|
// we don't have a source for the cloud connection
|
||||||
return handleWebRTCSignalWsMessages(c, true, "")
|
return handleWebRTCSignalWsMessages(c, true, wsURL.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
|
func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
|
||||||
|
@ -375,9 +405,6 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
|
||||||
|
|
||||||
func RunWebsocketClient() {
|
func RunWebsocketClient() {
|
||||||
for {
|
for {
|
||||||
// reset the metrics when we start the websocket client.
|
|
||||||
wsResetMetrics(false, "cloud", "")
|
|
||||||
|
|
||||||
// If the cloud token is not set, we don't need to run the websocket client.
|
// If the cloud token is not set, we don't need to run the websocket client.
|
||||||
if config.CloudToken == "" {
|
if config.CloudToken == "" {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
# Exit immediately if a command exits with a non-zero status
|
# Exit immediately if a command exits with a non-zero status
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
@ -16,7 +18,6 @@ show_help() {
|
||||||
echo "Example:"
|
echo "Example:"
|
||||||
echo " $0 -r 192.168.0.17"
|
echo " $0 -r 192.168.0.17"
|
||||||
echo " $0 -r 192.168.0.17 -u admin"
|
echo " $0 -r 192.168.0.17 -u admin"
|
||||||
exit 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
|
@ -70,7 +71,7 @@ cd bin
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||||
|
|
||||||
# Copy the binary to the remote host
|
# Copy the binary to the remote host
|
||||||
cat jetkvm_app | ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > $REMOTE_PATH/jetkvm_app_debug"
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < jetkvm_app
|
||||||
|
|
||||||
# Deploy and run the application on the remote host
|
# Deploy and run the application on the remote host
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||||
|
@ -84,13 +85,13 @@ killall jetkvm_app || true
|
||||||
killall jetkvm_app_debug || true
|
killall jetkvm_app_debug || true
|
||||||
|
|
||||||
# Navigate to the directory where the binary will be stored
|
# Navigate to the directory where the binary will be stored
|
||||||
cd "$REMOTE_PATH"
|
cd "${REMOTE_PATH}"
|
||||||
|
|
||||||
# Make the new binary executable
|
# Make the new binary executable
|
||||||
chmod +x jetkvm_app_debug
|
chmod +x jetkvm_app_debug
|
||||||
|
|
||||||
# Run the application in the background
|
# Run the application in the background
|
||||||
PION_LOG_TRACE=jetkvm,cloud ./jetkvm_app_debug
|
PION_LOG_TRACE=jetkvm,cloud,websocket ./jetkvm_app_debug
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Deployment complete."
|
echo "Deployment complete."
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -7,7 +7,7 @@ toolchain go1.21.1
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.3.0
|
github.com/Masterminds/semver/v3 v3.3.0
|
||||||
github.com/beevik/ntp v1.3.1
|
github.com/beevik/ntp v1.3.1
|
||||||
github.com/coder/websocket v1.8.12
|
github.com/coder/websocket v1.8.13
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0
|
github.com/coreos/go-oidc/v3 v3.11.0
|
||||||
github.com/creack/pty v1.1.23
|
github.com/creack/pty v1.1.23
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -18,6 +18,8 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
|
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||||
|
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||||
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||||
|
|
6
ota.go
6
ota.go
|
@ -126,12 +126,10 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
|
||||||
return fmt.Errorf("error creating request: %w", err)
|
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{
|
client := http.Client{
|
||||||
// allow a longer timeout for the download but keep the TLS handshake short
|
|
||||||
Timeout: 10 * time.Minute,
|
Timeout: 10 * time.Minute,
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSHandshakeTimeout: 1 * time.Minute,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Check if a commit message was provided
|
# Check if a commit message was provided
|
||||||
if [ -z "$1" ]; then
|
if [ -z "$1" ]; then
|
||||||
|
@ -26,7 +26,7 @@ git checkout -b release-temp
|
||||||
if git ls-remote --heads public main | grep -q 'refs/heads/main'; then
|
if git ls-remote --heads public main | grep -q 'refs/heads/main'; then
|
||||||
git reset --soft public/main
|
git reset --soft public/main
|
||||||
else
|
else
|
||||||
git reset --soft $(git rev-list --max-parents=0 HEAD)
|
git reset --soft "$(git rev-list --max-parents=0 HEAD)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Merge changes from main
|
# Merge changes from main
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Check if an IP address was provided as an argument
|
# Check if an IP address was provided as an argument
|
||||||
if [ -z "$1" ]; then
|
if [ -z "$1" ]; then
|
||||||
|
@ -16,4 +16,4 @@ echo "└───────────────────────
|
||||||
# Set the environment variable and run Vite
|
# Set the environment variable and run Vite
|
||||||
echo "Starting development server with JetKVM device at: $ip_address"
|
echo "Starting development server with JetKVM device at: $ip_address"
|
||||||
sleep 1
|
sleep 1
|
||||||
JETKVM_PROXY_URL="http://$ip_address" npx vite dev --mode=device
|
JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device
|
||||||
|
|
|
@ -15,12 +15,12 @@ const Modal = React.memo(function Modal({
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} className="relative z-10">
|
<Dialog open={open} onClose={onClose} className="relative z-20">
|
||||||
<DialogBackdrop
|
<DialogBackdrop
|
||||||
transition
|
transition
|
||||||
className="fixed inset-0 bg-gray-500/75 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-500 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in dark:bg-slate-900/90"
|
className="fixed inset-0 bg-gray-500/75 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-500 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in dark:bg-slate-900/90"
|
||||||
/>
|
/>
|
||||||
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
<div className="fixed inset-0 z-20 w-screen overflow-y-auto">
|
||||||
{/* TODO: This doesn't work well with other-sessions */}
|
{/* TODO: This doesn't work well with other-sessions */}
|
||||||
<div className="flex min-h-full items-end justify-center p-4 text-center md:items-baseline md:p-4">
|
<div className="flex min-h-full items-end justify-center p-4 text-center md:items-baseline md:p-4">
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
|
|
|
@ -28,6 +28,7 @@ export default function WebRTCVideo() {
|
||||||
const videoElm = useRef<HTMLVideoElement>(null);
|
const videoElm = useRef<HTMLVideoElement>(null);
|
||||||
const mediaStream = useRTCStore(state => state.mediaStream);
|
const mediaStream = useRTCStore(state => state.mediaStream);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const peerConnectionState = useRTCStore(state => state.peerConnectionState);
|
||||||
|
|
||||||
// Store hooks
|
// Store hooks
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
@ -601,7 +602,10 @@ export default function WebRTCVideo() {
|
||||||
"cursor-none":
|
"cursor-none":
|
||||||
settings.mouseMode === "absolute" &&
|
settings.mouseMode === "absolute" &&
|
||||||
settings.isCursorHidden,
|
settings.isCursorHidden,
|
||||||
"opacity-0": isVideoLoading || hdmiError,
|
"opacity-0":
|
||||||
|
isVideoLoading ||
|
||||||
|
hdmiError ||
|
||||||
|
peerConnectionState !== "connected",
|
||||||
"animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20":
|
"animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20":
|
||||||
isPlaying,
|
isPlaying,
|
||||||
},
|
},
|
||||||
|
|
|
@ -243,7 +243,7 @@ export default function KvmIdRoute() {
|
||||||
{
|
{
|
||||||
heartbeat: true,
|
heartbeat: true,
|
||||||
retryOnError: true,
|
retryOnError: true,
|
||||||
reconnectAttempts: 5,
|
reconnectAttempts: 15,
|
||||||
reconnectInterval: 1000,
|
reconnectInterval: 1000,
|
||||||
onReconnectStop: () => {
|
onReconnectStop: () => {
|
||||||
console.log("Reconnect stopped");
|
console.log("Reconnect stopped");
|
||||||
|
@ -398,11 +398,6 @@ export default function KvmIdRoute() {
|
||||||
setConnectionFailed(false);
|
setConnectionFailed(false);
|
||||||
setLoadingMessage("Connecting to device...");
|
setLoadingMessage("Connecting to device...");
|
||||||
|
|
||||||
if (peerConnection?.signalingState === "stable") {
|
|
||||||
console.log("[setupPeerConnection] Peer connection already established");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pc: RTCPeerConnection;
|
let pc: RTCPeerConnection;
|
||||||
try {
|
try {
|
||||||
console.log("[setupPeerConnection] Creating peer connection");
|
console.log("[setupPeerConnection] Creating peer connection");
|
||||||
|
@ -499,7 +494,6 @@ export default function KvmIdRoute() {
|
||||||
cleanupAndStopReconnecting,
|
cleanupAndStopReconnecting,
|
||||||
iceConfig?.iceServers,
|
iceConfig?.iceServers,
|
||||||
legacyHTTPSignaling,
|
legacyHTTPSignaling,
|
||||||
peerConnection?.signalingState,
|
|
||||||
sendWebRTCSignal,
|
sendWebRTCSignal,
|
||||||
setDiskChannel,
|
setDiskChannel,
|
||||||
setMediaMediaStream,
|
setMediaMediaStream,
|
||||||
|
@ -791,6 +785,7 @@ export default function KvmIdRoute() {
|
||||||
<button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
|
<button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
|
||||||
</div>
|
</div>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
|
|
||||||
<div className="grid h-full select-none grid-rows-headerBody">
|
<div className="grid h-full select-none grid-rows-headerBody">
|
||||||
<DashboardNavbar
|
<DashboardNavbar
|
||||||
primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]}
|
primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]}
|
||||||
|
@ -801,21 +796,23 @@ export default function KvmIdRoute() {
|
||||||
kvmName={deviceName || "JetKVM Device"}
|
kvmName={deviceName || "JetKVM Device"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex h-full w-full overflow-hidden">
|
<div className="relative flex h-full w-full overflow-hidden">
|
||||||
<div className="pointer-events-none fixed inset-0 isolate z-20 flex h-full w-full items-center justify-center">
|
<WebRTCVideo />
|
||||||
<div className="my-2 h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
<div
|
||||||
|
style={{ animationDuration: "500ms" }}
|
||||||
|
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center p-4 opacity-0"
|
||||||
|
>
|
||||||
|
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
||||||
{!!ConnectionStatusElement && ConnectionStatusElement}
|
{!!ConnectionStatusElement && ConnectionStatusElement}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{peerConnectionState === "connected" && <WebRTCVideo />}
|
|
||||||
<SidebarContainer sidebarView={sidebarView} />
|
<SidebarContainer sidebarView={sidebarView} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="isolate"
|
className="z-50"
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
120
web.go
120
web.go
|
@ -1,9 +1,11 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -99,6 +101,22 @@ func setupRouter() *gin.Engine {
|
||||||
protected := r.Group("/")
|
protected := r.Group("/")
|
||||||
protected.Use(protectedMiddleware())
|
protected.Use(protectedMiddleware())
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
|
* Legacy WebRTC session endpoint
|
||||||
|
*
|
||||||
|
* This endpoint is maintained for backward compatibility when users upgrade from a version
|
||||||
|
* using the legacy HTTP-based signaling method to the new WebSocket-based signaling method.
|
||||||
|
*
|
||||||
|
* During the upgrade process, when the "Rebooting device after update..." message appears,
|
||||||
|
* the browser still runs the previous JavaScript code which polls this endpoint to establish
|
||||||
|
* a new WebRTC session. Once the session is established, the page will automatically reload
|
||||||
|
* with the updated code.
|
||||||
|
*
|
||||||
|
* Without this endpoint, the stale JavaScript would fail to establish a connection,
|
||||||
|
* causing users to see the "Rebooting device after update..." message indefinitely
|
||||||
|
* until they manually refresh the page, leading to a confusing user experience.
|
||||||
|
*/
|
||||||
|
protected.POST("/webrtc/session", handleWebRTCSession)
|
||||||
protected.GET("/webrtc/signaling/client", handleLocalWebRTCSignal)
|
protected.GET("/webrtc/signaling/client", handleLocalWebRTCSignal)
|
||||||
protected.POST("/cloud/register", handleCloudRegister)
|
protected.POST("/cloud/register", handleCloudRegister)
|
||||||
protected.GET("/cloud/state", handleCloudState)
|
protected.GET("/cloud/state", handleCloudState)
|
||||||
|
@ -126,11 +144,59 @@ func setupRouter() *gin.Engine {
|
||||||
// TODO: support multiple sessions?
|
// TODO: support multiple sessions?
|
||||||
var currentSession *Session
|
var currentSession *Session
|
||||||
|
|
||||||
|
func handleWebRTCSession(c *gin.Context) {
|
||||||
|
var req WebRTCSessionRequest
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := newSession(SessionConfig{})
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sd, err := session.ExchangeOffer(req.Sd)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if currentSession != nil {
|
||||||
|
writeJSONRPCEvent("otherSessionConnected", nil, currentSession)
|
||||||
|
peerConn := currentSession.peerConnection
|
||||||
|
go func() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
_ = peerConn.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
currentSession = session
|
||||||
|
c.JSON(http.StatusOK, gin.H{"sd": sd})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
pingMessage = []byte("ping")
|
||||||
|
pongMessage = []byte("pong")
|
||||||
|
)
|
||||||
|
|
||||||
func handleLocalWebRTCSignal(c *gin.Context) {
|
func handleLocalWebRTCSignal(c *gin.Context) {
|
||||||
cloudLogger.Infof("new websocket connection established")
|
cloudLogger.Infof("new websocket connection established")
|
||||||
|
|
||||||
|
// get the source from the request
|
||||||
|
source := c.ClientIP()
|
||||||
|
|
||||||
// Create WebSocket options with InsecureSkipVerify to bypass origin check
|
// Create WebSocket options with InsecureSkipVerify to bypass origin check
|
||||||
wsOptions := &websocket.AcceptOptions{
|
wsOptions := &websocket.AcceptOptions{
|
||||||
InsecureSkipVerify: true, // Allow connections from any origin
|
InsecureSkipVerify: true, // Allow connections from any origin
|
||||||
|
OnPingReceived: func(ctx context.Context, payload []byte) bool {
|
||||||
|
websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: local", payload, source)
|
||||||
|
|
||||||
|
metricConnectionTotalPingReceivedCount.WithLabelValues("local", source).Inc()
|
||||||
|
metricConnectionLastPingReceivedTimestamp.WithLabelValues("local", source).SetToCurrentTime()
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
wsCon, err := websocket.Accept(c.Writer, c.Request, wsOptions)
|
wsCon, err := websocket.Accept(c.Writer, c.Request, wsOptions)
|
||||||
|
@ -139,9 +205,6 @@ func handleLocalWebRTCSignal(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the source from the request
|
|
||||||
source := c.ClientIP()
|
|
||||||
|
|
||||||
// Now use conn for websocket operations
|
// Now use conn for websocket operations
|
||||||
defer wsCon.Close(websocket.StatusNormalClosure, "")
|
defer wsCon.Close(websocket.StatusNormalClosure, "")
|
||||||
|
|
||||||
|
@ -164,7 +227,6 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool,
|
||||||
|
|
||||||
// Add connection tracking to detect reconnections
|
// Add connection tracking to detect reconnections
|
||||||
connectionID := uuid.New().String()
|
connectionID := uuid.New().String()
|
||||||
cloudLogger.Infof("new websocket connection established with ID: %s", connectionID)
|
|
||||||
|
|
||||||
// connection type
|
// connection type
|
||||||
var sourceType string
|
var sourceType string
|
||||||
|
@ -176,29 +238,40 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool,
|
||||||
|
|
||||||
// probably we can use a better logging framework here
|
// probably we can use a better logging framework here
|
||||||
logInfof := func(format string, args ...interface{}) {
|
logInfof := func(format string, args ...interface{}) {
|
||||||
args = append(args, source, sourceType)
|
args = append(args, source, sourceType, connectionID)
|
||||||
websocketLogger.Infof(format+", source: %s, sourceType: %s", args...)
|
websocketLogger.Infof(format+", source: %s, sourceType: %s, id: %s", args...)
|
||||||
}
|
}
|
||||||
logWarnf := func(format string, args ...interface{}) {
|
logWarnf := func(format string, args ...interface{}) {
|
||||||
args = append(args, source, sourceType)
|
args = append(args, source, sourceType, connectionID)
|
||||||
websocketLogger.Warnf(format+", source: %s, sourceType: %s", args...)
|
websocketLogger.Warnf(format+", source: %s, sourceType: %s, id: %s", args...)
|
||||||
}
|
}
|
||||||
logTracef := func(format string, args ...interface{}) {
|
logTracef := func(format string, args ...interface{}) {
|
||||||
args = append(args, source, sourceType)
|
args = append(args, source, sourceType, connectionID)
|
||||||
websocketLogger.Tracef(format+", source: %s, sourceType: %s", args...)
|
websocketLogger.Tracef(format+", source: %s, sourceType: %s, id: %s", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logInfof("new websocket connection established")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
time.Sleep(WebsocketPingInterval)
|
time.Sleep(WebsocketPingInterval)
|
||||||
|
|
||||||
|
if ctxErr := runCtx.Err(); ctxErr != nil {
|
||||||
|
if !errors.Is(ctxErr, context.Canceled) {
|
||||||
|
logWarnf("websocket connection closed: %v", ctxErr)
|
||||||
|
} else {
|
||||||
|
logTracef("websocket connection closed as the context was canceled: %v")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// set the timer for the ping duration
|
// set the timer for the ping duration
|
||||||
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
|
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
|
||||||
metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(v)
|
metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(v)
|
||||||
metricConnectionPingDuration.WithLabelValues(sourceType, source).Observe(v)
|
metricConnectionPingDuration.WithLabelValues(sourceType, source).Observe(v)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
logInfof("pinging websocket")
|
logTracef("sending ping frame")
|
||||||
err := wsCon.Ping(runCtx)
|
err := wsCon.Ping(runCtx)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -208,10 +281,12 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// dont use `defer` here because we want to observe the duration of the ping
|
// dont use `defer` here because we want to observe the duration of the ping
|
||||||
timer.ObserveDuration()
|
duration := timer.ObserveDuration()
|
||||||
|
|
||||||
metricConnectionTotalPingCount.WithLabelValues(sourceType, source).Inc()
|
metricConnectionTotalPingSentCount.WithLabelValues(sourceType, source).Inc()
|
||||||
metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
|
metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
|
||||||
|
|
||||||
|
logTracef("received pong frame, duration: %v", duration)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -249,6 +324,20 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool,
|
||||||
Data json.RawMessage `json:"data"`
|
Data json.RawMessage `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(msg, pingMessage) {
|
||||||
|
logInfof("ping message received: %s", string(msg))
|
||||||
|
err = wsCon.Write(context.Background(), websocket.MessageText, pongMessage)
|
||||||
|
if err != nil {
|
||||||
|
logWarnf("unable to write pong message: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
metricConnectionTotalPingReceivedCount.WithLabelValues(sourceType, source).Inc()
|
||||||
|
metricConnectionLastPingReceivedTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(msg, &message)
|
err = json.Unmarshal(msg, &message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logWarnf("unable to parse ws message: %v", err)
|
logWarnf("unable to parse ws message: %v", err)
|
||||||
|
@ -264,8 +353,9 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool,
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfof("new session request: %v", req.OidcGoogle)
|
if req.OidcGoogle != "" {
|
||||||
logTracef("session request info: %v", req)
|
logInfof("new session request with OIDC Google: %v", req.OidcGoogle)
|
||||||
|
}
|
||||||
|
|
||||||
metricConnectionSessionRequestCount.WithLabelValues(sourceType, source).Inc()
|
metricConnectionSessionRequestCount.WithLabelValues(sourceType, source).Inc()
|
||||||
metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
|
metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
|
||||||
|
|
Loading…
Reference in New Issue