mirror of https://github.com/jetkvm/kvm.git
Compare commits
7 Commits
ac304b3855
...
cceb8d18ed
Author | SHA1 | Date |
---|---|---|
|
cceb8d18ed | |
|
1d6b7ad83a | |
|
0d7efe5c0e | |
|
15768ee0ab | |
|
2e8ea8cccc | |
|
a819739790 | |
|
7602aefe98 |
4
Makefile
4
Makefile
|
@ -1,5 +1,5 @@
|
||||||
VERSION_DEV := 0.3.5-dev$(shell date +%Y%m%d%H%M)
|
VERSION_DEV := 0.3.6-dev$(shell date +%Y%m%d%H%M)
|
||||||
VERSION := 0.3.4
|
VERSION := 0.3.5
|
||||||
|
|
||||||
hash_resource:
|
hash_resource:
|
||||||
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
||||||
|
|
11
cloud.go
11
cloud.go
|
@ -7,13 +7,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"github.com/coder/websocket/wsjson"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/websocket/wsjson"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CloudRegisterRequest struct {
|
type CloudRegisterRequest struct {
|
||||||
|
@ -192,7 +193,11 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
|
||||||
return fmt.Errorf("google identity mismatch")
|
return fmt.Errorf("google identity mismatch")
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := newSession()
|
session, err := newSession(SessionConfig{
|
||||||
|
ICEServers: req.ICEServers,
|
||||||
|
LocalIP: req.IP,
|
||||||
|
IsCloud: true,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = wsjson.Write(context.Background(), c, gin.H{"error": err})
|
_ = wsjson.Write(context.Background(), c, gin.H{"error": err})
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -10,12 +10,14 @@ import Container from "@components/Container";
|
||||||
import { LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
import { LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import PasteModal from "@/components/popovers/PasteModal";
|
import PasteModal from "@/components/popovers/PasteModal";
|
||||||
import { FaKeyboard } from "react-icons/fa6";
|
import { FaKeyboard, FaLock } from "react-icons/fa6";
|
||||||
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
import MountPopopover from "./popovers/MountPopover";
|
import MountPopopover from "./popovers/MountPopover";
|
||||||
import { Fragment, useCallback, useRef } from "react";
|
import { Fragment, useCallback, useRef } from "react";
|
||||||
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
||||||
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export default function Actionbar({
|
export default function Actionbar({
|
||||||
requestFullscreen,
|
requestFullscreen,
|
||||||
|
@ -52,6 +54,8 @@ export default function Actionbar({
|
||||||
[setDisableFocusTrap],
|
[setDisableFocusTrap],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="bg-white border-b border-b-slate-800/20 dark:bg-slate-900 dark:border-b-slate-300/20">
|
<Container className="bg-white border-b border-b-slate-800/20 dark:bg-slate-900 dark:border-b-slate-300/20">
|
||||||
<div
|
<div
|
||||||
|
@ -203,6 +207,23 @@ export default function Actionbar({
|
||||||
onClick={() => setVirtualKeyboard(!virtualKeyboard)}
|
onClick={() => setVirtualKeyboard(!virtualKeyboard)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{useSettingsStore().actionBarCtrlAltDel && (
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<Button
|
||||||
|
size="XS"
|
||||||
|
theme="light"
|
||||||
|
text="Ctrl + Alt + Del"
|
||||||
|
LeadingIcon={FaLock}
|
||||||
|
onClick={() => {
|
||||||
|
sendKeyboardEvent(
|
||||||
|
[keys["Delete"]],
|
||||||
|
[modifiers["ControlLeft"], modifiers["AltLeft"]],
|
||||||
|
);
|
||||||
|
setTimeout(resetKeyboardState, 100);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-2">
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-2">
|
||||||
|
|
|
@ -534,17 +534,17 @@ function UrlView({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Debian 12",
|
name: "Debian 12",
|
||||||
url: "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.7.0-amd64-netinst.iso",
|
url: "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.9.0-amd64-netinst.iso",
|
||||||
icon: DebianIcon,
|
icon: DebianIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Fedora 38",
|
name: "Fedora 41",
|
||||||
url: "https://mirror.ihost.md/fedora/releases/38/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-38-1.6.iso",
|
url: "https://download.fedoraproject.org/pub/fedora/linux/releases/41/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-41-1.4.iso",
|
||||||
icon: FedoraIcon,
|
icon: FedoraIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Arch Linux",
|
name: "Arch Linux",
|
||||||
url: "https://archlinux.doridian.net/iso/2024.10.01/archlinux-2024.10.01-x86_64.iso",
|
url: "https://archlinux.doridian.net/iso/2025.02.01/archlinux-2025.02.01-x86_64.iso",
|
||||||
icon: ArchIcon,
|
icon: ArchIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -425,7 +425,7 @@ export default function WebRTCVideo() {
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
controlsList="nofullscreen"
|
controlsList="nofullscreen"
|
||||||
className={cx(
|
className={cx(
|
||||||
"outline-50 max-h-full max-w-full rounded-md object-contain transition-all duration-1000",
|
"outline-50 max-h-full max-w-full object-contain transition-all duration-1000",
|
||||||
{
|
{
|
||||||
"cursor-none": settings.isCursorHidden,
|
"cursor-none": settings.isCursorHidden,
|
||||||
"opacity-0": isLoading || isConnectionError || hdmiError,
|
"opacity-0": isLoading || isConnectionError || hdmiError,
|
||||||
|
|
|
@ -796,6 +796,15 @@ export default function SettingsSidebar() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
<SettingsItem
|
||||||
|
title="Ctrl + Alt + Del Button"
|
||||||
|
description="Display Ctrl + Alt + Del button on the Action Bar"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.actionBarCtrlAltDel}
|
||||||
|
onChange={e => settings.setActionBarCtrlAltDel(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
<div className="pb-2 space-y-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
|
|
|
@ -270,6 +270,9 @@ interface SettingsState {
|
||||||
// Add new developer mode state
|
// Add new developer mode state
|
||||||
developerMode: boolean;
|
developerMode: boolean;
|
||||||
setDeveloperMode: (enabled: boolean) => void;
|
setDeveloperMode: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
actionBarCtrlAltDel: boolean;
|
||||||
|
setActionBarCtrlAltDel: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create(
|
export const useSettingsStore = create(
|
||||||
|
@ -287,6 +290,9 @@ export const useSettingsStore = create(
|
||||||
// Add developer mode with default value
|
// Add developer mode with default value
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
||||||
|
|
||||||
|
actionBarCtrlAltDel: false,
|
||||||
|
setActionBarCtrlAltDel: enabled => set({ actionBarCtrlAltDel: enabled }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
|
|
4
web.go
4
web.go
|
@ -19,6 +19,8 @@ var staticFiles embed.FS
|
||||||
type WebRTCSessionRequest struct {
|
type WebRTCSessionRequest struct {
|
||||||
Sd string `json:"sd"`
|
Sd string `json:"sd"`
|
||||||
OidcGoogle string `json:"OidcGoogle,omitempty"`
|
OidcGoogle string `json:"OidcGoogle,omitempty"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
ICEServers []string `json:"iceServers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SetPasswordRequest struct {
|
type SetPasswordRequest struct {
|
||||||
|
@ -116,7 +118,7 @@ func handleWebRTCSession(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := newSession()
|
session, err := newSession(SessionConfig{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err})
|
||||||
return
|
return
|
||||||
|
|
33
webrtc.go
33
webrtc.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
|
@ -19,6 +20,12 @@ type Session struct {
|
||||||
shouldUmountVirtualMedia bool
|
shouldUmountVirtualMedia bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SessionConfig struct {
|
||||||
|
ICEServers []string
|
||||||
|
LocalIP string
|
||||||
|
IsCloud bool
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
||||||
b, err := base64.StdEncoding.DecodeString(offerStr)
|
b, err := base64.StdEncoding.DecodeString(offerStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -61,9 +68,29 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
||||||
return base64.StdEncoding.EncodeToString(localDescription), nil
|
return base64.StdEncoding.EncodeToString(localDescription), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSession() (*Session, error) {
|
func newSession(config SessionConfig) (*Session, error) {
|
||||||
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
|
webrtcSettingEngine := webrtc.SettingEngine{}
|
||||||
ICEServers: []webrtc.ICEServer{{}},
|
iceServer := webrtc.ICEServer{}
|
||||||
|
|
||||||
|
if config.IsCloud {
|
||||||
|
if config.ICEServers == nil {
|
||||||
|
fmt.Printf("ICE Servers not provided by cloud")
|
||||||
|
} else {
|
||||||
|
iceServer.URLs = config.ICEServers
|
||||||
|
fmt.Printf("Using ICE Servers provided by cloud: %v\n", iceServer.URLs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.LocalIP == "" || net.ParseIP(config.LocalIP) == nil {
|
||||||
|
fmt.Printf("Local IP address %v not provided or invalid, won't set NAT1To1IPs\n", config.LocalIP)
|
||||||
|
} else {
|
||||||
|
webrtcSettingEngine.SetNAT1To1IPs([]string{config.LocalIP}, webrtc.ICECandidateTypeSrflx)
|
||||||
|
fmt.Printf("Setting NAT1To1IPs to %s\n", config.LocalIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api := webrtc.NewAPI(webrtc.WithSettingEngine(webrtcSettingEngine))
|
||||||
|
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
|
||||||
|
ICEServers: []webrtc.ICEServer{iceServer},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
Loading…
Reference in New Issue