Skip to content

Commit 3ea45bb

Browse files
authored
Merge branch 'dev' into refactor/keyboard-layouts
2 parents 6a7577d + 718b343 commit 3ea45bb

23 files changed

+747
-134
lines changed

.devcontainer/devcontainer.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@
99
},
1010
"mounts": [
1111
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
12-
]
12+
],
13+
"customizations": {
14+
"vscode": {
15+
"extensions": [
16+
"bradlc.vscode-tailwindcss",
17+
"GitHub.vscode-pull-request-github",
18+
"dbaeumer.vscode-eslint",
19+
"golang.go",
20+
"ms-vscode.makefile-tools",
21+
"esbenp.prettier-vscode",
22+
"github.vscode-github-actions"
23+
]
24+
}
25+
}
1326
}
1427

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"tailwindCSS.classFunctions": ["cva", "cx"]
3+
}

config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type Config struct {
8585
HashedPassword string `json:"hashed_password"`
8686
LocalAuthToken string `json:"local_auth_token"`
8787
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
88+
LocalLoopbackOnly bool `json:"local_loopback_only"`
8889
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
8990
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
9091
KeyboardLayout string `json:"keyboard_layout"`

internal/usbgadget/hid_keyboard.go

Lines changed: 136 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package usbgadget
22

33
import (
4+
"context"
45
"fmt"
56
"os"
7+
"reflect"
8+
"time"
69
)
710

811
var keyboardConfig = gadgetConfigItem{
@@ -36,6 +39,7 @@ var keyboardReportDesc = []byte{
3639
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
3740
0x95, 0x05, /* REPORT_COUNT (5) */
3841
0x75, 0x01, /* REPORT_SIZE (1) */
42+
3943
0x05, 0x08, /* USAGE_PAGE (LEDs) */
4044
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
4145
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
@@ -54,13 +58,139 @@ var keyboardReportDesc = []byte{
5458
0xc0, /* END_COLLECTION */
5559
}
5660

57-
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
58-
if u.keyboardHidFile == nil {
59-
var err error
60-
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
61-
if err != nil {
62-
return fmt.Errorf("failed to open hidg0: %w", err)
61+
const (
62+
hidReadBufferSize = 8
63+
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
64+
// https://www.usb.org/sites/default/files/hut1_2.pdf
65+
KeyboardLedMaskNumLock = 1 << 0
66+
KeyboardLedMaskCapsLock = 1 << 1
67+
KeyboardLedMaskScrollLock = 1 << 2
68+
KeyboardLedMaskCompose = 1 << 3
69+
KeyboardLedMaskKana = 1 << 4
70+
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana
71+
)
72+
73+
// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
74+
// COMPOSE, and KANA events is maintained by the host and NOT the keyboard. If
75+
// using the keyboard descriptor in Appendix B, LED states are set by sending a
76+
// 5-bit absolute report to the keyboard via a Set_Report(Output) request.
77+
type KeyboardState struct {
78+
NumLock bool `json:"num_lock"`
79+
CapsLock bool `json:"caps_lock"`
80+
ScrollLock bool `json:"scroll_lock"`
81+
Compose bool `json:"compose"`
82+
Kana bool `json:"kana"`
83+
}
84+
85+
func getKeyboardState(b byte) KeyboardState {
86+
// should we check if it's the correct usage page?
87+
return KeyboardState{
88+
NumLock: b&KeyboardLedMaskNumLock != 0,
89+
CapsLock: b&KeyboardLedMaskCapsLock != 0,
90+
ScrollLock: b&KeyboardLedMaskScrollLock != 0,
91+
Compose: b&KeyboardLedMaskCompose != 0,
92+
Kana: b&KeyboardLedMaskKana != 0,
93+
}
94+
}
95+
96+
func (u *UsbGadget) updateKeyboardState(b byte) {
97+
u.keyboardStateLock.Lock()
98+
defer u.keyboardStateLock.Unlock()
99+
100+
if b&^ValidKeyboardLedMasks != 0 {
101+
u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring")
102+
return
103+
}
104+
105+
newState := getKeyboardState(b)
106+
if reflect.DeepEqual(u.keyboardState, newState) {
107+
return
108+
}
109+
u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated")
110+
u.keyboardState = newState
111+
112+
if u.onKeyboardStateChange != nil {
113+
(*u.onKeyboardStateChange)(newState)
114+
}
115+
}
116+
117+
func (u *UsbGadget) SetOnKeyboardStateChange(f func(state KeyboardState)) {
118+
u.onKeyboardStateChange = &f
119+
}
120+
121+
func (u *UsbGadget) GetKeyboardState() KeyboardState {
122+
u.keyboardStateLock.Lock()
123+
defer u.keyboardStateLock.Unlock()
124+
125+
return u.keyboardState
126+
}
127+
128+
func (u *UsbGadget) listenKeyboardEvents() {
129+
var path string
130+
if u.keyboardHidFile != nil {
131+
path = u.keyboardHidFile.Name()
132+
}
133+
l := u.log.With().Str("listener", "keyboardEvents").Str("path", path).Logger()
134+
l.Trace().Msg("starting")
135+
136+
go func() {
137+
buf := make([]byte, hidReadBufferSize)
138+
for {
139+
select {
140+
case <-u.keyboardStateCtx.Done():
141+
l.Info().Msg("context done")
142+
return
143+
default:
144+
l.Trace().Msg("reading from keyboard")
145+
if u.keyboardHidFile == nil {
146+
l.Error().Msg("keyboardHidFile is nil")
147+
time.Sleep(time.Second)
148+
continue
149+
}
150+
n, err := u.keyboardHidFile.Read(buf)
151+
if err != nil {
152+
l.Error().Err(err).Msg("failed to read")
153+
continue
154+
}
155+
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
156+
if n != 1 {
157+
l.Trace().Int("n", n).Msg("expected 1 byte, got")
158+
continue
159+
}
160+
u.updateKeyboardState(buf[0])
161+
}
63162
}
163+
}()
164+
}
165+
166+
func (u *UsbGadget) openKeyboardHidFile() error {
167+
if u.keyboardHidFile != nil {
168+
return nil
169+
}
170+
171+
var err error
172+
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
173+
if err != nil {
174+
return fmt.Errorf("failed to open hidg0: %w", err)
175+
}
176+
177+
if u.keyboardStateCancel != nil {
178+
u.keyboardStateCancel()
179+
}
180+
181+
u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background())
182+
u.listenKeyboardEvents()
183+
184+
return nil
185+
}
186+
187+
func (u *UsbGadget) OpenKeyboardHidFile() error {
188+
return u.openKeyboardHidFile()
189+
}
190+
191+
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
192+
if err := u.openKeyboardHidFile(); err != nil {
193+
return err
64194
}
65195

66196
_, err := u.keyboardHidFile.Write(data)

internal/usbgadget/usbgadget.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package usbgadget
44

55
import (
6+
"context"
67
"os"
78
"path"
89
"sync"
@@ -59,6 +60,11 @@ type UsbGadget struct {
5960
relMouseHidFile *os.File
6061
relMouseLock sync.Mutex
6162

63+
keyboardState KeyboardState
64+
keyboardStateLock sync.Mutex
65+
keyboardStateCtx context.Context
66+
keyboardStateCancel context.CancelFunc
67+
6268
enabledDevices Devices
6369

6470
strictMode bool // only intended for testing for now
@@ -70,6 +76,8 @@ type UsbGadget struct {
7076
tx *UsbGadgetTransaction
7177
txLock sync.Mutex
7278

79+
onKeyboardStateChange *func(state KeyboardState)
80+
7381
log *zerolog.Logger
7482
}
7583

@@ -96,20 +104,25 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
96104
config = &Config{isEmpty: true}
97105
}
98106

107+
keyboardCtx, keyboardCancel := context.WithCancel(context.Background())
108+
99109
g := &UsbGadget{
100-
name: name,
101-
kvmGadgetPath: path.Join(gadgetPath, name),
102-
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
103-
configMap: configMap,
104-
customConfig: *config,
105-
configLock: sync.Mutex{},
106-
keyboardLock: sync.Mutex{},
107-
absMouseLock: sync.Mutex{},
108-
relMouseLock: sync.Mutex{},
109-
txLock: sync.Mutex{},
110-
enabledDevices: *enabledDevices,
111-
lastUserInput: time.Now(),
112-
log: logger,
110+
name: name,
111+
kvmGadgetPath: path.Join(gadgetPath, name),
112+
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
113+
configMap: configMap,
114+
customConfig: *config,
115+
configLock: sync.Mutex{},
116+
keyboardLock: sync.Mutex{},
117+
absMouseLock: sync.Mutex{},
118+
relMouseLock: sync.Mutex{},
119+
txLock: sync.Mutex{},
120+
keyboardStateCtx: keyboardCtx,
121+
keyboardStateCancel: keyboardCancel,
122+
keyboardState: KeyboardState{},
123+
enabledDevices: *enabledDevices,
124+
lastUserInput: time.Now(),
125+
log: logger,
113126

114127
strictMode: config.strictMode,
115128

internal/websecure/ed25519_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package websecure
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
var (
9+
fixtureEd25519Certificate = `-----BEGIN CERTIFICATE-----
10+
MIIBQDCB86ADAgECAhQdB4qB6dV0/u1lwhJofQgkmjjV1zAFBgMrZXAwLzELMAkG
11+
A1UEBhMCREUxIDAeBgNVBAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMB4XDTI1
12+
MDUyMzEyNTkyN1oXDTI3MDQyMzEyNTkyN1owLzELMAkGA1UEBhMCREUxIDAeBgNV
13+
BAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMCowBQYDK2VwAyEA9tLyoulJn7Ev
14+
bf8kuD1ZGdA092773pCRjFEDKpXHonyjITAfMB0GA1UdDgQWBBRkmrVMfsLY57iy
15+
r/0POP0S4QxCADAFBgMrZXADQQBfTRvqavLHDYQiKQTgbGod+Yn+fIq2lE584+1U
16+
C4wh9peIJDFocLBEAYTQpEMKxa4s0AIRxD+a7aCS5oz0e/0I
17+
-----END CERTIFICATE-----`
18+
19+
fixtureEd25519PrivateKey = `-----BEGIN PRIVATE KEY-----
20+
MC4CAQAwBQYDK2VwBCIEIKV08xUsLRHBfMXqZwxVRzIbViOp8G7aQGjPvoRFjujB
21+
-----END PRIVATE KEY-----`
22+
23+
certStore *CertStore
24+
certSigner *SelfSigner
25+
)
26+
27+
func TestMain(m *testing.M) {
28+
tlsStorePath, err := os.MkdirTemp("", "jktls.*")
29+
if err != nil {
30+
defaultLogger.Fatal().Err(err).Msg("failed to create temp directory")
31+
}
32+
33+
certStore = NewCertStore(tlsStorePath, nil)
34+
certStore.LoadCertificates()
35+
36+
certSigner = NewSelfSigner(
37+
certStore,
38+
nil,
39+
"ci.jetkvm.com",
40+
"JetKVM",
41+
"JetKVM",
42+
"JetKVM",
43+
)
44+
45+
m.Run()
46+
47+
os.RemoveAll(tlsStorePath)
48+
}
49+
50+
func TestSaveEd25519Certificate(t *testing.T) {
51+
err, _ := certStore.ValidateAndSaveCertificate("ed25519-test.jetkvm.com", fixtureEd25519Certificate, fixtureEd25519PrivateKey, true)
52+
if err != nil {
53+
t.Fatalf("failed to save certificate: %v", err)
54+
}
55+
}

internal/websecure/utils.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package websecure
22

33
import (
44
"crypto/ecdsa"
5+
"crypto/ed25519"
56
"crypto/rand"
67
"crypto/rsa"
78
"crypto/tls"
@@ -37,11 +38,15 @@ func keyToFile(cert *tls.Certificate, filename string) error {
3738
if e != nil {
3839
return fmt.Errorf("failed to marshal EC private key: %v", e)
3940
}
40-
4141
keyBlock = pem.Block{
4242
Type: "EC PRIVATE KEY",
4343
Bytes: b,
4444
}
45+
case ed25519.PrivateKey:
46+
keyBlock = pem.Block{
47+
Type: "ED25519 PRIVATE KEY",
48+
Bytes: k,
49+
}
4550
default:
4651
return fmt.Errorf("unknown private key type: %T", k)
4752
}

jsonrpc.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,25 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
10061006
return nil, nil
10071007
}
10081008

1009+
func rpcGetLocalLoopbackOnly() (bool, error) {
1010+
return config.LocalLoopbackOnly, nil
1011+
}
1012+
1013+
func rpcSetLocalLoopbackOnly(enabled bool) error {
1014+
// Check if the setting is actually changing
1015+
if config.LocalLoopbackOnly == enabled {
1016+
return nil
1017+
}
1018+
1019+
// Update the setting
1020+
config.LocalLoopbackOnly = enabled
1021+
if err := SaveConfig(); err != nil {
1022+
return fmt.Errorf("failed to save config: %w", err)
1023+
}
1024+
1025+
return nil
1026+
}
1027+
10091028
var rpcHandlers = map[string]RPCHandler{
10101029
"ping": {Func: rpcPing},
10111030
"reboot": {Func: rpcReboot, Params: []string{"force"}},
@@ -1017,6 +1036,7 @@ var rpcHandlers = map[string]RPCHandler{
10171036
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
10181037
"renewDHCPLease": {Func: rpcRenewDHCPLease},
10191038
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
1039+
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
10201040
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
10211041
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
10221042
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
@@ -1082,4 +1102,6 @@ var rpcHandlers = map[string]RPCHandler{
10821102
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
10831103
"getKeyboardMacros": {Func: getKeyboardMacros},
10841104
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
1105+
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
1106+
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
10851107
}

0 commit comments

Comments
 (0)