Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions component/timelapse.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def __init__(self, confighelper: ConfigHelper) -> None:
'enabled': True,
'mode': "layermacro",
'camera': "",
'gphoto2_camera_model': "",
'snapshoturl': "http://localhost:8080/?action=snapshot",
'stream_delay_compensation': 0.05,
'gcode_verbose': False,
Expand Down Expand Up @@ -464,25 +465,48 @@ async def newframe(self) -> None:
# make sure webcamconfig is uptodate before grabbing a new frame
await self.getWebcamConfig()

options = ""
if self.wget_skip_cert:
options += "--no-check-certificate "

self.framecount += 1
framefile = "frame" + str(self.framecount).zfill(6) + ".jpg"
cmd = "wget " + options + self.config['snapshoturl'] \
+ " -O " + self.temp_dir + framefile
self.lastframefile = framefile
logging.debug(f"cmd: {cmd}")

shell_cmd: SCMDComp = self.server.lookup_component('shell_command')
scmd = shell_cmd.build_shell_command(cmd, None)
try:
cmdstatus = await scmd.run(timeout=2., verbose=False)
except Exception:
logging.exception(f"Error running cmd '{cmd}'")

result = {'action': 'newframe'}
cmdstatus = False

# Route to appropriate camera handler
if self.config['gphoto2_camera_model'] != "":
framepath = os.path.join(self.temp_dir, framefile)
# Build gphoto2 command
# --camera selects which camera to use needs model from gphoto2 --auto-detect
# --capture-image-and-download folder/file captures and downloads in one operation
cmd = f"gphoto2 --camera '{self.config['gphoto2_camera_model']}' --capture-image-and-download --filename '{framepath}'"
logging.debug(f"Executing gphoto2 command: {cmd}")

shell_cmd: SCMDComp = self.server.lookup_component('shell_command')
scmd = shell_cmd.build_shell_command(cmd, None)

try:
cmdstatus = await scmd.run(timeout=5., verbose=False)
except Exception:
logging.exception(f"Error executing gphoto2 command: {cmd}")

else:
# Default webcam capture via wget
options = ""
if self.wget_skip_cert:
options += "--no-check-certificate "

cmd = "wget " + options + self.config['snapshoturl'] \
+ " -O " + self.temp_dir + framefile
logging.debug(f"cmd: {cmd}")

shell_cmd: SCMDComp = self.server.lookup_component('shell_command')
scmd = shell_cmd.build_shell_command(cmd, None)
try:
cmdstatus = await scmd.run(timeout=2., verbose=False)
Comment on lines +475 to +505

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Prevent shell command injection in capture command construction.

Line 481 and Lines 498-499 build shell commands from configurable values and execute them. Because gphoto2_camera_model and snapshoturl are writable settings, this creates an RCE path via shell injection.

Suggested hardening
@@
 import re
+import shlex
@@
-            cmd = f"gphoto2 --camera '{self.config['gphoto2_camera_model']}' --capture-image-and-download --filename '{framepath}'"
+            model = shlex.quote(self.config['gphoto2_camera_model'])
+            output_file = shlex.quote(framepath)
+            cmd = (
+                "gphoto2 "
+                f"--camera {model} "
+                "--capture-image-and-download "
+                f"--filename {output_file}"
+            )
@@
-            cmd = "wget " + options + self.config['snapshoturl'] \
-                  + " -O " + self.temp_dir + framefile
+            snapshot_url = shlex.quote(self.config['snapshoturl'])
+            output_file = shlex.quote(os.path.join(self.temp_dir, framefile))
+            cmd = f"wget {options}{snapshot_url} -O {output_file}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@component/timelapse.py` around lines 475 - 505, The gphoto2 and wget command
strings are built from writable settings (self.config['gphoto2_camera_model'],
self.config['snapshoturl']) and passed to shell_command.build_shell_command,
creating a shell injection/RCE risk; update the capture logic in
component/timelapse.py to pass arguments safely instead of interpolating raw
strings: for gphoto2 use an argv-style invocation or properly quote the camera
model (e.g., use a list of args or shlex.quote) when constructing the command,
and for the snapshot use a safe download API or pass the URL as a single
argument (or quoted) rather than concatenating into a shell string, and ensure
tempfile path (self.temp_dir + framefile) is joined and sanitized; finally,
modify calls around build_shell_command and scmd.run to accept the argv form (or
properly escaped string) so untrusted values cannot inject additional shell
operators.

except Exception:
logging.exception(f"Error running cmd '{cmd}'")

# Process result
if cmdstatus:
result.update({
'frame': str(self.framecount),
Expand Down
43 changes: 42 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ does.
#previewimage: True
#saveframes: False
#wget_skip_cert_check: False
#gphoto2_camera_model: Nikon DSC D40 (PTP mode)

```

Expand Down Expand Up @@ -267,4 +268,44 @@ To get that change at every klipper start:
[delayed_gcode _INIT_TIMELAPSE_CHECK_TIME]
initial_duration: 1
gcode: SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=check_time VALUE=0.5
```
```
# gphoto2 Configuration

## Connect Your Camera

1. Connect camera via USB
2. Enable PTP/USB mode on camera (varies by model)
3. Verify detection:

```bash
gphoto2 --auto-detect
```

You should see your camera listed:

```bash
Model Port
----------------------------------------------------------
Nikon DSC D40 (PTP mode) usb:003,004
```

## Update Configuration

Add a dummy webcam and give it a stream and snapshot URL that is not in use.
Select that Camara in the timelaps menue.
Set the Stream Delay Compensation to the amount of time it takes to focus and take a picture (setting it to manual focus decreases the time taken)
Comment on lines +294 to +296

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Correct spelling in the gphoto2 setup steps.

Line 295 has multiple typos (“Camara”, “timelaps”, “menue”), which makes the instructions look unreliable.

🧰 Tools
🪛 LanguageTool

[grammar] ~295-~295: Ensure spelling is correct
Context: ...hot URL that is not in use. Select that Camara in the timelaps menue. Set the Stream D...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~295-~295: Ensure spelling is correct
Context: ...s not in use. Select that Camara in the timelaps menue. Set the Stream Delay Compensation to t...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/configuration.md` around lines 294 - 296, Update the spelling of the
gphoto2 setup steps by correcting the misspelled tokens: change "Camara" to
"Camera", "timelaps" to "timelapse", and "menue" to "menu" in the sentence that
reads "Select that Camara in the timelaps menue" and ensure the surrounding
guidance still reads clearly about adding a dummy webcam and setting Stream
Delay Compensation.

Source: Linters/SAST tools


Edit `moonraker.conf`:

```ini
[timelapse]
gphoto2_camera_model = Nikon DSC D40 (PTP mode)
```

## Camera "Device busy"

```bash
# Kill any process using camera
killall gvfs-gphoto2-volume-monitor
killall gphoto2
```
16 changes: 16 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,19 @@ managed_services: klipper moonraker

Please see [configuration.md](configuration.md) for details on how to
configure the timelapse component.


# Install gphoto2 if an External camara is to be used like a Nikon D40

```bash
# Debian/Ubuntu/Raspbian
sudo apt-get update
sudo apt-get install gphoto2
```

## Support for gphoto2

For issues:
1. Check `moonraker.log` for errors
2. Test gphoto2 manually: `gphoto2 --camera '{Model}' --capture-image-and-download --filename '/tmp/test_Image000001.jpg'`
3. Verify camera compatibility at: http://gphoto.org/proj/libgphoto2/support.php