REPOSITORY / ScuroNeko/Laniakea

Pull Requests

PULL REQUESTS REPOSITORY

v1.0.1 #15

Merged
ScuroNeko merged 2 commits from dev into main 2026-06-11 13:42:07 +03:00
7 changed files with 42 additions and 18 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog # Changelog
## v1.0.1
### Fixed
- Fixed webhook always accepting unauthenticated requests when `SecretToken` is not configured. A cryptographically random 32-byte token is now generated automatically when `SecretToken` is empty, so the webhook endpoint is always authenticated. The generated token is logged as a warning so the operator can record it.
- Fixed `tgapi.NewAPI` and `tgapi.NewUploader` not installing token redaction on their managed loggers. The bot token is now masked as `<TOKEN>` in debug output even when the `tgapi` package is used standalone without the `laniakea.Bot` wrapper.
## v1.0.0 ## v1.0.0
### Breaking Changes ### Breaking Changes
+11 -6
View File
@@ -137,6 +137,15 @@ func (bot *Bot[T]) RunWebhookWithContext(ctx context.Context, opts *BotWebhookOp
if len(bot.plugins) == 0 { if len(bot.plugins) == 0 {
return ErrNoPlugins return ErrNoPlugins
} }
autoSecret := ""
if opts.SecretToken == "" {
rndSecret, err := generateToken(32)
if err != nil {
return err
}
opts.SecretToken = rndSecret
autoSecret = rndSecret
}
if opts.URL == "" { if opts.URL == "" {
return ErrNoBotWebhookOptsURL return ErrNoBotWebhookOptsURL
} }
@@ -146,9 +155,6 @@ func (bot *Bot[T]) RunWebhookWithContext(ctx context.Context, opts *BotWebhookOp
if err := validateWebhookPath(opts.Path, opts.UseStatusPath); err != nil { if err := validateWebhookPath(opts.Path, opts.UseStatusPath); err != nil {
return err return err
} }
if opts.UseStatusPath && opts.SecretToken == "" {
return ErrStatusPathSecretRequired
}
if err := validateWebhookTLSFiles(tlsFiles); err != nil { if err := validateWebhookTLSFiles(tlsFiles); err != nil {
return err return err
} }
@@ -158,10 +164,9 @@ func (bot *Bot[T]) RunWebhookWithContext(ctx context.Context, opts *BotWebhookOp
} }
return bot.runWebhookRuntime(ctx, func(runCtx context.Context) error { return bot.runWebhookRuntime(ctx, func(runCtx context.Context) error {
if opts.SecretToken == "" { if autoSecret != "" {
bot.webhookLogger.Warnln("Using webhook without secret is very dangerous. Anyone can simulate Telegram requests.") bot.webhookLogger.Warnln("Using webhook without secret is very dangerous. Using random 32 bytes token:", autoSecret)
} }
i, err := bot.api.GetWebhookInfoWithContext(runCtx) i, err := bot.api.GetWebhookInfoWithContext(runCtx)
if err != nil { if err != nil {
return err return err
+8 -8
View File
@@ -350,20 +350,20 @@ func TestRunWebhookWithContextRejectsInvalidTLSFilesBeforeRemoteSetup(t *testing
} }
} }
func TestRunWebhookWithContextRequiresSecretWhenStatusPathEnabled(t *testing.T) { func TestRunWebhookWithContextAutoGeneratesSecretWhenEmpty(t *testing.T) {
bot := &Bot[NoData]{ bot := &Bot[NoData]{
prefixes: []string{"/"}, prefixes: []string{"/"},
plugins: []Plugin[NoData]{{name: "demo"}}, plugins: []Plugin[NoData]{{name: "demo"}},
} }
opts := NewBotWebhookOpts(). // No SecretToken, no URL — function should auto-generate the token
SetURL("https://bot.example.com"). // and then fail with ErrNoBotWebhookOptsURL before any network call.
SetUseStatusPath(true) opts := NewBotWebhookOpts().SetUseStatusPath(true)
err := bot.RunWebhookWithContext(context.Background(), opts) err := bot.RunWebhookWithContext(context.Background(), opts)
if err == nil { if !errors.Is(err, ErrNoBotWebhookOptsURL) {
t.Fatal("expected status-path secret validation error, got nil") t.Fatalf("expected ErrNoBotWebhookOptsURL after auto-generation, got: %v", err)
} }
if !strings.Contains(err.Error(), "SecretToken required") { if opts.SecretToken == "" {
t.Fatalf("unexpected error: %v", err) t.Fatal("expected SecretToken to be auto-generated, got empty string")
} }
} }
+1
View File
@@ -122,6 +122,7 @@ func NewAPI(opts *APIOpts) *API {
"API", utils.GetLoggerLevel(), "API", utils.GetLoggerLevel(),
opts.logFormat, opts.logFormatter, opts.logFormat, opts.logFormatter,
) )
logger.AddReplacer(opts.token, "<TOKEN>")
client := opts.client client := opts.client
if client == nil { if client == nil {
+3 -2
View File
@@ -79,6 +79,7 @@ func NewUploader(api *API) *Uploader {
"UPLOADER", utils.GetLoggerLevel(), "UPLOADER", utils.GetLoggerLevel(),
api.logFormat, api.logFormatter, api.logFormat, api.logFormatter,
) )
logger.AddReplacer(api.token, "<TOKEN>")
return &Uploader{api, logger} return &Uploader{api, logger}
} }
@@ -143,7 +144,7 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
req.Header.Set("User-Agent", fmt.Sprintf("Laniakea/%s", utils.VersionString)) req.Header.Set("User-Agent", fmt.Sprintf("Laniakea/%s", utils.VersionString))
req.ContentLength = int64(buf.Len()) req.ContentLength = int64(buf.Len())
up.logger.Debugln("UPLOADER REQ", r.method) up.logger.Debugln("UPLOADER REQ", url)
resp, err := up.api.client.Do(req) resp, err := up.api.client.Do(req)
if err != nil { if err != nil {
return zero, err return zero, err
@@ -154,7 +155,7 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
if err != nil { if err != nil {
return zero, err return zero, err
} }
up.logger.Debugln("UPLOADER RES", r.method, string(body)) up.logger.Debugln("UPLOADER RES", url, string(body))
response, err := parseBody[R](body) response, err := parseBody[R](body)
if err != nil { if err != nil {
+11
View File
@@ -1,6 +1,9 @@
package laniakea package laniakea
import ( import (
"crypto/rand"
"encoding/base64"
"git.scuroneko.dev/scuroneko/laniakea/utils" "git.scuroneko.dev/scuroneko/laniakea/utils"
) )
@@ -27,3 +30,11 @@ const (
// VersionBeta re-exports the module prerelease counter. // VersionBeta re-exports the module prerelease counter.
VersionBeta = utils.VersionBeta VersionBeta = utils.VersionBeta
) )
func generateToken(b int) (string, error) {
bytes := make([]byte, b)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
+2 -2
View File
@@ -2,13 +2,13 @@ package utils
const ( const (
// VersionString is the module version string. // VersionString is the module version string.
VersionString = "1.0.0" VersionString = "1.0.1"
// VersionMajor is the module major version. // VersionMajor is the module major version.
VersionMajor = 1 VersionMajor = 1
// VersionMinor is the module minor version. // VersionMinor is the module minor version.
VersionMinor = 0 VersionMinor = 0
// VersionPatch is the module patch version. // VersionPatch is the module patch version.
VersionPatch = 0 VersionPatch = 1
// VersionBeta is the prerelease counter for the current version. // VersionBeta is the prerelease counter for the current version.
VersionBeta = 0 VersionBeta = 0
) )