Skip to content

Commit

Permalink
Feature: Ability to verify webhook secret token inside the bot program (
Browse files Browse the repository at this point in the history
#95)

* feat(webhook-token): ability to verify webhook secret token inside the bot program

* test(webhook-token): added additional tests for wrong tokens
  • Loading branch information
dharsanb authored Jun 28, 2024
1 parent 0fa1988 commit 7f34de5
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 6 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func main() {

opts := []bot.Option{
bot.WithDefaultHandler(handler),
bot.WithWebhookSecretToken(os.Getenv("EXAMPLE_TELEGRAM_WEBHOOK_SECRET_TOKEN"))
}

b, _ := bot.New(os.Getenv("EXAMPLE_TELEGRAM_BOT_TOKEN"), opts...)
Expand Down Expand Up @@ -181,6 +182,7 @@ b, err := bot.New("YOUR_BOT_TOKEN_FROM_BOTFATHER", opts...)
- `WithSkipGetMe()` - skip call GetMe on bot init
- `WithAllowedUpdates(params AllowedUpdates)` - set [allowed_updates](https://core.telegram.org/bots/api#getupdates) for getUpdates method
- `WithUpdatesChannelCap(cap int)` - set updates channel capacity, by default 1024
- `WithWebhookSecretToken(webhookSecretToken string)` - set X-Telegram-Bot-Api-Secret-Token header sent from telegram servers to confirm validity of update

## Message.Text and CallbackQuery.Data handlers

Expand Down
9 changes: 5 additions & 4 deletions bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ type MatchFunc func(update *models.Update) bool

// Bot represents Telegram Bot main object
type Bot struct {
url string
token string
pollTimeout time.Duration
skipGetMe bool
url string
token string
pollTimeout time.Duration
skipGetMe bool
webhookSecretToken string

defaultHandlerFunc HandlerFunc

Expand Down
103 changes: 101 additions & 2 deletions bot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,12 @@ func TestNew(t *testing.T) {
}
}

func TestBot_StartWebhook(t *testing.T) {
func TestBot_StartWebhookWithCorrectSecret(t *testing.T) {
s := newServerMock("xxx")
defer s.Close()

b, err := New("xxx", WithServerURL(s.URL()))
opts := []Option{WithServerURL(s.URL()), WithWebhookSecretToken("zzzz")}
b, err := New("xxx", opts...)
if err != nil {
t.Fatalf("unexpected error %q", err)
}
Expand Down Expand Up @@ -197,6 +198,7 @@ func TestBot_StartWebhook(t *testing.T) {
t.Error(errReq)
return
}
req.Header.Set("X-Telegram-Bot-Api-Secret-Token", "zzzz")

b.WebhookHandler().ServeHTTP(nil, req)

Expand All @@ -211,6 +213,103 @@ func TestBot_StartWebhook(t *testing.T) {
mx.Unlock()
}

func TestBot_StartWebhookWithNoSecret(t *testing.T) {
s := newServerMock("xxx")
defer s.Close()

opts := []Option{WithServerURL(s.URL())}
b, err := New("xxx", opts...)
if err != nil {
t.Fatalf("unexpected error %q", err)
}

mx := sync.Mutex{}
var called bool

b.defaultHandlerFunc = func(ctx context.Context, bot *Bot, update *models.Update) {
if update.Message.ID != 1 {
t.Errorf("unexpected message id")
}
mx.Lock()
called = true
mx.Unlock()
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go b.StartWebhook(ctx)

time.Sleep(time.Millisecond * 100)

req, errReq := http.NewRequest(http.MethodPost, "", strings.NewReader(`{"update_id":1,"message":{"message_id":1}}`))
if errReq != nil {
t.Error(errReq)
return
}

b.WebhookHandler().ServeHTTP(nil, req)

cancel()

time.Sleep(time.Millisecond * 100)

mx.Lock()
if !called {
t.Errorf("not called default handler")
}
mx.Unlock()
}

func TestBot_StartWebhookWithWrongSecret(t *testing.T) {
s := newServerMock("xxx")
defer s.Close()

opts := []Option{WithServerURL(s.URL()), WithWebhookSecretToken("zzzz")}
b, err := New("xxx", opts...)
if err != nil {
t.Fatalf("unexpected error %q", err)
}

mx := sync.Mutex{}
var called bool

b.defaultHandlerFunc = func(ctx context.Context, bot *Bot, update *models.Update) {
if update.Message.ID != 1 {
t.Errorf("unexpected message id")
}
mx.Lock()
called = true
mx.Unlock()
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go b.StartWebhook(ctx)

time.Sleep(time.Millisecond * 100)

req, errReq := http.NewRequest(http.MethodPost, "", strings.NewReader(`{"update_id":1,"message":{"message_id":1}}`))
if errReq != nil {
t.Error(errReq)
return
}
req.Header.Set("X-Telegram-Bot-Api-Secret-Token", "wrong_secret")

b.WebhookHandler().ServeHTTP(nil, req)

cancel()

time.Sleep(time.Millisecond * 100)

mx.Lock()
if called {
t.Errorf("not supposed to call the default handler with wrong token")
}
mx.Unlock()
}

func TestBot_Start(t *testing.T) {
s := newServerMock("xxx")
defer s.Close()
Expand Down
7 changes: 7 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,10 @@ func WithUpdatesChannelCap(cap int) Option {
b.updates = make(chan *models.Update, cap)
}
}

// WithWebhookSecretToken allows setting X-Telegram-Bot-Api-Secret-Token sent from Telegram servers
func WithWebhookSecretToken(webhookSecretToken string) Option {
return func(b *Bot) {
b.webhookSecretToken = webhookSecretToken
}
}
5 changes: 5 additions & 0 deletions webhook_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import (

func (b *Bot) WebhookHandler() http.HandlerFunc {
return func(_ http.ResponseWriter, req *http.Request) {
if b.webhookSecretToken != "" && req.Header.Get("X-Telegram-Bot-Api-Secret-Token") != b.webhookSecretToken {
b.error("invalid webhook secret token received from update")
return
}

body, errReadBody := io.ReadAll(req.Body)
if errReadBody != nil {
b.error("error read request body, %w", errReadBody)
Expand Down

0 comments on commit 7f34de5

Please sign in to comment.