mirror of https://github.com/jetkvm/kvm.git
Compare commits
1 Commits
84f487a17f
...
9b78cf9f0b
| Author | SHA1 | Date |
|---|---|---|
|
|
9b78cf9f0b |
14
Makefile
14
Makefile
|
|
@ -62,19 +62,7 @@ build_dev_test: build_test2json build_gotestsum
|
||||||
tar czfv device-tests.tar.gz -C $(BIN_DIR)/tests .
|
tar czfv device-tests.tar.gz -C $(BIN_DIR)/tests .
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
cd ui && npm ci && npm run build:device && \
|
cd ui && npm ci && npm run build:device
|
||||||
find ../static/assets \
|
|
||||||
-type f \
|
|
||||||
\( -name '*.js' \
|
|
||||||
-o -name '*.css' \
|
|
||||||
-o -name '*.png' \
|
|
||||||
-o -name '*.jpg' \
|
|
||||||
-o -name '*.jpeg' \
|
|
||||||
-o -name '*.gif' \
|
|
||||||
-o -name '*.webp' \
|
|
||||||
-o -name '*.woff2' \
|
|
||||||
\) \
|
|
||||||
-exec sh -c 'gzip -9 -kfv {}' \;
|
|
||||||
|
|
||||||
dev_release: frontend build_dev
|
dev_release: frontend build_dev
|
||||||
@echo "Uploading release..."
|
@echo "Uploading release..."
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -81,7 +81,6 @@ require (
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
github.com/vearutop/statigz v1.5.0 // indirect
|
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/arch v0.18.0 // indirect
|
golang.org/x/arch v0.18.0 // indirect
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -171,8 +171,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/vearutop/statigz v1.5.0 h1:FuWwZiT82yBw4xbWdWIawiP2XFTyEPhIo8upRxiKLqk=
|
|
||||||
github.com/vearutop/statigz v1.5.0/go.mod h1:oHmjFf3izfCO804Di1ZjB666P3fAlVzJEx2k6jNt/Gk=
|
|
||||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidSSHKeyTypes is a list of valid SSH key types
|
|
||||||
//
|
|
||||||
// Please make sure that all the types in this list are supported by dropbear
|
|
||||||
// https://github.com/mkj/dropbear/blob/003c5fcaabc114430d5d14142e95ffdbbd2d19b6/src/signkey.c#L37
|
|
||||||
//
|
|
||||||
// ssh-dss is not allowed here as it's insecure
|
|
||||||
var ValidSSHKeyTypes = []string{
|
|
||||||
ssh.KeyAlgoRSA,
|
|
||||||
ssh.KeyAlgoED25519,
|
|
||||||
ssh.KeyAlgoECDSA256,
|
|
||||||
ssh.KeyAlgoECDSA384,
|
|
||||||
ssh.KeyAlgoECDSA521,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateSSHKey validates authorized_keys file content
|
|
||||||
func ValidateSSHKey(sshKey string) error {
|
|
||||||
// validate SSH key
|
|
||||||
var (
|
|
||||||
hasValidPublicKey = false
|
|
||||||
lastError = fmt.Errorf("no valid SSH key found")
|
|
||||||
)
|
|
||||||
for _, key := range strings.Split(sshKey, "\n") {
|
|
||||||
key = strings.TrimSpace(key)
|
|
||||||
|
|
||||||
// skip empty lines and comments
|
|
||||||
if key == "" || strings.HasPrefix(key, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedPublicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
|
|
||||||
if err != nil {
|
|
||||||
lastError = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedPublicKey == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedType := parsedPublicKey.Type()
|
|
||||||
textType := strings.Fields(key)[0]
|
|
||||||
|
|
||||||
if parsedType != textType {
|
|
||||||
lastError = fmt.Errorf("parsed SSH key type %s does not match type in text %s", parsedType, textType)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !slices.Contains(ValidSSHKeyTypes, parsedType) {
|
|
||||||
lastError = fmt.Errorf("invalid SSH key type: %s", parsedType)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
hasValidPublicKey = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasValidPublicKey {
|
|
||||||
return lastError
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateSSHKey(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
sshKey string
|
|
||||||
expectError bool
|
|
||||||
errorMsg string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid RSA key",
|
|
||||||
sshKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDiYUb9Fy2vlPfO+HwubnshimpVrWPoePyvyN+jPC5gWqZSycjMy6Is2vFVn7oQc72bkY0wZalspT5wUOwKtltSoLpL7vcqGL9zHVw4yjYXtPGIRd3zLpU9wdngevnepPQWTX3LvZTZfmOsrGoMDKIG+Lbmiq/STMuWYecIqMp7tUKRGS8vfAmpu6MsrN9/4UTcdWWXYWJQQn+2nCyMz28jYlWRsKtqFK6owrdZWt8WQnPN+9Upcf2ByQje+0NLnpNrnh+yd2ocuVW9wQYKAZXy7IaTfEJwd5m34sLwkqlZTaBBcmWJU+3RfpYXE763cf3rUoPIGQ8eUEBJ8IdM4vhp test@example.com",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid ED25519 key",
|
|
||||||
sshKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBSbM8wuD5ab0nHsXaYOqaD3GLLUwmDzSk79Xi/N+H2j test@example.com",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid ECDSA key",
|
|
||||||
sshKey: "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAlTkxIo4mXBR+gEX0Q74BpYX4bFFHoX+8Uz7tsob8HvsnMvsEE+BW9h9XrbWX4/4ppL/o6sHbvsqNr9HcyKfdc= test@example.com",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple valid keys",
|
|
||||||
sshKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDiYUb9Fy2vlPfO+HwubnshimpVrWPoePyvyN+jPC5gWqZSycjMy6Is2vFVn7oQc72bkY0wZalspT5wUOwKtltSoLpL7vcqGL9zHVw4yjYXtPGIRd3zLpU9wdngevnepPQWTX3LvZTZfmOsrGoMDKIG+Lbmiq/STMuWYecIqMp7tUKRGS8vfAmpu6MsrN9/4UTcdWWXYWJQQn+2nCyMz28jYlWRsKtqFK6owrdZWt8WQnPN+9Upcf2ByQje+0NLnpNrnh+yd2ocuVW9wQYKAZXy7IaTfEJwd5m34sLwkqlZTaBBcmWJU+3RfpYXE763cf3rUoPIGQ8eUEBJ8IdM4vhp test@example.com\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBSbM8wuD5ab0nHsXaYOqaD3GLLUwmDzSk79Xi/N+H2j test@example.com",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid key with comment",
|
|
||||||
sshKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDiYUb9Fy2vlPfO+HwubnshimpVrWPoePyvyN+jPC5gWqZSycjMy6Is2vFVn7oQc72bkY0wZalspT5wUOwKtltSoLpL7vcqGL9zHVw4yjYXtPGIRd3zLpU9wdngevnepPQWTX3LvZTZfmOsrGoMDKIG+Lbmiq/STMuWYecIqMp7tUKRGS8vfAmpu6MsrN9/4UTcdWWXYWJQQn+2nCyMz28jYlWRsKtqFK6owrdZWt8WQnPN+9Upcf2ByQje+0NLnpNrnh+yd2ocuVW9wQYKAZXy7IaTfEJwd5m34sLwkqlZTaBBcmWJU+3RfpYXE763cf3rUoPIGQ8eUEBJ8IdM4vhp user@example.com",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid key with options and comment (we don't support options yet)",
|
|
||||||
sshKey: "command=\"echo hello\" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDiYUb9Fy2vlPfO+HwubnshimpVrWPoePyvyN+jPC5gWqZSycjMy6Is2vFVn7oQc72bkY0wZalspT5wUOwKtltSoLpL7vcqGL9zHVw4yjYXtPGIRd3zLpU9wdngevnepPQWTX3LvZTZfmOsrGoMDKIG+Lbmiq/STMuWYecIqMp7tUKRGS8vfAmpu6MsrN9/4UTcdWWXYWJQQn+2nCyMz28jYlWRsKtqFK6owrdZWt8WQnPN+9Upcf2ByQje+0NLnpNrnh+yd2ocuVW9wQYKAZXy7IaTfEJwd5m34sLwkqlZTaBBcmWJU+3RfpYXE763cf3rUoPIGQ8eUEBJ8IdM4vhp user@example.com",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty string",
|
|
||||||
sshKey: "",
|
|
||||||
expectError: true,
|
|
||||||
errorMsg: "no valid SSH key found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "whitespace only",
|
|
||||||
sshKey: " \n\t \n ",
|
|
||||||
expectError: true,
|
|
||||||
errorMsg: "no valid SSH key found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "comment only",
|
|
||||||
sshKey: "# This is a comment\n# Another comment",
|
|
||||||
expectError: true,
|
|
||||||
errorMsg: "no valid SSH key found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid key format",
|
|
||||||
sshKey: "not-a-valid-ssh-key",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid key type",
|
|
||||||
sshKey: "ssh-dss AAAAB3NzaC1kc3MAAACBAOeB...",
|
|
||||||
expectError: true,
|
|
||||||
errorMsg: "invalid SSH key type: ssh-dss",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unsupported key type",
|
|
||||||
sshKey: "ssh-rsa-cert-v01@openssh.com AAAAB3NzaC1yc2EAAAADAQABAAABgQC7vbqajDhA...",
|
|
||||||
expectError: true,
|
|
||||||
errorMsg: "invalid SSH key type: ssh-rsa-cert-v01@openssh.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "malformed key data",
|
|
||||||
sshKey: "ssh-rsa invalid-base64-data",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "type mismatch",
|
|
||||||
sshKey: "ssh-rsa AAAAC3NzaC1lZDI1NTE5AAAAIGomKoH...",
|
|
||||||
expectError: true,
|
|
||||||
errorMsg: "parsed SSH key type ssh-ed25519 does not match type in text ssh-rsa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mixed valid and invalid keys",
|
|
||||||
sshKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDiYUb9Fy2vlPfO+HwubnshimpVrWPoePyvyN+jPC5gWqZSycjMy6Is2vFVn7oQc72bkY0wZalspT5wUOwKtltSoLpL7vcqGL9zHVw4yjYXtPGIRd3zLpU9wdngevnepPQWTX3LvZTZfmOsrGoMDKIG+Lbmiq/STMuWYecIqMp7tUKRGS8vfAmpu6MsrN9/4UTcdWWXYWJQQn+2nCyMz28jYlWRsKtqFK6owrdZWt8WQnPN+9Upcf2ByQje+0NLnpNrnh+yd2ocuVW9wQYKAZXy7IaTfEJwd5m34sLwkqlZTaBBcmWJU+3RfpYXE763cf3rUoPIGQ8eUEBJ8IdM4vhp test@example.com\ninvalid-key\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBSbM8wuD5ab0nHsXaYOqaD3GLLUwmDzSk79Xi/N+H2j test@example.com",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid key with empty lines and comments",
|
|
||||||
sshKey: "# Comment line\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDiYUb9Fy2vlPfO+HwubnshimpVrWPoePyvyN+jPC5gWqZSycjMy6Is2vFVn7oQc72bkY0wZalspT5wUOwKtltSoLpL7vcqGL9zHVw4yjYXtPGIRd3zLpU9wdngevnepPQWTX3LvZTZfmOsrGoMDKIG+Lbmiq/STMuWYecIqMp7tUKRGS8vfAmpu6MsrN9/4UTcdWWXYWJQQn+2nCyMz28jYlWRsKtqFK6owrdZWt8WQnPN+9Upcf2ByQje+0NLnpNrnh+yd2ocuVW9wQYKAZXy7IaTfEJwd5m34sLwkqlZTaBBcmWJU+3RfpYXE763cf3rUoPIGQ8eUEBJ8IdM4vhp test@example.com\n# Another comment\n\t\n",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "all invalid keys",
|
|
||||||
sshKey: "invalid-key-1\ninvalid-key-2\nssh-dss AAAAB3NzaC1kc3MAAACBAOeB...",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
err := ValidateSSHKey(tt.sshKey)
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("ValidateSSHKey() expected error but got none")
|
|
||||||
} else if tt.errorMsg != "" && !strings.ContainsAny(err.Error(), tt.errorMsg) {
|
|
||||||
t.Errorf("ValidateSSHKey() error = %v, expected to contain %v", err, tt.errorMsg)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("ValidateSSHKey() unexpected error = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidSSHKeyTypes(t *testing.T) {
|
|
||||||
expectedTypes := []string{
|
|
||||||
"ssh-rsa",
|
|
||||||
"ssh-ed25519",
|
|
||||||
"ecdsa-sha2-nistp256",
|
|
||||||
"ecdsa-sha2-nistp384",
|
|
||||||
"ecdsa-sha2-nistp521",
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ValidSSHKeyTypes) != len(expectedTypes) {
|
|
||||||
t.Errorf("ValidSSHKeyTypes length = %d, expected %d", len(ValidSSHKeyTypes), len(expectedTypes))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, expectedType := range expectedTypes {
|
|
||||||
found := false
|
|
||||||
for _, actualType := range ValidSSHKeyTypes {
|
|
||||||
if actualType == expectedType {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("ValidSSHKeyTypes missing expected type: %s", expectedType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestValidateSSHKeyEdgeCases tests edge cases and boundary conditions
|
|
||||||
func TestValidateSSHKeyEdgeCases(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
sshKey string
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "key with only type",
|
|
||||||
sshKey: "ssh-rsa",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key with type and empty data",
|
|
||||||
sshKey: "ssh-rsa ",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key with type and whitespace data",
|
|
||||||
sshKey: "ssh-rsa \t ",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key with multiple spaces between type and data",
|
|
||||||
sshKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDiYUb9Fy2vlPfO+HwubnshimpVrWPoePyvyN+jPC5gWqZSycjMy6Is2vFVn7oQc72bkY0wZalspT5wUOwKtltSoLpL7vcqGL9zHVw4yjYXtPGIRd3zLpU9wdngevnepPQWTX3LvZTZfmOsrGoMDKIG+Lbmiq/STMuWYecIqMp7tUKRGS8vfAmpu6MsrN9/4UTcdWWXYWJQQn+2nCyMz28jYlWRsKtqFK6owrdZWt8WQnPN+9Upcf2ByQje+0NLnpNrnh+yd2ocuVW9wQYKAZXy7IaTfEJwd5m34sLwkqlZTaBBcmWJU+3RfpYXE763cf3rUoPIGQ8eUEBJ8IdM4vhp test@example.com",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key with tabs",
|
|
||||||
sshKey: "\tssh-rsa\tAAAAB3NzaC1yc2EAAAADAQABAAABAQDiYUb9Fy2vlPfO+HwubnshimpVrWPoePyvyN+jPC5gWqZSycjMy6Is2vFVn7oQc72bkY0wZalspT5wUOwKtltSoLpL7vcqGL9zHVw4yjYXtPGIRd3zLpU9wdngevnepPQWTX3LvZTZfmOsrGoMDKIG+Lbmiq/STMuWYecIqMp7tUKRGS8vfAmpu6MsrN9/4UTcdWWXYWJQQn+2nCyMz28jYlWRsKtqFK6owrdZWt8WQnPN+9Upcf2ByQje+0NLnpNrnh+yd2ocuVW9wQYKAZXy7IaTfEJwd5m34sLwkqlZTaBBcmWJU+3RfpYXE763cf3rUoPIGQ8eUEBJ8IdM4vhp test@example.com",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "very long line",
|
|
||||||
sshKey: "ssh-rsa " + string(make([]byte, 10000)),
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
err := ValidateSSHKey(tt.sshKey)
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("ValidateSSHKey() expected error but got none")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("ValidateSSHKey() unexpected error = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
jsonrpc.go
29
jsonrpc.go
|
|
@ -20,7 +20,6 @@ import (
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/hidrpc"
|
"github.com/jetkvm/kvm/internal/hidrpc"
|
||||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
"github.com/jetkvm/kvm/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONRPCRequest struct {
|
type JSONRPCRequest struct {
|
||||||
|
|
@ -433,27 +432,21 @@ func rpcGetSSHKeyState() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetSSHKeyState(sshKey string) error {
|
func rpcSetSSHKeyState(sshKey string) error {
|
||||||
if sshKey == "" {
|
if sshKey != "" {
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if err := os.MkdirAll(sshKeyDir, 0700); err != nil {
|
||||||
|
return fmt.Errorf("failed to create SSH key directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write SSH key to file
|
||||||
|
if err := os.WriteFile(sshKeyFile, []byte(sshKey), 0600); err != nil {
|
||||||
|
return fmt.Errorf("failed to write SSH key: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
// Remove SSH key file if empty string is provided
|
// Remove SSH key file if empty string is provided
|
||||||
if err := os.Remove(sshKeyFile); err != nil && !os.IsNotExist(err) {
|
if err := os.Remove(sshKeyFile); err != nil && !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("failed to remove SSH key file: %w", err)
|
return fmt.Errorf("failed to remove SSH key file: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate SSH key
|
|
||||||
if err := utils.ValidateSSHKey(sshKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
|
||||||
if err := os.MkdirAll(sshKeyDir, 0700); err != nil {
|
|
||||||
return fmt.Errorf("failed to create SSH key directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write SSH key to file
|
|
||||||
if err := os.WriteFile(sshKeyFile, []byte(sshKey), 0600); err != nil {
|
|
||||||
return fmt.Errorf("failed to write SSH key: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -22,39 +22,6 @@ const USBStateMap: Record<USBStates, string> = {
|
||||||
"not attached": "Disconnected",
|
"not attached": "Disconnected",
|
||||||
suspended: "Low power mode",
|
suspended: "Low power mode",
|
||||||
};
|
};
|
||||||
const StatusCardProps: StatusProps = {
|
|
||||||
configured: {
|
|
||||||
icon: ({ className }) => (
|
|
||||||
<img className={cx(className)} src={KeyboardAndMouseConnectedIcon} alt="" />
|
|
||||||
),
|
|
||||||
iconClassName: "h-5 w-5 shrink-0",
|
|
||||||
statusIndicatorClassName: "bg-green-500 border-green-600",
|
|
||||||
},
|
|
||||||
attached: {
|
|
||||||
icon: ({ className }) => <LoadingSpinner className={cx(className)} />,
|
|
||||||
iconClassName: "h-5 w-5 text-blue-500",
|
|
||||||
statusIndicatorClassName: "bg-slate-300 border-slate-400",
|
|
||||||
},
|
|
||||||
addressed: {
|
|
||||||
icon: ({ className }) => <LoadingSpinner className={cx(className)} />,
|
|
||||||
iconClassName: "h-5 w-5 text-blue-500",
|
|
||||||
statusIndicatorClassName: "bg-slate-300 border-slate-400",
|
|
||||||
},
|
|
||||||
"not attached": {
|
|
||||||
icon: ({ className }) => (
|
|
||||||
<img className={cx(className)} src={KeyboardAndMouseConnectedIcon} alt="" />
|
|
||||||
),
|
|
||||||
iconClassName: "h-5 w-5 opacity-50 grayscale filter",
|
|
||||||
statusIndicatorClassName: "bg-slate-300 border-slate-400",
|
|
||||||
},
|
|
||||||
suspended: {
|
|
||||||
icon: ({ className }) => (
|
|
||||||
<img className={cx(className)} src={KeyboardAndMouseConnectedIcon} alt="" />
|
|
||||||
),
|
|
||||||
iconClassName: "h-5 w-5 opacity-50 grayscale filter",
|
|
||||||
statusIndicatorClassName: "bg-green-500 border-green-600",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function USBStateStatus({
|
export default function USBStateStatus({
|
||||||
state,
|
state,
|
||||||
|
|
@ -63,7 +30,39 @@ export default function USBStateStatus({
|
||||||
state: USBStates;
|
state: USBStates;
|
||||||
peerConnectionState?: RTCPeerConnectionState | null;
|
peerConnectionState?: RTCPeerConnectionState | null;
|
||||||
}) {
|
}) {
|
||||||
|
const StatusCardProps: StatusProps = {
|
||||||
|
configured: {
|
||||||
|
icon: ({ className }) => (
|
||||||
|
<img className={cx(className)} src={KeyboardAndMouseConnectedIcon} alt="" />
|
||||||
|
),
|
||||||
|
iconClassName: "h-5 w-5 shrink-0",
|
||||||
|
statusIndicatorClassName: "bg-green-500 border-green-600",
|
||||||
|
},
|
||||||
|
attached: {
|
||||||
|
icon: ({ className }) => <LoadingSpinner className={cx(className)} />,
|
||||||
|
iconClassName: "h-5 w-5 text-blue-500",
|
||||||
|
statusIndicatorClassName: "bg-slate-300 border-slate-400",
|
||||||
|
},
|
||||||
|
addressed: {
|
||||||
|
icon: ({ className }) => <LoadingSpinner className={cx(className)} />,
|
||||||
|
iconClassName: "h-5 w-5 text-blue-500",
|
||||||
|
statusIndicatorClassName: "bg-slate-300 border-slate-400",
|
||||||
|
},
|
||||||
|
"not attached": {
|
||||||
|
icon: ({ className }) => (
|
||||||
|
<img className={cx(className)} src={KeyboardAndMouseConnectedIcon} alt="" />
|
||||||
|
),
|
||||||
|
iconClassName: "h-5 w-5 opacity-50 grayscale filter",
|
||||||
|
statusIndicatorClassName: "bg-slate-300 border-slate-400",
|
||||||
|
},
|
||||||
|
suspended: {
|
||||||
|
icon: ({ className }) => (
|
||||||
|
<img className={cx(className)} src={KeyboardAndMouseConnectedIcon} alt="" />
|
||||||
|
),
|
||||||
|
iconClassName: "h-5 w-5 opacity-50 grayscale filter",
|
||||||
|
statusIndicatorClassName: "bg-green-500 border-green-600",
|
||||||
|
},
|
||||||
|
};
|
||||||
const props = StatusCardProps[state];
|
const props = StatusCardProps[state];
|
||||||
if (!props) {
|
if (!props) {
|
||||||
console.warn("Unsupported USB state: ", state);
|
console.warn("Unsupported USB state: ", state);
|
||||||
|
|
|
||||||
|
|
@ -31,35 +31,20 @@ export default defineConfig(({ mode, command }) => {
|
||||||
esbuild: {
|
esbuild: {
|
||||||
pure: ["console.debug"],
|
pure: ["console.debug"],
|
||||||
},
|
},
|
||||||
build: {
|
build: { outDir: isCloud ? "dist" : "../static" },
|
||||||
outDir: isCloud ? "dist" : "../static",
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
manualChunks: (id) => {
|
|
||||||
if (id.includes("node_modules")) {
|
|
||||||
return "vendor";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
assetFileNames: "assets/immutable/[name]-[hash][extname]",
|
|
||||||
chunkFileNames: "assets/immutable/[name]-[hash].js",
|
|
||||||
entryFileNames: "assets/immutable/[name]-[hash].js",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
https: useSSL,
|
https: useSSL,
|
||||||
proxy: JETKVM_PROXY_URL
|
proxy: JETKVM_PROXY_URL
|
||||||
? {
|
? {
|
||||||
"/me": JETKVM_PROXY_URL,
|
"/me": JETKVM_PROXY_URL,
|
||||||
"/device": JETKVM_PROXY_URL,
|
"/device": JETKVM_PROXY_URL,
|
||||||
"/webrtc": JETKVM_PROXY_URL,
|
"/webrtc": JETKVM_PROXY_URL,
|
||||||
"/auth": JETKVM_PROXY_URL,
|
"/auth": JETKVM_PROXY_URL,
|
||||||
"/storage": JETKVM_PROXY_URL,
|
"/storage": JETKVM_PROXY_URL,
|
||||||
"/cloud": JETKVM_PROXY_URL,
|
"/cloud": JETKVM_PROXY_URL,
|
||||||
"/developer": JETKVM_PROXY_URL,
|
"/developer": JETKVM_PROXY_URL,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
base: onDevice && command === "build" ? "/static" : "/",
|
base: onDevice && command === "build" ? "/static" : "/",
|
||||||
|
|
|
||||||
24
web.go
24
web.go
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -25,7 +24,6 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/vearutop/statigz"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -68,11 +66,6 @@ type SetupRequest struct {
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var cachableFileExtensions = []string{
|
|
||||||
".jpg", ".jpeg", ".png", ".gif", ".webp", ".woff2",
|
|
||||||
".ico",
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupRouter() *gin.Engine {
|
func setupRouter() *gin.Engine {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
gin.DisableConsoleColor()
|
gin.DisableConsoleColor()
|
||||||
|
|
@ -82,36 +75,23 @@ func setupRouter() *gin.Engine {
|
||||||
return *ginLogger
|
return *ginLogger
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
|
|
||||||
staticFS, _ := fs.Sub(staticFiles, "static")
|
staticFS, _ := fs.Sub(staticFiles, "static")
|
||||||
staticFileServer := http.StripPrefix("/static", statigz.FileServer(
|
|
||||||
staticFS.(fs.ReadDirFS),
|
|
||||||
))
|
|
||||||
|
|
||||||
// Add a custom middleware to set cache headers for images
|
// Add a custom middleware to set cache headers for images
|
||||||
// This is crucial for optimizing the initial welcome screen load time
|
// This is crucial for optimizing the initial welcome screen load time
|
||||||
// By enabling caching, we ensure that pre-loaded images are stored in the browser cache
|
// By enabling caching, we ensure that pre-loaded images are stored in the browser cache
|
||||||
// This allows for a smoother enter animation and improved user experience on the welcome screen
|
// This allows for a smoother enter animation and improved user experience on the welcome screen
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
if strings.HasPrefix(c.Request.URL.Path, "/static/assets/immutable/") {
|
|
||||||
c.Header("Cache-Control", "public, max-age=31536000, immutable") // Cache for 1 year
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(c.Request.URL.Path, "/static/") {
|
if strings.HasPrefix(c.Request.URL.Path, "/static/") {
|
||||||
ext := filepath.Ext(c.Request.URL.Path)
|
ext := filepath.Ext(c.Request.URL.Path)
|
||||||
if slices.Contains(cachableFileExtensions, ext) {
|
if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".webp" {
|
||||||
c.Header("Cache-Control", "public, max-age=300") // Cache for 5 minutes
|
c.Header("Cache-Control", "public, max-age=300") // Cache for 5 minutes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Next()
|
c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Any("/static/*w", func(c *gin.Context) {
|
r.StaticFS("/static", http.FS(staticFS))
|
||||||
staticFileServer.ServeHTTP(c.Writer, c.Request)
|
|
||||||
})
|
|
||||||
r.POST("/auth/login-local", handleLogin)
|
r.POST("/auth/login-local", handleLogin)
|
||||||
|
|
||||||
// We use this to determine if the device is setup
|
// We use this to determine if the device is setup
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue