mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-28 17:11:20 +02:00
feat(api): implement API key generation and management functionality
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
10
config.go
10
config.go
@@ -3,6 +3,8 @@ package kvm
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -497,6 +499,14 @@ func SaveConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateAPIKey() (string, error) {
|
||||||
|
bytes := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
func ensureConfigLoaded() {
|
func ensureConfigLoaded() {
|
||||||
if config == nil {
|
if config == nil {
|
||||||
LoadConfig()
|
LoadConfig()
|
||||||
|
|||||||
97
jsonrpc.go
97
jsonrpc.go
@@ -79,15 +79,12 @@ func writeJSONRPCEvent(event string, params interface{}, session *Session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestString := string(requestBytes)
|
requestString := string(requestBytes)
|
||||||
scopedLogger := jsonRpcLogger.With().
|
|
||||||
Str("data", requestString).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
scopedLogger.Info().Msg("sending JSONRPC event")
|
jsonRpcLogger.Trace().Str("event", event).Msg("sending JSONRPC event")
|
||||||
|
|
||||||
err = session.RPCChannel.SendText(requestString)
|
err = session.RPCChannel.SendText(requestString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("error sending JSONRPC event")
|
jsonRpcLogger.Warn().Err(err).Str("event", event).Msg("error sending JSONRPC event")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,6 +494,36 @@ func rpcGetUpdateStatus() (*UpdateStatus, error) {
|
|||||||
return updateStatus, nil
|
return updateStatus, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelfSignatureStatus struct {
|
||||||
|
AppSignatureAbsent bool `json:"appSignatureAbsent,omitempty"`
|
||||||
|
AppSignatureInvalid bool `json:"appSignatureInvalid,omitempty"`
|
||||||
|
AppNoPublicKey bool `json:"appNoPublicKey,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetSelfSignatureStatus() (*SelfSignatureStatus, error) {
|
||||||
|
return getSelfSignatureStatus(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSelfSignatureStatus() *SelfSignatureStatus {
|
||||||
|
status := &SelfSignatureStatus{}
|
||||||
|
publicKey := getOTAPublicKey()
|
||||||
|
|
||||||
|
appBinPath := "/userdata/picokvm/bin/kvm_app"
|
||||||
|
appSigPath := appBinPath + ".sig"
|
||||||
|
|
||||||
|
status.AppSignatureAbsent = isSigFileAbsent(appSigPath)
|
||||||
|
|
||||||
|
if !status.AppSignatureAbsent {
|
||||||
|
if publicKey == nil {
|
||||||
|
status.AppNoPublicKey = true
|
||||||
|
} else {
|
||||||
|
status.AppSignatureInvalid = !verifyLocalFileSignature(appBinPath, appSigPath, publicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
func rpcTryUpdate() error {
|
func rpcTryUpdate() error {
|
||||||
includePreRelease := config.IncludePreRelease
|
includePreRelease := config.IncludePreRelease
|
||||||
go func() {
|
go func() {
|
||||||
@@ -1298,6 +1325,30 @@ func rpcSetLocalLoopbackOnly(enabled bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcGetApiKey() (string, error) {
|
||||||
|
return config.APIKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetApiKey(apiKey string) error {
|
||||||
|
config.APIKey = apiKey
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGenerateApiKey() (string, error) {
|
||||||
|
key, err := generateAPIKey()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate API key: %w", err)
|
||||||
|
}
|
||||||
|
config.APIKey = key
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
type IOSettings struct {
|
type IOSettings struct {
|
||||||
IO0Status bool `json:"io0Status"`
|
IO0Status bool `json:"io0Status"`
|
||||||
IO1Status bool `json:"io1Status"`
|
IO1Status bool `json:"io1Status"`
|
||||||
@@ -1530,25 +1581,41 @@ func captureScreenshot(format string) ([]byte, error) {
|
|||||||
|
|
||||||
os.Remove(jpegScreenshotPath)
|
os.Remove(jpegScreenshotPath)
|
||||||
|
|
||||||
resp, err := CallCtrlAction("jpeg_take_snapshot", nil)
|
// drain any stale signal before triggering
|
||||||
|
select {
|
||||||
|
case <-jpegReadyCh:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := CallCtrlAction("jpeg_take_snapshot", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("jpeg_take_snapshot failed")
|
logger.Error().Err(err).Msg("jpeg_take_snapshot failed")
|
||||||
return nil, fmt.Errorf("failed to trigger JPEG capture: %w", err)
|
return nil, fmt.Errorf("failed to trigger JPEG capture: %w", err)
|
||||||
}
|
}
|
||||||
logger.Info().Interface("response", resp).Msg("jpeg_take_snapshot response")
|
|
||||||
|
|
||||||
// Poll for file with timeout
|
// wait for jpeg_ready event from native, fall back to polling on timeout
|
||||||
maxAttempts := 10
|
timeout := time.NewTimer(2 * time.Second)
|
||||||
for i := 0; i < maxAttempts; i++ {
|
defer timeout.Stop()
|
||||||
|
select {
|
||||||
|
case <-jpegReadyCh:
|
||||||
|
case <-timeout.C:
|
||||||
|
logger.Warn().Msg("jpeg_ready event not received within 2s, falling back to polling")
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
if data, err := os.ReadFile(jpegScreenshotPath); err == nil && len(data) > 0 {
|
if data, err := os.ReadFile(jpegScreenshotPath); err == nil && len(data) > 0 {
|
||||||
logger.Info().Int("size", len(data)).Int("attempts", i+1).Msg("JPEG captured successfully")
|
logger.Info().Int("size", len(data)).Msg("JPEG captured (fallback polling)")
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(200 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Error().Str("path", jpegScreenshotPath).Msg("JPEG file not found after timeout")
|
|
||||||
return nil, fmt.Errorf("JPEG file not found at %s", jpegScreenshotPath)
|
return nil, fmt.Errorf("JPEG file not found at %s", jpegScreenshotPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(jpegScreenshotPath)
|
||||||
|
if err != nil || len(data) == 0 {
|
||||||
|
return nil, fmt.Errorf("JPEG file not readable after jpeg_ready event: %w", err)
|
||||||
|
}
|
||||||
|
logger.Info().Int("size", len(data)).Msg("JPEG captured successfully")
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var rpcHandlers = map[string]RPCHandler{
|
var rpcHandlers = map[string]RPCHandler{
|
||||||
@@ -1588,6 +1655,7 @@ var rpcHandlers = map[string]RPCHandler{
|
|||||||
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
|
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
|
||||||
"getLocalUpdateStatus": {Func: rpcGetLocalUpdateStatus},
|
"getLocalUpdateStatus": {Func: rpcGetLocalUpdateStatus},
|
||||||
"getUpdateStatus": {Func: rpcGetUpdateStatus},
|
"getUpdateStatus": {Func: rpcGetUpdateStatus},
|
||||||
|
"getSelfSignatureStatus": {Func: rpcGetSelfSignatureStatus},
|
||||||
"tryUpdate": {Func: rpcTryUpdate},
|
"tryUpdate": {Func: rpcTryUpdate},
|
||||||
"getCustomUpdateBaseURL": {Func: rpcGetCustomUpdateBaseURL},
|
"getCustomUpdateBaseURL": {Func: rpcGetCustomUpdateBaseURL},
|
||||||
"setCustomUpdateBaseURL": {Func: rpcSetCustomUpdateBaseURL, Params: []string{"baseURL"}},
|
"setCustomUpdateBaseURL": {Func: rpcSetCustomUpdateBaseURL, Params: []string{"baseURL"}},
|
||||||
@@ -1596,6 +1664,9 @@ var rpcHandlers = map[string]RPCHandler{
|
|||||||
"getDevModeState": {Func: rpcGetDevModeState},
|
"getDevModeState": {Func: rpcGetDevModeState},
|
||||||
"getSSHKeyState": {Func: rpcGetSSHKeyState},
|
"getSSHKeyState": {Func: rpcGetSSHKeyState},
|
||||||
"setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}},
|
"setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}},
|
||||||
|
"getApiKey": {Func: rpcGetApiKey},
|
||||||
|
"setApiKey": {Func: rpcSetApiKey, Params: []string{"apiKey"}},
|
||||||
|
"generateApiKey": {Func: rpcGenerateApiKey},
|
||||||
"getTLSState": {Func: rpcGetTLSState},
|
"getTLSState": {Func: rpcGetTLSState},
|
||||||
"setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}},
|
"setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}},
|
||||||
"setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}},
|
"setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}},
|
||||||
|
|||||||
14
main.go
14
main.go
@@ -18,6 +18,20 @@ func Main() {
|
|||||||
SyncConfigSD(true)
|
SyncConfigSD(true)
|
||||||
LoadConfig()
|
LoadConfig()
|
||||||
|
|
||||||
|
if config.APIKey == "" {
|
||||||
|
key, err := generateAPIKey()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to generate API key")
|
||||||
|
} else {
|
||||||
|
config.APIKey = key
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to save API key to config")
|
||||||
|
} else {
|
||||||
|
logger.Info().Msg("generated new API key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
appCtx, cancel = context.WithCancel(context.Background())
|
appCtx, cancel = context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
7
mcp.go
7
mcp.go
@@ -26,9 +26,12 @@ func StartMCP(port int, stdio bool) {
|
|||||||
// SSE mode
|
// SSE mode
|
||||||
addr := fmt.Sprintf(":%d", port)
|
addr := fmt.Sprintf(":%d", port)
|
||||||
sseServer := server.NewSSEServer(s)
|
sseServer := server.NewSSEServer(s)
|
||||||
handler := sseServer.SSEHandler()
|
|
||||||
|
|
||||||
// Add auth for non-localhost
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/sse", sseServer.SSEHandler())
|
||||||
|
mux.Handle("/message", sseServer.MessageHandler())
|
||||||
|
|
||||||
|
var handler http.Handler = mux
|
||||||
if config.APIKey != "" {
|
if config.APIKey != "" {
|
||||||
handler = withAPIKeyAuth(handler, config.APIKey)
|
handler = withAPIKeyAuth(handler, config.APIKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export default function SettingsAdvanced() {
|
|||||||
const [configContent, setConfigContent] = useState("");
|
const [configContent, setConfigContent] = useState("");
|
||||||
const [isSavingConfig, setIsSavingConfig] = useState(false);
|
const [isSavingConfig, setIsSavingConfig] = useState(false);
|
||||||
const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false);
|
const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false);
|
||||||
|
const [apiKey, setApiKey] = useState<string>("");
|
||||||
|
const [apiKeyInput, setApiKeyInput] = useState<string>("");
|
||||||
|
const [showApiKeyClearWarning, setShowApiKeyClearWarning] = useState(false);
|
||||||
|
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
const isReinitializingGadget = useHidStore(state => state.isReinitializingGadget);
|
const isReinitializingGadget = useHidStore(state => state.isReinitializingGadget);
|
||||||
@@ -53,6 +56,13 @@ export default function SettingsAdvanced() {
|
|||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
setLocalLoopbackOnly(resp.result as boolean);
|
setLocalLoopbackOnly(resp.result as boolean);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
send("getApiKey", {}, resp => {
|
||||||
|
if ("error" in resp) return;
|
||||||
|
const key = resp.result as string;
|
||||||
|
setApiKey(key);
|
||||||
|
setApiKeyInput(key);
|
||||||
|
});
|
||||||
}, [send, setDeveloperMode]);
|
}, [send, setDeveloperMode]);
|
||||||
|
|
||||||
const getUsbEmulationState = useCallback(() => {
|
const getUsbEmulationState = useCallback(() => {
|
||||||
@@ -108,6 +118,54 @@ export default function SettingsAdvanced() {
|
|||||||
});
|
});
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
|
||||||
|
const handleUpdateApiKey = useCallback(() => {
|
||||||
|
if (apiKeyInput === "") {
|
||||||
|
setShowApiKeyClearWarning(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send("setApiKey", { apiKey: apiKeyInput }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to update API key: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setApiKey(apiKeyInput);
|
||||||
|
notifications.success("API key updated successfully");
|
||||||
|
});
|
||||||
|
}, [send, apiKeyInput]);
|
||||||
|
|
||||||
|
const handleGenerateApiKey = useCallback(() => {
|
||||||
|
send("generateApiKey", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to generate API key: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newKey = resp.result as string;
|
||||||
|
setApiKey(newKey);
|
||||||
|
setApiKeyInput(newKey);
|
||||||
|
notifications.success("New API key generated and saved");
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const confirmClearApiKey = useCallback(() => {
|
||||||
|
send("generateApiKey", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to generate API key: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newKey = resp.result as string;
|
||||||
|
setApiKey(newKey);
|
||||||
|
setApiKeyInput(newKey);
|
||||||
|
notifications.success("New API key generated and saved");
|
||||||
|
});
|
||||||
|
setShowApiKeyClearWarning(false);
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
const handleUpdateSSHKey = useCallback(() => {
|
const handleUpdateSSHKey = useCallback(() => {
|
||||||
send("setSSHKeyState", { sshKey }, resp => {
|
send("setSSHKeyState", { sshKey }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
@@ -240,6 +298,42 @@ export default function SettingsAdvanced() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isOnDevice && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title={$at("API Key")}
|
||||||
|
description={$at("API key for MCP and REST API authentication")}
|
||||||
|
/>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<TextAreaWithLabel
|
||||||
|
label={$at("API Key")}
|
||||||
|
value={apiKeyInput || ""}
|
||||||
|
rows={2}
|
||||||
|
onChange={e => setApiKeyInput(e.target.value)}
|
||||||
|
placeholder={$at("Enter API key or leave empty to auto-generate")}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-slate-600 dark:text-[#ffffff]">
|
||||||
|
{$at("Used for authenticating MCP and REST API requests.")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<AntdButton
|
||||||
|
type="primary"
|
||||||
|
onClick={handleUpdateApiKey}
|
||||||
|
className={isMobile?"w-full":""}
|
||||||
|
>
|
||||||
|
{$at("Save API Key")}
|
||||||
|
</AntdButton>
|
||||||
|
<AntdButton
|
||||||
|
onClick={handleGenerateApiKey}
|
||||||
|
className={isMobile?"w-full":""}
|
||||||
|
>
|
||||||
|
{$at("Generate New")}
|
||||||
|
</AntdButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title={$at("Force HTTP Transmission")}
|
title={$at("Force HTTP Transmission")}
|
||||||
badge="Experimental"
|
badge="Experimental"
|
||||||
@@ -379,6 +473,28 @@ export default function SettingsAdvanced() {
|
|||||||
onConfirm={confirmLoopbackModeEnable}
|
onConfirm={confirmLoopbackModeEnable}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={showApiKeyClearWarning}
|
||||||
|
onClose={() => {
|
||||||
|
setShowApiKeyClearWarning(false);
|
||||||
|
}}
|
||||||
|
title={$at("Clear API Key?")}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{$at("Setting the API key to empty will auto-generate a new random key.")}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-600 dark:text-slate-400 mt-2">
|
||||||
|
{$at("Make sure to update your clients with the new key after saving.")}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
variant="warning"
|
||||||
|
cancelText={$at("Cancel")}
|
||||||
|
confirmText={$at("Generate New Key")}
|
||||||
|
onConfirm={confirmClearApiKey}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={showRebootConfirm}
|
open={showRebootConfirm}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user