Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6253afe
Add devcontainer support
tutman96 Dec 31, 2024
7bc6516
Add dev:device script and support for setting JETKVM_PROXY_URL for de…
tutman96 Jan 1, 2025
377c3e8
Implement plugin upload support and placeholder settings item
tutman96 Jan 1, 2025
0a77200
Add extracting and validating the plugin
tutman96 Jan 1, 2025
00fdbaf
Write plugin database to tmp file first
tutman96 Jan 4, 2025
3853b58
Implement pluginList RPC and associated UI
tutman96 Jan 4, 2025
88f3e97
Add enable/disable button
tutman96 Jan 4, 2025
5de7bc7
Add process_manager and subprocess spawning support
tutman96 Jan 4, 2025
2ffb463
Handle "errored" condition instead of "stopped"
tutman96 Jan 4, 2025
5a05719
When tar extraction fails, delete extraction folder
tutman96 Jan 4, 2025
79305da
Fix net Listener interface and implement max process backoff time
tutman96 Jan 5, 2025
5652e8f
Fix bad pointer reference
tutman96 Jan 5, 2025
562f6c4
Add ability to uninstall a plugin
tutman96 Jan 5, 2025
e764000
Golang standards :)
tutman96 Jan 5, 2025
27b3395
Newlines for all things
tutman96 Jan 5, 2025
ce86105
Merge branch 'main' into plugin-system
tutman96 Jan 5, 2025
0b3cd59
Refactor jsonrpc server into its own package
tutman96 Jan 5, 2025
e61decf
wip: Plugin RPC with status reporting to the UI
tutman96 Jan 5, 2025
2428c15
Handle error conditions better and detect support methods automatically
tutman96 Jan 6, 2025
2e24916
Change wording from TODO to coming soon
tutman96 Jan 6, 2025
16064aa
Better handle install and re-install lifecycle. Also display all the …
tutman96 Jan 6, 2025
d1abc4b
Handle messages async to datachannel receive
tutman96 Jan 6, 2025
6fd978b
Rename JSONRPCServer to JSONRPCRouter
tutman96 Jan 19, 2025
ec20835
Fix jsonrpc references
tutman96 Jan 30, 2025
b9c871c
Merge branch 'dev' into plugin-system
tutman96 Feb 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "JetKVM",
"image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm",
"features": {
"ghcr.io/devcontainers/features/node:1": {
// Should match what is defined in ui/package.json
"version": "21.1.0"
}
}
}
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ build_dev: hash_resource
GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w -X kvm.builtAppVersion=$(VERSION_DEV)" -o bin/jetkvm_app cmd/main.go

frontend:
cd ui && npm run build:device
cd ui && npm ci && npm run build:device

dev_release: build_dev
@echo "Uploading release..."
Expand Down
42 changes: 8 additions & 34 deletions dev_deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,28 @@ set -e

# Function to display help message
show_help() {
echo "Usage: $0 [options] -h <host_ip> -r <remote_ip>"
echo "Usage: $0 [options] -r <remote_ip>"
echo
echo "Required:"
echo " -h, --host <host_ip> Local host IP address"
echo " -r, --remote <remote_ip> Remote host IP address"
echo
echo "Optional:"
echo " -u, --user <remote_user> Remote username (default: root)"
echo " -p, --port <port> Python server port (default: 8000)"
echo " --help Display this help message"
echo
echo "Example:"
echo " $0 -h 192.168.0.13 -r 192.168.0.17"
echo " $0 -h 192.168.0.13 -r 192.168.0.17 -u admin -p 8080"
echo " $0 -r 192.168.0.17"
echo " $0 -r 192.168.0.17 -u admin"
exit 0
}

# Default values
PYTHON_PORT=8000
REMOTE_USER="root"
REMOTE_PATH="/userdata/jetkvm/bin"

# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--host)
HOST_IP="$2"
shift 2
;;
-r|--remote)
REMOTE_HOST="$2"
shift 2
Expand All @@ -40,10 +33,6 @@ while [[ $# -gt 0 ]]; do
REMOTE_USER="$2"
shift 2
;;
-p|--port)
PYTHON_PORT="$2"
shift 2
;;
--help)
show_help
exit 0
Expand All @@ -57,8 +46,8 @@ while [[ $# -gt 0 ]]; do
done

# Verify required parameters
if [ -z "$HOST_IP" ] || [ -z "$REMOTE_HOST" ]; then
echo "Error: Host IP and Remote IP are required parameters"
if [ -z "$REMOTE_HOST" ]; then
echo "Error: Remote IP is a required parameter"
show_help
fi

Expand All @@ -69,12 +58,8 @@ make build_dev
# Change directory to the binary output directory
cd bin

# Start a Python HTTP server in the background to serve files
python3 -m http.server "$PYTHON_PORT" &
PYTHON_SERVER_PID=$!

# Ensure that the Python server is terminated if the script exits unexpectedly
trap "echo 'Terminating Python server...'; kill $PYTHON_SERVER_PID" EXIT
# Copy the binary to the remote host
cat jetkvm_app | ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > $REMOTE_PATH/jetkvm_app_debug"

# Deploy and run the application on the remote host
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash <<EOF
Expand All @@ -90,23 +75,12 @@ killall jetkvm_app_debug || true
# Navigate to the directory where the binary will be stored
cd "$REMOTE_PATH"

# Remove any old binary
rm -f jetkvm_app

# Download the new binary from the host
wget ${HOST_IP}:${PYTHON_PORT}/jetkvm_app

# Make the new binary executable
chmod +x jetkvm_app

# Rename the binary to jetkvm_app_debug
mv jetkvm_app jetkvm_app_debug
chmod +x jetkvm_app_debug

# Run the application in the background
./jetkvm_app_debug

EOF

# Once the SSH session finishes, shut down the Python server
kill "$PYTHON_SERVER_PID"
echo "Deployment complete."
60 changes: 60 additions & 0 deletions internal/plugin/database.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package plugin

import (
"encoding/json"
"fmt"
"os"
"sync"
)

const databaseFile = pluginsFolder + "/plugins.json"

type PluginDatabase struct {
// Map with the plugin name as the key
Plugins map[string]*PluginInstall `json:"plugins"`

saveMutex sync.Mutex
}

var pluginDatabase = PluginDatabase{}

func (d *PluginDatabase) Load() error {
file, err := os.Open(databaseFile)
if os.IsNotExist(err) {
d.Plugins = make(map[string]*PluginInstall)
return nil
}
if err != nil {
return fmt.Errorf("failed to open plugin database: %v", err)
}
defer file.Close()

if err := json.NewDecoder(file).Decode(d); err != nil {
return fmt.Errorf("failed to decode plugin database: %v", err)
}

return nil
}

func (d *PluginDatabase) Save() error {
d.saveMutex.Lock()
defer d.saveMutex.Unlock()

file, err := os.Create(databaseFile + ".tmp")
if err != nil {
return fmt.Errorf("failed to create plugin database tmp: %v", err)
}
defer file.Close()

encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
if err := encoder.Encode(d); err != nil {
return fmt.Errorf("failed to encode plugin database: %v", err)
}

if err := os.Rename(databaseFile+".tmp", databaseFile); err != nil {
return fmt.Errorf("failed to move plugin database to active file: %v", err)
}

return nil
}
83 changes: 83 additions & 0 deletions internal/plugin/extract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package plugin

import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"

"github.com/google/uuid"
)

const pluginsExtractsFolder = pluginsFolder + "/extracts"

func init() {
_ = os.MkdirAll(pluginsExtractsFolder, 0755)
}

func extractPlugin(filePath string) (*string, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file for extraction: %v", err)
}
defer file.Close()

var reader io.Reader = file
// TODO: there's probably a better way of doing this without relying on the file extension
if strings.HasSuffix(filePath, ".gz") {
Comment on lines +30 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grabbing the first few bytes of the file and doing:

if bytes.HasPrefix(buf, []byte{0x1F, 0x8B, 0x08}) {

can be a nicer way to handle this.

gzipReader, err := gzip.NewReader(file)
if err != nil {
return nil, fmt.Errorf("failed to create gzip reader: %v", err)
}
defer gzipReader.Close()
reader = gzipReader
}

destinationFolder := path.Join(pluginsExtractsFolder, uuid.New().String())
if err := os.MkdirAll(destinationFolder, 0755); err != nil {
return nil, fmt.Errorf("failed to create extracts folder: %v", err)
}

tarReader := tar.NewReader(reader)

for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("failed to read tar header: %v", err)
}

// Prevent path traversal attacks
targetPath := filepath.Join(destinationFolder, header.Name)
if !strings.HasPrefix(targetPath, filepath.Clean(destinationFolder)+string(os.PathSeparator)) {
return nil, fmt.Errorf("tar file contains illegal path: %s", header.Name)
}

switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
return nil, fmt.Errorf("failed to create directory: %v", err)
}
case tar.TypeReg:
file, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode))
if err != nil {
return nil, fmt.Errorf("failed to create file: %v", err)
}
defer file.Close()

if _, err := io.Copy(file, tarReader); err != nil {
return nil, fmt.Errorf("failed to extract file: %v", err)
}
default:
return nil, fmt.Errorf("unsupported tar entry type: %v", header.Typeflag)
}
}

return &destinationFolder, nil
}
Loading