Skip to content

Make the Telegram API base URL settable, and close what that opens - #45

Merged
umputun merged 1 commit into
go-pkgz:masterfrom
paskal:feat/telegram-api-url
Aug 29, 2026
Merged

Make the Telegram API base URL settable, and close what that opens#45
umputun merged 1 commit into
go-pkgz:masterfrom
paskal:feat/telegram-api-url

Conversation

@paskal

@paskal paskal commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

The bot API host is fixed at api.telegram.org, reachable only through an unexported field the package's own tests set (apiPrefix, "changed only in tests"). Two consequences: an operator behind a proxy has no way in, and the Telegram notifier is unreachable from any test unwilling to talk to the live API.

remark42 hit the second one. Its Telegram auth now points at a stub, since go-pkgz/auth gained the same option in go-pkgz/auth#316, but the notify service still reaches the public API — and remark42's update dispatcher takes its requester from that service, so the whole Telegram subscription flow cannot be covered by a browser test at all. Those cases are the last block of its jest suite with no e2e equivalent.

APIURL takes the base and the bot segment is appended here, so a caller passes https://proxy.example.com and requests come out as https://proxy.example.com/bot<token>/<method> — what Telegram itself serves, and what go-pkgz/auth's option accepts, so an operator configuring both writes the same value twice.

Moving a token-bearing URL across a boundary that used to be frozen is what the rest of this is for. Each of these is inert while the host is api.telegram.org and live the moment it is not, which is why they belong in this change rather than after it.

The base is validated rather than trimmed. https://api.telegram.org@evil.tld is a valid URL whose host is evil.tld, and every request built from the base carries the bot token in its path, so it would ship the token there. Absolute http or https, host required, no userinfo, query, fragment or opaque part; a path prefix is allowed. No rejection echoes the value — it is configuration that can carry credentials in its userinfo or a secret where the port belongs — and the parse error is dropped rather than wrapped, because *url.Error prints the URL it was given and its inner error quotes the input back too.

Errors are scrubbed of the token, not only of *url.Error's URL field. The upstream decides an API error's text, and parseError interpolated its description raw, so a proxy answering for the API could echo the request URI back and put the token into a caller's log. The scrub covers the raw and encoded forms, then checks a decoded copy and withholds the text when the token cannot be shown absent from it: after a decode failure nothing was established, and withholding is the only answer that cannot leak. It applies only to a value shaped like a bot token, <id>:<secret> — blanking a short arbitrary string out of a diagnostic corrupts more than it protects, and the package's own tests use a token of "404".

Redirects are refused. Go copies the previous URL into Referer on any hop that is not https-to-http, so following one hands the destination the token. Telegram does not redirect; something standing in for it can.

Each guard fails against the thing it names:

reverted what fails
the validation 8 of the rejection subtests
the error redaction both leak cases
the redirect refusal the Referer case
fail-closed on a decode error a double-encoded token is released

go test ./... and golangci-lint run ./... are clean.

One thing this does not change: the HTTP client is still built inside Request, so a proxy whose TLS material lives on a caller-supplied client cannot be used yet. That is a larger change and I have left it alone.

The bot API host was fixed at api.telegram.org, reachable only through an
unexported field the package's own tests set. Two consequences: an operator
behind a proxy had no way in, and the Telegram notifier was unreachable from
any test unwilling to talk to the live API.

remark42 hit the second one. Its Telegram auth now points at a stub through
go-pkgz/auth, which gained the same option, but the notify service still
reaches the public API, and the update dispatcher takes its requester from
that service, so the subscription flow cannot be covered by a browser test at
all.

APIURL takes the base and the "bot" segment is appended here, so a caller
passes https://proxy.example.com and requests come out as
https://proxy.example.com/bot<token>/<method>, matching what Telegram serves
and what go-pkgz/auth's own option accepts.

Moving a token-bearing URL across a boundary that used to be frozen is what
the rest of this is for.

The base is validated rather than trimmed. "https://api.telegram.org@evil.tld"
is a valid URL whose host is evil.tld, and every request built from the base
carries the bot token in its path, so it would ship the token there. Absolute
http or https, host required, no userinfo, query, fragment or opaque part; a
path prefix is allowed for a proxy mounted under one. No rejection echoes the
value, since it is configuration that can carry credentials in its userinfo or
a secret where the port belongs, and the parse error is dropped rather than
wrapped because *url.Error prints the URL it was given.

Errors are scrubbed of the token itself, not only of *url.Error's URL field.
The upstream decides an API error's text, and parseError interpolated its
description raw, so something standing in for Telegram could echo the request
URI back and put the token in a caller's log. The scrub covers the raw and
encoded forms and then checks a decoded copy, withholding the text when the
token cannot be shown absent from it: after a decode failure nothing was
established, and withholding is the only answer that cannot leak. It applies
only to a value shaped like a bot token, "<id>:<secret>", since blanking a
short arbitrary string out of a diagnostic corrupts more than it protects.

Redirects are refused. Go copies the previous URL into Referer on any hop that
is not https-to-http, so following one hands the destination the token.
Telegram does not redirect; something standing in for it can.

Each guard fails against the thing it names: reverting the validation fails
eight of the rejection cases, dropping the redaction fails two, following
redirects fails the referer case, and answering "not recoverable" on a decode
error releases a double-encoded token.

One thing this does not change: the HTTP client is still built here, so a
proxy whose TLS material lives on a caller-supplied client cannot be used yet.
@paskal
paskal requested a review from umputun as a code owner August 26, 2026 20:55
paskal added a commit to umputun/remark42 that referenced this pull request Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the
reader messages a bot, and no page can reach that. e2e/telegramstub
answers as the bot API for the four calls the flow makes and takes the
reader's side of the exchange through /control/send, so the round trip
becomes drivable end to end. It runs on its own instance, because adding
a provider to the main one would change the auth panel every other case
reads.

Reaching it needs the bot API base url to be settable, which is what
--telegram.api-url adds. It serves a real operator need beyond the tests,
a proxy or a self-hosted Bot API server where Telegram is blocked, and
both consumers honour it: the auth provider and the notification service.
The value is an origin, and the token travels in the request path, so it
has to be a host the operator controls.

Writing the coverage found two defects in the wiring. An instance with
telegram auth enabled and notifications failing registered the provider
and never started anything to listen, so it accepted the configuration
and stayed permanently deaf. And a nil *notify.Telegram was assigned into
an interface, where it is not nil, so the guard downstream let it through.

Blocked on two upstream releases, and cannot merge before both: the base
url reaches the notification service through go-pkgz/notify#45, which is
open, and the auth provider through a go-pkgz/auth change that is merged
but unreleased. backend/go.mod carries a replace for the first, which a
published module cannot ship, so it comes off when they land.
paskal added a commit to umputun/remark42 that referenced this pull request Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the
reader messages a bot, and no page can reach that. e2e/telegramstub
answers as the bot API for the four calls the flow makes and takes the
reader's side of the exchange through /control/send, so the round trip
becomes drivable end to end. It runs on its own instance, because adding
a provider to the main one would change the auth panel every other case
reads.

Reaching it needs the bot API base url to be settable, which is what
--telegram.api-url adds. It serves a real operator need beyond the tests,
a proxy or a self-hosted Bot API server where Telegram is blocked, and
both consumers honour it: the auth provider and the notification service.
The value is an origin, and the token travels in the request path, so it
has to be a host the operator controls.

Writing the coverage found two defects in the wiring. An instance with
telegram auth enabled and notifications failing registered the provider
and never started anything to listen, so it accepted the configuration
and stayed permanently deaf. And a nil *notify.Telegram was assigned into
an interface, where it is not nil, so the guard downstream let it through.

Blocked on two upstream releases, and cannot merge before both: the base
url reaches the notification service through go-pkgz/notify#45, which is
open, and the auth provider through a go-pkgz/auth change that is merged
but unreleased. backend/go.mod carries a replace for the first, which a
published module cannot ship, so it comes off when they land.
paskal added a commit to umputun/remark42 that referenced this pull request Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the
reader messages a bot, and no page can reach that. e2e/telegramstub
answers as the bot API for the four calls the flow makes and takes the
reader's side of the exchange through /control/send, so the round trip
becomes drivable end to end. It runs on its own instance, because adding
a provider to the main one would change the auth panel every other case
reads.

Reaching it needs the bot API base url to be settable, which is what
--telegram.api-url adds. It serves a real operator need beyond the tests,
a proxy or a self-hosted Bot API server where Telegram is blocked, and
both consumers honour it: the auth provider and the notification service.
The value is an origin, and the token travels in the request path, so it
has to be a host the operator controls.

Writing the coverage found two defects in the wiring. An instance with
telegram auth enabled and notifications failing registered the provider
and never started anything to listen, so it accepted the configuration
and stayed permanently deaf. And a nil *notify.Telegram was assigned into
an interface, where it is not nil, so the guard downstream let it through.

Blocked on two upstream releases, and cannot merge before both: the base
url reaches the notification service through go-pkgz/notify#45, which is
open, and the auth provider through a go-pkgz/auth change that is merged
but unreleased. backend/go.mod carries a replace for the first, which a
published module cannot ship, so it comes off when they land.
paskal added a commit to umputun/remark42 that referenced this pull request Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the
reader messages a bot, and no page can reach that. e2e/telegramstub
answers as the bot API for the four calls the flow makes and takes the
reader's side of the exchange through /control/send, so the round trip
becomes drivable end to end. It runs on its own instance, because adding
a provider to the main one would change the auth panel every other case
reads.

Reaching it needs the bot API base url to be settable, which is what
--telegram.api-url adds. It serves a real operator need beyond the tests,
a proxy or a self-hosted Bot API server where Telegram is blocked, and
both consumers honour it: the auth provider and the notification service.
The value is an origin, and the token travels in the request path, so it
has to be a host the operator controls.

Writing the coverage found two defects in the wiring. An instance with
telegram auth enabled and notifications failing registered the provider
and never started anything to listen, so it accepted the configuration
and stayed permanently deaf. And a nil *notify.Telegram was assigned into
an interface, where it is not nil, so the guard downstream let it through.

Blocked on two upstream releases, and cannot merge before both: the base
url reaches the notification service through go-pkgz/notify#45, which is
open, and the auth provider through a go-pkgz/auth change that is merged
but unreleased. backend/go.mod carries a replace for the first, which a
published module cannot ship, so it comes off when they land.
paskal added a commit to umputun/remark42 that referenced this pull request Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the
reader messages a bot, and no page can reach that. e2e/telegramstub
answers as the bot API for the four calls the flow makes and takes the
reader's side of the exchange through /control/send, so the round trip
becomes drivable end to end. It runs on its own instance, because adding
a provider to the main one would change the auth panel every other case
reads.

Reaching it needs the bot API base url to be settable, which is what
--telegram.api-url adds. It serves a real operator need beyond the tests,
a proxy or a self-hosted Bot API server where Telegram is blocked, and
both consumers honour it: the auth provider and the notification service.
The value is an origin, and the token travels in the request path, so it
has to be a host the operator controls.

Writing the coverage found two defects in the wiring. An instance with
telegram auth enabled and notifications failing registered the provider
and never started anything to listen, so it accepted the configuration
and stayed permanently deaf. And a nil *notify.Telegram was assigned into
an interface, where it is not nil, so the guard downstream let it through.

Blocked on two upstream releases, and cannot merge before both: the base
url reaches the notification service through go-pkgz/notify#45, which is
open, and the auth provider through a go-pkgz/auth change that is merged
but unreleased. backend/go.mod carries a replace for the first, which a
published module cannot ship, so it comes off when they land.
paskal added a commit to umputun/remark42 that referenced this pull request Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the
reader messages a bot, and no page can reach that. e2e/telegramstub
answers as the bot API for the four calls the flow makes and takes the
reader's side of the exchange through /control/send, so the round trip
becomes drivable end to end. It runs on its own instance, because adding
a provider to the main one would change the auth panel every other case
reads.

Reaching it needs the bot API base url to be settable, which is what
--telegram.api-url adds. It serves a real operator need beyond the tests,
a proxy or a self-hosted Bot API server where Telegram is blocked, and
both consumers honour it: the auth provider and the notification service.
The value is an origin, and the token travels in the request path, so it
has to be a host the operator controls.

Writing the coverage found two defects in the wiring. An instance with
telegram auth enabled and notifications failing registered the provider
and never started anything to listen, so it accepted the configuration
and stayed permanently deaf. And a nil *notify.Telegram was assigned into
an interface, where it is not nil, so the guard downstream let it through.

Blocked on two upstream releases, and cannot merge before both: the base
url reaches the notification service through go-pkgz/notify#45, which is
open, and the auth provider through a go-pkgz/auth change that is merged
but unreleased. backend/go.mod carries a replace for the first, which a
published module cannot ship, so it comes off when they land.
paskal added a commit to umputun/remark42 that referenced this pull request Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the
reader messages a bot, and no page can reach that. e2e/telegramstub
answers as the bot API for the four calls the flow makes and takes the
reader's side of the exchange through /control/send, so the round trip
becomes drivable end to end. It runs on its own instance, because adding
a provider to the main one would change the auth panel every other case
reads.

Reaching it needs the bot API base url to be settable, which is what
--telegram.api-url adds. It serves a real operator need beyond the tests,
a proxy or a self-hosted Bot API server where Telegram is blocked, and
both consumers honour it: the auth provider and the notification service.
The value is an origin, and the token travels in the request path, so it
has to be a host the operator controls.

Writing the coverage found two defects in the wiring. An instance with
telegram auth enabled and notifications failing registered the provider
and never started anything to listen, so it accepted the configuration
and stayed permanently deaf. And a nil *notify.Telegram was assigned into
an interface, where it is not nil, so the guard downstream let it through.

Blocked on two upstream releases, and cannot merge before both: the base
url reaches the notification service through go-pkgz/notify#45, which is
open, and the auth provider through a go-pkgz/auth change that is merged
but unreleased. backend/go.mod carries a replace for the first, which a
published module cannot ship, so it comes off when they land.
paskal added a commit to umputun/remark42 that referenced this pull request Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the
reader messages a bot, and no page can reach that. e2e/telegramstub
answers as the bot API for the four calls the flow makes and takes the
reader's side of the exchange through /control/send, so the round trip
becomes drivable end to end. It runs on its own instance, because adding
a provider to the main one would change the auth panel every other case
reads.

Reaching it needs the bot API base url to be settable, which is what
--telegram.api-url adds. It serves a real operator need beyond the tests,
a proxy or a self-hosted Bot API server where Telegram is blocked, and
both consumers honour it: the auth provider and the notification service.
The value is an origin, and the token travels in the request path, so it
has to be a host the operator controls.

Writing the coverage found two defects in the wiring. An instance with
telegram auth enabled and notifications failing registered the provider
and never started anything to listen, so it accepted the configuration
and stayed permanently deaf. And a nil *notify.Telegram was assigned into
an interface, where it is not nil, so the guard downstream let it through.

Blocked on two upstream releases, and cannot merge before both: the base
url reaches the notification service through go-pkgz/notify#45, which is
open, and the auth provider through a go-pkgz/auth change that is merged
but unreleased. backend/go.mod carries a replace for the first, which a
published module cannot ship, so it comes off when they land.

@umputun umputun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@umputun
umputun merged commit 57a5309 into go-pkgz:master Aug 29, 2026
1 check passed
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.

2 participants