Skip to content

Settings: send query parameters on http(s) settings urls - #1404

Merged
mickem merged 4 commits into
mainfrom
claude/nscp-issue-460-check-39fdt1
Aug 14, 2026
Merged

Settings: send query parameters on http(s) settings urls#1404
mickem merged 4 commits into
mainfrom
claude/nscp-issue-460-check-39fdt1

Conversation

@mickem

@mickem mickem commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Fixes #460.

The problem

A boot.ini entry such as

[settings]
1 = http://nsclient.mydom.local/nsclient/nsclient.php?RootFolder=myhost/&Filename=nsclient.ini
2 = ini://${shared-path}/nsclient.ini

was fetched as a bare GET /nsclient.php. net::parse splits the query off the path into url.query (include/net/net.hpp), but settings_http only ever handed url.path to the downloader, so every parameter was silently dropped — a script that generates per-host configuration could never see which host was asking. net::url::query was in fact read nowhere in the tree.

The fix

  • net::url::get_request_path() reassembles path and query into the form that belongs on the request line, and settings_http::cache_remote_file uses it for the download. Proxied requests come along for free: make_proxy_request builds its absolute URI from the same path_.
  • url::to_string() now includes the query, so Creating instance for: … and the TLS advisories name the url that is actually being fetched rather than a truncated one.
  • Cache file naming. resolve_cache_file derived the name from the url path alone, so two entries pointing at the same script with different parameters resolved to one file and clobbered each other. The query now contributes a short digest to the name — it cannot be appended verbatim, since ? and & are not legal in a Windows file name. Urls without a query keep their historic plain name, so existing installs do not re-download on upgrade.
  • A url with no file name at all (http://host/?file=x) previously collapsed onto the cache directory itself; it now falls back to cached.ini.
  • Dropped the dead http::request in cache_remote_file — it was built from url.path and never sent.

Tests

New include/net/net_test.cpp (added to the net_test target) covers url parsing, get_request_path and to_string round-tripping, including the empty-query and ini:// cases.

settings_http_test.cpp: the loopback server now records the request line it receives, and four new cases assert that the query reaches the wire, that a url without one is unchanged, that two queries against the same script resolve to different cache files with names legal on every platform, and that a url without a file name still resolves inside the cache folder.

Both new settings_http cases were confirmed to fail against the old code:

request line was: GET /nsclient.php HTTP/1.0
[  FAILED  ] settings_http.download_sends_the_query_string
Expected: (ra) != (rb), actual: ".../nsclient.php" vs ".../nsclient.php"
[  FAILED  ] settings_http.resolve_cache_file_separates_urls_differing_only_in_query

With the fix, settings_http_test passes 14/14 and net_test 497/497 (1 skipped: the IPv6 pinger case, environmental).

Docs: a "Query parameters" subsection under http settings in docs/docs/concepts/settings.md.


Generated by Claude Code

mickem added 4 commits August 13, 2026 14:17
A boot.ini entry such as

  [settings]
  1 = http://nsclient.mydom.local/nsclient.php?RootFolder=myhost/&Filename=nsclient.ini

was fetched as a bare "GET /nsclient.php": net::parse splits the query off
the path into url.query, but settings_http only ever handed url.path to the
downloader, so every parameter was silently dropped and a script that
generates per-host configuration could never see which host was asking.

Add net::url::get_request_path(), which reassembles path and query into the
form that belongs on the request line, and use it for the settings download.
url::to_string() now includes the query too, so the log lines and TLS
warnings name the url that is actually being fetched. Proxied requests are
covered as well: make_proxy_request builds its absolute URI from the same
path.

Caching had to follow. resolve_cache_file derived the cache file name from
the url path alone, so two entries pointing at the same script with
different parameters resolved to one file and overwrote each other. The
query now contributes a short digest to the name - it cannot be appended
verbatim since '?' and '&' are not legal in a Windows file name. A url with
no file name at all ("http://host/?file=x") no longer collapses onto the
cache directory itself but falls back to cached.ini.

Also drop the dead http::request in cache_remote_file; it was built from
url.path and never sent.

Tests: new net_test.cpp covers url parsing, get_request_path and to_string
round-tripping; settings_http_test now asserts on the request line the
loopback server actually receives, and on cache file separation.

Fixes #460

Assisted-by: Claude Code
Signed-off-by: Michael Medin <michael@medin.name>
Follow-up to the http settings query fix, addressing review findings.

Cache file migration. Adding the query digest to the cache file name
renames the cache file of exactly the urls the fix is aimed at. That
matters because cache_remote_file falls back to the cached copy when the
settings server cannot be reached: an agent upgrading while its server was
down would have found nothing under the new name and booted with an empty
configuration, having booted fine off the cache the day before.
migrate_legacy_cache_file moves the old file into place once, so the
fallback keeps working across the upgrade. Query-less urls keep their name
and are left alone.

Request line encoding. The query is now concatenated onto the request
target, so characters that are illegal there reach the wire for the first
time. A space produced a malformed three-token request line; a CR or LF
split one request into two. net::encode_query percent-encodes anything
outside the RFC 3986 query grammar, passing an existing "%XX" through
untouched so an already-encoded query is not encoded twice, and escaping a
'%' that introduces no valid pair.

Log hygiene. A settings url may authenticate with its parameters
("?token=..."), and the previous commit put the whole url in the log,
including at warning level on every boot when TLS verification is off or
the CA bundle is missing. Add url::get_baseurl(), url::get_path() and
url::to_log_safe_string() composing the two, and use the latter for the
settings log lines and TLS advisories. to_string() keeps the query and
remains the faithful rendering.

Not addressed here: the cache file name still ignores host and directory,
so two urls whose paths differ only above the file name continue to share
one cache file. That predates this series and is left for its own change.

Fixes #460

Assisted-by: Claude Code
Signed-off-by: Michael Medin <michael@medin.name>
Now that a settings url can carry parameters, the obvious next thing to put
in them is which host is asking - that is what turns one boot.ini into a
fleet-wide configuration instead of a per-host file:

  [settings]
  1 = http://cfgsrv/nsclient.php?host=${hostname}

Run settings urls through socket_helpers::expand_hostname, the same helper
the submit clients (NRDP, Graphite, Syslog, Icinga, ...) already use for
their "hostname" setting, so the placeholders mean the same thing wherever
they appear. Expansion happens before parsing, so a placeholder may sit
anywhere in the url - query, path or host - and before the query is
percent-encoded, so a host name needing an escape gets one rather than
corrupting the request line. Attachment urls go through the same path.

The cache file name is derived from the expanded url, so each host caches
its own configuration rather than sharing one "${hostname}" bucket.

expand_hostname itself gains ${hostname}, ${hostname_lc} and ${hostname_uc}
for the full system name. It only had ${host} and ${domain}, which split on
the first '.', so a template had no way to ask for the name as reported -
"auto" gives it, but only as the entire spec, which a url cannot be. This
is additive: the token was previously left in place as literal text. Every
module using expand_hostname picks it up, which is the intent - the
placeholder set should not differ between a settings url and an NRDP
sender name.

Assisted-by: Claude Code
Signed-off-by: Michael Medin <michael@medin.name>
The docs promised that NSClient++ logs settings urls as scheme, host and
path only, but only the two TLS warnings in settings_http actually went
through to_log_safe_string(). boot() still echoed the raw boot.ini entry
three times - once at info level - and get_info(), which `nscp settings
--show` prints, embedded the raw context. An operator who put a token in
a settings url on the strength of that paragraph got it written to the
log file verbatim on every boot.

Redact at the remaining sites: the "Activating"/"Failed to activate"/
"using that" messages and the boot order list in settings_manager_impl,
the "Undefined settings protocol" exception boot() logs, and get_info().
Also correct the version markers in the docs - these changes land after
0.16.0, not in 0.14 - and note that only the agent's own output is
covered, not a proxy or the settings server's access log.

Signed-off-by: Michael Medin <michael@medin.name>
Assisted-by: Claude Code:claude-opus-5
@mickem
mickem merged commit 913553a into main Aug 14, 2026
29 checks passed
@mickem
mickem deleted the claude/nscp-issue-460-check-39fdt1 branch August 14, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

http settings 'boot.ini' URL with parameter not work

1 participant