feat(api): add secure command execution API and HTTP client fallback

- Introduce /api/command/exec endpoint in webui-server.sh for executing a limited set of safe shell commands with JSON response.
- Implement security checks to restrict allowed commands and return appropriate HTTP status codes.
- Add HTTPClient in webroot/js/api.js to communicate with backend API, including execCommand method.
- Enhance ksu mock object to try HTTPClient execCommand first, then fallback to mock implementation.
- Add accessibility improvements in CSS with skip-link styles.

This enables secure remote command execution via the web UI and improves development mode API simulation.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
This commit is contained in:
2025-07-21 23:39:12 +00:00
parent 34d154acca
commit 8071a21d71
3 changed files with 144 additions and 3 deletions

View File

@@ -157,6 +157,42 @@ get_settings() {
echo "}"
}
exec_command_api() {
# Read POST body and extract command
local command=""
if [ -n "$REQUEST_BODY" ]; then
# Simple JSON parsing - extract command value
command=$(echo "$REQUEST_BODY" | sed -n 's/.*"command":\s*"\([^"]*\)".*/\1/p')
fi
if [ -z "$command" ]; then
echo "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\n\r\n{\"error\":\"Missing command parameter\"}"
return
fi
# Security check - only allow safe commands
case "$command" in
"getprop "*|"cat /proc/uptime"|"uname -r"|"su -v"|"id"|"whoami"|"date"|"uptime")
# Execute safe command and capture output
local output
output=$(eval "$command" 2>&1)
local exit_code=$?
# Return JSON response
echo "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{"
echo "\"status\":\"success\","
echo "\"output\":\"$(echo "$output" | sed 's/\\/\\\\/g; s/"/\\"/g')\","
echo "\"exit_code\":$exit_code"
echo "}"
;;
*)
echo "HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\n\r\n{\"error\":\"Command not allowed for security reasons\"}"
;;
esac
log_message "Command execution API called: $command"
}
# Handle API requests
handle_api_request() {
REQUEST="$1"
@@ -183,6 +219,9 @@ handle_api_request() {
"/api/settings")
get_settings
;;
"/api/command/exec")
exec_command_api
;;
*)
echo "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\n\r\n{\"error\":\"Endpoint not found\"}"
;;

View File

@@ -350,6 +350,25 @@ body {
font-weight: var(--md-sys-typescale-label-small-font-weight);
}
/* ===== ACCESSIBILITY ===== */
.skip-link {
position: absolute;
top: -100px;
left: 8px;
z-index: 1000;
padding: 8px 16px;
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
text-decoration: none;
border-radius: var(--md-sys-shape-corner-small);
font-weight: var(--md-sys-typescale-label-medium-font-weight);
transition: top var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}
.skip-link:focus {
top: 8px;
}
/* ===== LAYOUT ===== */
.app {
display: flex;
@@ -689,7 +708,7 @@ body {
gap: 8px;
padding: 10px 24px;
border: none;
border-radius: var(--md-sys-shape-corner-full);
border-radius: var(--md-sys-shape-corner-large);
cursor: pointer;
font-size: var(--md-sys-typescale-label-large-font-size);
font-weight: var(--md-sys-typescale-label-large-font-weight);

View File

@@ -1004,9 +1004,92 @@ document.addEventListener('DOMContentLoaded', () => {
});
} else {
console.warn('WebUIX environment not detected. Running in development/fallback mode.');
// Define mock ksu object for development
// HTTP API Client for backend communication
const HTTPClient = {
baseURL: `${window.location.protocol}//${window.location.host}`,
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
...options
};
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.warn(`HTTP request failed for ${endpoint}:`, error.message);
throw error;
}
},
async execCommand(cmd) {
try {
const response = await this.request('/api/command/exec', {
method: 'POST',
body: JSON.stringify({ command: cmd })
});
return response.output || '';
} catch (error) {
console.warn(`Command execution via HTTP failed: ${cmd}`, error.message);
return `Error: ${error.message}`;
}
},
async getSystemInfo() {
try {
return await this.request('/api/system/info');
} catch (error) {
console.warn('Failed to get system info via HTTP:', error.message);
return null;
}
},
async listBackups() {
try {
return await this.request('/api/backups/list');
} catch (error) {
console.warn('Failed to list backups via HTTP:', error.message);
return { backups: [] };
}
},
async createBackup(options) {
try {
return await this.request('/api/backups/create', {
method: 'POST',
body: JSON.stringify(options)
});
} catch (error) {
console.warn('Failed to create backup via HTTP:', error.message);
throw error;
}
}
};
// Enhanced ksu object that tries HTTP first, then falls back to mock
window.ksu = {
exec: (cmd) => {
exec: async (cmd) => {
try {
// Try HTTP client first
const result = await HTTPClient.execCommand(cmd);
if (!result.startsWith('Error:')) {
return result;
}
} catch (error) {
console.warn('HTTP execution failed, using mock:', error.message);
}
// Fallback to mock
console.log('Mock exec:', cmd);
return `Mock output for: ${cmd}`;
},