Skip to content

Commit 71d1952

Browse files
committed
feat: Enhanced security and log management
- Remove redundant 'fopen open' command for cleaner architecture - Strengthen security validation in fopen-handler-simple.js - Add automatic log rotation (1MB limit with backup) - Add manual log cleanup command (fopen clean-logs) - Improve security logging with detailed violation tracking - Update documentation with log management features - Release v1.0.6 with enhanced security and log management
1 parent 372f20e commit 71d1952

File tree

4 files changed

+179
-155
lines changed

4 files changed

+179
-155
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ A powerful command-line interface for opening local files via custom URL scheme
2121
- 🎯 **Config URL Support**: Access configuration via `fileopener://config`
2222
- 🔧 **ES Module Support**: Built with modern JavaScript (ES modules)
2323
- 🧹 **Memory Leak Prevention**: Automatic process cleanup after file operations
24+
- 📝 **Log Management**: Automatic log rotation and manual log cleanup
2425
- 🌐 **Web Integration**: Compatible with [fileopener-redirect-worker](https://github.com/mineclover/fileopener-redirect-worker) for HTTP-to-protocol redirection
2526

2627
## 🚀 Quick Start
@@ -281,6 +282,13 @@ Opens the configuration file in your default editor.
281282
fopen config
282283
```
283284

285+
### `fopen clean-logs`
286+
Cleans up log files to free disk space. Log files are automatically rotated when they exceed 1MB.
287+
288+
```bash
289+
fopen clean-logs
290+
```
291+
284292
### `fopen uninstall`
285293
Unregisters the protocol from your system.
286294

@@ -389,6 +397,34 @@ Allowed project path: /path/to/project
389397
- Validates project aliases exist in configuration
390398
- Ensures resolved paths remain within project boundaries
391399

400+
## 📝 Log Management
401+
402+
The tool automatically manages log files to prevent disk space issues:
403+
404+
### Automatic Log Rotation
405+
- Log files are automatically rotated when they exceed 1MB
406+
- Old logs are backed up as `handler.log.old`
407+
- Maximum total log size is limited to 2MB (current + backup)
408+
409+
### Manual Log Cleanup
410+
```bash
411+
# Clean all log files
412+
fopen clean-logs
413+
```
414+
415+
### Log File Locations
416+
- **macOS/Linux**: `~/.fopen-cli/handler.log`
417+
- **Windows**: `%USERPROFILE%\.fopen-cli\handler.log`
418+
419+
### Log Contents
420+
Logs include:
421+
- URL processing attempts
422+
- Security violations
423+
- File opening results
424+
- Error messages
425+
426+
This ensures the tool doesn't consume excessive disk space while maintaining useful debugging information.
427+
392428
### Error Handling
393429
Comprehensive error messages for various scenarios:
394430
- Non-existent files or projects

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@context-action/fopen-cli",
3-
"version": "1.0.5",
3+
"version": "1.0.6",
44
"packageManager": "pnpm@10.14.0",
55
"type": "module",
66
"license": "MIT",

src/bin-simple.js

Lines changed: 82 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function saveConfig(config) {
3939

4040
// Commands
4141
async function install() {
42-
console.log("Installing file opener protocol...")
42+
logInfo("Installing file opener protocol...")
4343
ensureConfigDir()
4444

4545
// Register protocol using protocol-registry
@@ -48,17 +48,24 @@ async function install() {
4848
const handlerPath = path.join(__dirname, "bin", "fopen-handler-simple.cjs")
4949
const command = `node "${handlerPath}" "$_URL_"`
5050

51-
console.log(`Registering protocol: "fileopener"`)
52-
console.log(`With command: "${command}"`)
51+
logInfo("Registering protocol", {
52+
"Protocol": "fileopener",
53+
"Command": command,
54+
"Handler path": handlerPath
55+
})
5356

5457
await protocolRegistry.register("fileopener", command, {
5558
override: true,
5659
terminal: false
5760
})
58-
console.log('\nProtocol registration successful!')
59-
console.log("Configuration directory: " + CONFIG_DIR)
61+
logInfo("Protocol registration successful", {
62+
"Configuration directory": CONFIG_DIR
63+
})
6064
} catch (error) {
61-
console.error('\nError during protocol registration:', error)
65+
logError("Error during protocol registration", {
66+
"Error": error.message,
67+
"Stack": error.stack
68+
})
6269
}
6370
}
6471

@@ -129,6 +136,29 @@ function uninstall() {
129136
}
130137
}
131138

139+
function cleanLogs() {
140+
const LOG_FILE = path.join(CONFIG_DIR, "handler.log")
141+
const BACKUP_LOG_FILE = LOG_FILE + ".old"
142+
143+
let cleaned = false
144+
145+
if (fs.existsSync(LOG_FILE)) {
146+
fs.unlinkSync(LOG_FILE)
147+
console.log("Current log file removed")
148+
cleaned = true
149+
}
150+
151+
if (fs.existsSync(BACKUP_LOG_FILE)) {
152+
fs.unlinkSync(BACKUP_LOG_FILE)
153+
console.log("Backup log file removed")
154+
cleaned = true
155+
}
156+
157+
if (!cleaned) {
158+
console.log("No log files found to clean")
159+
}
160+
}
161+
132162
// File opening functionality
133163
function openFile(filePath) {
134164
const platform = process.platform
@@ -180,146 +210,55 @@ function openFile(filePath) {
180210
}, 5000) // 5 second timeout
181211
}
182212

183-
// Security validation functions
184-
function validateFilePath(filePath, projectPath) {
185-
// Check for path traversal attempts
186-
if (filePath.includes('..') || filePath.includes('~')) {
187-
console.log("Security violation: Path traversal attempt detected")
188-
return false
213+
// Logging functions
214+
function logInfo(message, details = {}) {
215+
const timestamp = new Date().toISOString()
216+
console.log(`[${timestamp}] INFO: ${message}`)
217+
218+
for (const [key, value] of Object.entries(details)) {
219+
console.log(`[${timestamp}] ${key}: "${value}"`)
189220
}
221+
}
190222

191-
// Check for absolute paths (should be relative to project)
192-
if (path.isAbsolute(filePath)) {
193-
console.log("Security violation: Absolute path not allowed")
194-
return false
223+
function logWarning(message, details = {}) {
224+
const timestamp = new Date().toISOString()
225+
console.log(`[${timestamp}] WARNING: ${message}`)
226+
227+
for (const [key, value] of Object.entries(details)) {
228+
console.log(`[${timestamp}] ${key}: "${value}"`)
195229
}
230+
}
196231

197-
// Normalize the path to prevent various bypass attempts
198-
const normalizedPath = path.normalize(filePath)
232+
function logError(message, details = {}) {
233+
const timestamp = new Date().toISOString()
234+
console.log(`[${timestamp}] ERROR: ${message}`)
199235

200-
// Check for any remaining traversal attempts after normalization
201-
if (normalizedPath.includes('..')) {
202-
console.log("Security violation: Path traversal detected after normalization")
203-
return false
236+
for (const [key, value] of Object.entries(details)) {
237+
console.log(`[${timestamp}] ${key}: "${value}"`)
204238
}
239+
}
205240

206-
// Resolve full file path
207-
const fullPath = path.resolve(path.join(projectPath, normalizedPath))
208-
const normalizedProjectPath = path.resolve(projectPath)
209-
210-
// Ensure the resolved path is within the project directory
211-
if (!fullPath.startsWith(normalizedProjectPath)) {
212-
console.log("Security violation: Path outside project directory")
213-
return false
241+
function logSecurityViolation(violationType, details) {
242+
const timestamp = new Date().toISOString()
243+
console.log(`[${timestamp}] SECURITY VIOLATION: ${violationType}`)
244+
245+
for (const [key, value] of Object.entries(details)) {
246+
console.log(`[${timestamp}] ${key}: "${value}"`)
214247
}
248+
}
215249

216-
// Check for symbolic links that might escape the project directory
217-
try {
218-
const realPath = fs.realpathSync(fullPath)
219-
if (!realPath.startsWith(normalizedProjectPath)) {
220-
console.log("Security violation: Symbolic link escapes project directory")
221-
return false
222-
}
223-
} catch (error) {
224-
// If realpathSync fails, the file might not exist yet, but we'll check later
225-
// This is not a security violation by itself
250+
function logSecurityAttempt(attemptType, filePath, projectPath, additionalInfo = {}) {
251+
const timestamp = new Date().toISOString()
252+
console.log(`[${timestamp}] SECURITY VIOLATION: ${attemptType}`)
253+
console.log(`[${timestamp}] Attempted path: "${filePath}"`)
254+
console.log(`[${timestamp}] Project path: "${projectPath}"`)
255+
256+
for (const [key, value] of Object.entries(additionalInfo)) {
257+
console.log(`[${timestamp}] ${key}: "${value}"`)
226258
}
227-
228-
return true
229259
}
230260

231-
// Parse URL and open file
232-
function handleUrl(url) {
233-
console.log(`Processing URL: ${url}`)
234261

235-
try {
236-
const parsedUrl = new URL(url)
237-
238-
if (parsedUrl.protocol !== "fileopener:") {
239-
console.log(`Invalid protocol: ${parsedUrl.protocol}. Expected 'fileopener:'`)
240-
return
241-
}
242-
243-
const project = parsedUrl.hostname
244-
if (!project) {
245-
console.log("Project name is required in URL")
246-
return
247-
}
248-
249-
// Handle special case for config
250-
if (project === 'config' && !parsedUrl.pathname.replace(/^\/+/, '')) {
251-
console.log(`Opening config file: ${CONFIG_FILE}`)
252-
openFile(CONFIG_FILE)
253-
return
254-
}
255-
256-
let filePath
257-
// Check for legacy query parameter format
258-
const queryPath = parsedUrl.searchParams.get("path")
259-
if (queryPath) {
260-
// Legacy format: fileopener://project?path=file/path
261-
filePath = decodeURIComponent(queryPath)
262-
} else {
263-
// Modern format: fileopener://project/file/path
264-
filePath = parsedUrl.pathname.slice(1) // Remove leading slash
265-
if (!filePath) {
266-
console.log("File path is required in URL")
267-
return
268-
}
269-
filePath = decodeURIComponent(filePath)
270-
}
271-
272-
console.log(`Project: ${project}, File: ${filePath}`)
273-
274-
// Get project path from config (whitelist check)
275-
const config = getConfig()
276-
const projectPath = config.projects[project]
277-
278-
if (!projectPath) {
279-
console.log(`Project '${project}' not found in configuration`)
280-
console.log("Available projects:")
281-
for (const [name, path] of Object.entries(config.projects)) {
282-
console.log(` ${name} -> ${path}`)
283-
}
284-
return
285-
}
286-
287-
// Security validation: whitelist-based path checking
288-
if (!validateFilePath(filePath, projectPath)) {
289-
console.log("Access denied: Security policy violation")
290-
console.log(`Attempted access to: ${filePath}`)
291-
console.log(`Allowed project path: ${projectPath}`)
292-
return
293-
}
294-
295-
// Resolve full file path (after validation)
296-
const fullPath = path.resolve(path.join(projectPath, path.normalize(filePath)))
297-
298-
// Final security check: ensure the resolved path is within the project directory
299-
const normalizedProjectPath = path.resolve(projectPath)
300-
if (!fullPath.startsWith(normalizedProjectPath)) {
301-
console.log("Security violation: Final path validation failed")
302-
return
303-
}
304-
305-
// Check if file exists
306-
if (!fs.existsSync(fullPath)) {
307-
console.log(`File not found: ${fullPath}`)
308-
return
309-
}
310-
311-
// Open the file
312-
openFile(fullPath)
313-
314-
// Exit the process after opening the file
315-
setTimeout(() => {
316-
process.exit(0)
317-
}, 100) // Small delay to ensure file opening message is displayed
318-
} catch (error) {
319-
console.log(`Failed to parse URL: ${error.message}`)
320-
process.exit(1)
321-
}
322-
}
323262

324263
// Open config file
325264
function openConfig() {
@@ -357,24 +296,19 @@ switch (command) {
357296
case "uninstall":
358297
uninstall()
359298
break
360-
case "open":
361-
if (!args[1]) {
362-
console.log("Missing required URL")
363-
console.log("Usage: fopen open <fileopener://url>")
364-
} else {
365-
handleUrl(args[1])
366-
}
367-
break
368299
case "config":
369300
openConfig()
370301
break
302+
case "clean-logs":
303+
cleanLogs()
304+
break
371305
default:
372306
console.log("File opener CLI - Use one of the subcommands:")
373-
console.log(" install - Register the fileopener:// protocol")
374-
console.log(" add - Add a project alias")
375-
console.log(" list - List all configured projects")
376-
console.log(" remove - Remove a project alias")
377-
console.log(" uninstall - Unregister the protocol")
378-
console.log(" open - Open a file using fileopener:// URL")
379-
console.log(" config - Open the configuration file")
307+
console.log(" install - Register the fileopener:// protocol")
308+
console.log(" add - Add a project alias")
309+
console.log(" list - List all configured projects")
310+
console.log(" remove - Remove a project alias")
311+
console.log(" uninstall - Unregister the protocol")
312+
console.log(" config - Open the configuration file")
313+
console.log(" clean-logs - Clean log files")
380314
}

0 commit comments

Comments
 (0)