navidrome/plugins/examples/webhook-rs
Deluan Quintão 1a8463f7de
Merge commit from fork
* fix(share): always assign the authenticated user as share owner

A share's UserID was taken from the request body and only defaulted when
empty, so any authenticated user could create a share attributed to
another user. For playlist shares the contents are resolved in the
owner's library-access context, turning the spoofed owner into an
access-escalation vector in multi-library setups.

Force the owner from the request context at both the service boundary
and the persistence layer, ignoring any client-supplied UserID.

* fix(plugins): block SSRF to private IPs resolved from hostnames

The HTTP host client only checked the literal host string, so a symbolic
hostname (or a trailing-dot "localhost.") resolving to a private/loopback
address bypassed the SSRF guard when a plugin declared no requiredHosts.

Enforce the check at dial time via net.Dialer.Control on the resolved IP,
which also covers redirect hops and DNS rebinding. When an explicit
requiredHosts allowlist is set, defer to it as the operator's trust decision.

* fix(plugins): gate private IPs on explicit IP/CIDR allowlist entries

Following review feedback: an allowlisted hostname authorizes the external
service, not whatever private IP it may resolve or rebind to. Enforce the
resolved-IP guard even when requiredHosts is set, permitting a private
address only when a literal IP or CIDR entry explicitly covers it. This
keeps "reach this external API" and "reach my internal network" as two
separate, explicit operator decisions.

* fix(plugins): treat unspecified addresses as private in the SSRF guard

Dialing 0.0.0.0 or :: reaches the local host, so they bypassed the
private/loopback check.

* fix(plugins): let a bare "*" allowlist reach private addresses

Plugins such as AudioMuse-AI declare requiredHosts ["*"] to reach a
user-configured service on the LAN, whose address the manifest cannot
know. Requiring a literal IP/CIDR entry broke them. Named hosts and
subdomain wildcards still cannot resolve to private addresses.

* refactor(plugins): simplify the SSRF-guarded HTTP client and release its pool

Build the client directly around the guarded transport instead of
replacing a throwaway one, fail closed on an unparseable dial address,
and close the per-plugin transport's idle connections when the plugin
unloads. Trim stale comments.

* fix(plugins): stop enabling extism's unguarded http_request host function

Passing requiredHosts as the extism manifest's AllowedHosts enabled
extism's own http_request (pdk.NewHTTPRequest), which only glob-matches
the hostname and follows redirects without re-checking, bypassing the
resolved-IP SSRF guard. Plugins must use host.HTTPSend.

* fix(plugins): move bundled Rust examples to the host HTTP service

Extism's built-in http_request is now disabled, so the webhook and
Discord examples switch to nd_pdk::host::http::send. Update the README
to say host.HTTPSend is the only supported way to make HTTP requests.

* fix(plugins): move the Python example to the host HTTP service

coverartarchive-py used extism's built-in Http.request, which is now
disabled. Call Navidrome's http_send host function instead. The plugin
can no longer run under the standalone extism CLI, so drop the CLI test
targets and instructions.
2026-09-12 13:38:08 -04:00
..
.cargo feat(plugins): New Plugin System with multi-language PDK support (#4833) 2026-01-14 19:22:48 -05:00
src Merge commit from fork 2026-09-12 13:38:08 -04:00
Cargo.toml feat(plugins): New Plugin System with multi-language PDK support (#4833) 2026-01-14 19:22:48 -05:00
manifest.json feat(plugins): New Plugin System with multi-language PDK support (#4833) 2026-01-14 19:22:48 -05:00
README.md feat(plugins): New Plugin System with multi-language PDK support (#4833) 2026-01-14 19:22:48 -05:00

Webhook Scrobbler Plugin (Rust)

A Navidrome plugin written in Rust that sends HTTP webhook notifications when tracks are scrobbled. This is useful for integrating with external services like home automation systems, Discord bots, monitoring tools, or any service that can receive HTTP requests.

Features

  • Sends HTTP GET requests to configured URLs on every scrobble event
  • Includes track metadata (title, artist, album, username, timestamp) as query parameters
  • Supports multiple webhook URLs (comma-separated)
  • All users are automatically authorized (no external service authentication required)
  • Now playing events are ignored (webhooks fire only on completed scrobbles)

Prerequisites

  • Rust toolchain
  • WebAssembly target: rustup target add wasm32-unknown-unknown

Building

From the plugins/examples directory:

make webhook-rs.ndp

Or build directly with cargo:

cd webhook-rs
cargo build --release
zip -j webhook-rs.ndp manifest.json target/wasm32-unknown-unknown/release/webhook_rs.wasm

Installation

Copy webhook-rs.ndp to your Navidrome plugins folder (configured via Plugins.Folder in your config).

Configuration

Configure in the Navidrome UI (Settings → Plugins → webhook-rs):

Key Description Example
urls Comma-separated list of webhook URLs https://example.com/hook1,https://example.com/hook2

Webhook Request Format

When a scrobble occurs, the plugin sends an HTTP GET request to each configured URL with the following query parameters:

Parameter Description
title Track title
artist Track artist
album Album name
user Username who scrobbled
timestamp Unix timestamp when the track started playing

Example request:

GET https://example.com/webhook?title=Song%20Name&artist=Artist%20Name&album=Album%20Name&user=john&timestamp=1703270400

Use Cases

  • Home Automation: Trigger lights or displays when music starts playing
  • Discord/Slack Notifications: Post currently playing tracks to a channel
  • Logging/Analytics: Track listening history in an external system
  • IFTTT/Zapier Integration: Connect to thousands of services via webhook triggers

Development

The plugin is built using the Extism Rust PDK. Key exports:

  • nd_manifest - Returns plugin metadata and permissions
  • nd_scrobbler_is_authorized - Always returns true (all users authorized)
  • nd_scrobbler_now_playing - No-op (returns success without action)
  • nd_scrobbler_scrobble - Sends webhooks to configured URLs