Add Docker support and enhance server configuration

- Created Dockerfile for containerized deployment
- Added .dockerignore to optimize Docker build context
- Updated README with comprehensive Docker setup instructions
- Implemented new server endpoints:
  * Health check endpoint
  * Device listing
  * Device control
  * SSE event subscription
- Enhanced security middleware with request validation and input sanitization
- Added error handling and token-based authentication for new endpoints
This commit is contained in:
jango-blockchained
2025-02-01 04:21:45 +01:00
parent b855b05dca
commit 13773d2977
5 changed files with 244 additions and 25 deletions

View File

@@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
import { sseManager } from './sse/index.js';
import { ILogger } from "@digital-alchemy/core";
import express from 'express';
import { rateLimiter, securityHeaders } from './security/index.js';
import { rateLimiter, securityHeaders, validateRequest, sanitizeInput, errorHandler } from './security/index.js';
// Load environment variables based on NODE_ENV
const envFile = process.env.NODE_ENV === 'production'
@@ -36,10 +36,21 @@ const app = express();
app.use(securityHeaders);
app.use(rateLimiter);
app.use(express.json());
app.use(validateRequest);
app.use(sanitizeInput);
// Initialize LiteMCP
const server = new LiteMCP('home-assistant', '0.1.0');
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
version: '0.1.0'
});
});
// Define Tool interface
interface Tool {
name: string;
@@ -48,23 +59,31 @@ interface Tool {
execute: (params: any) => Promise<any>;
}
// Array to track tools (moved outside main function)
// Array to track tools
const tools: Tool[] = [];
// Create API endpoint for each tool
app.post('/api/:tool', async (req, res) => {
const toolName = req.params.tool;
const tool = tools.find((t: Tool) => t.name === toolName);
if (!tool) {
return res.status(404).json({
success: false,
message: `Tool '${toolName}' not found`
});
}
// List devices endpoint
app.get('/list_devices', async (req, res) => {
try {
const result = await tool.execute(req.body);
// Get token from Authorization header
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token || token !== HASS_TOKEN) {
return res.status(401).json({
success: false,
message: 'Unauthorized - Invalid token'
});
}
const tool = tools.find(t => t.name === 'list_devices');
if (!tool) {
return res.status(404).json({
success: false,
message: 'Tool not found'
});
}
const result = await tool.execute({ token });
res.json(result);
} catch (error) {
res.status(500).json({
@@ -74,6 +93,108 @@ app.post('/api/:tool', async (req, res) => {
}
});
app.post('/control', async (req, res) => {
try {
// Get token from Authorization header
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token || token !== HASS_TOKEN) {
return res.status(401).json({
success: false,
message: 'Unauthorized - Invalid token'
});
}
const tool = tools.find(t => t.name === 'control');
if (!tool) {
return res.status(404).json({
success: false,
message: 'Tool not found'
});
}
const result = await tool.execute({
...req.body,
token
});
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred'
});
}
});
// SSE endpoints
app.get('/subscribe_events', (req, res) => {
try {
// Get token from query parameter
const token = req.query.token?.toString();
if (!token || token !== HASS_TOKEN) {
return res.status(401).json({
success: false,
message: 'Unauthorized - Invalid token'
});
}
const tool = tools.find(t => t.name === 'subscribe_events');
if (!tool) {
return res.status(404).json({
success: false,
message: 'Tool not found'
});
}
tool.execute({
token,
events: req.query.events?.toString().split(','),
entity_id: req.query.entity_id?.toString(),
domain: req.query.domain?.toString(),
response: res
});
} catch (error) {
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred'
});
}
});
app.get('/get_sse_stats', async (req, res) => {
try {
// Get token from query parameter
const token = req.query.token?.toString();
if (!token || token !== HASS_TOKEN) {
return res.status(401).json({
success: false,
message: 'Unauthorized - Invalid token'
});
}
const tool = tools.find(t => t.name === 'get_sse_stats');
if (!tool) {
return res.status(404).json({
success: false,
message: 'Tool not found'
});
}
const result = await tool.execute({ token });
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred'
});
}
});
// Error handling middleware
app.use(errorHandler);
interface CommandParams {
command: string;
entity_id: string;

View File

@@ -127,6 +127,11 @@ export class TokenManager {
// Request validation middleware
export function validateRequest(req: Request, res: Response, next: NextFunction) {
// Skip validation for health endpoint
if (req.path === '/health') {
return next();
}
// Validate content type
if (req.method !== 'GET' && !req.is('application/json')) {
return res.status(415).json({