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:
@@ -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\"}"
|
||||
;;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user