• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

go-pkgz / auth / 7255079117

18 Dec 2023 11:27PM UTC coverage: 82.941%. Remained the same
7255079117

Pull #189

github

web-flow
Bump golang.org/x/crypto from 0.14.0 to 0.17.0 in /_example

Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #189: Bump golang.org/x/crypto from 0.14.0 to 0.17.0 in /_example

2572 of 3101 relevant lines covered (82.94%)

6.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.27
/provider/telegram.go
1
package provider
2

3
//go:generate moq --out telegram_moq_test.go . TelegramAPI
4

5
import (
6
        "context"
7
        "crypto/sha1"
8
        "encoding/json"
9
        "fmt"
10
        "io"
11
        "net/http"
12
        neturl "net/url"
13
        "strings"
14
        "sync"
15
        "sync/atomic"
16
        "time"
17

18
        "github.com/go-pkgz/repeater"
19
        "github.com/go-pkgz/rest"
20
        "github.com/golang-jwt/jwt"
21

22
        "github.com/go-pkgz/auth/logger"
23
        authtoken "github.com/go-pkgz/auth/token"
24
)
25

26
// TelegramHandler implements login via telegram
27
type TelegramHandler struct {
28
        logger.L
29

30
        ProviderName         string
31
        ErrorMsg, SuccessMsg string
32

33
        TokenService TokenService
34
        AvatarSaver  AvatarSaver
35
        Telegram     TelegramAPI
36

37
        run      int32  // non-zero if Run goroutine has started
38
        username string // bot username
39
        requests struct {
40
                sync.RWMutex
41
                data map[string]tgAuthRequest
42
        }
43
}
44

45
type tgAuthRequest struct {
46
        confirmed bool // whether login request has been confirmed and user info set
47
        expires   time.Time
48
        user      *authtoken.User
49
}
50

51
// TelegramAPI is used for interacting with telegram API
52
type TelegramAPI interface {
53
        GetUpdates(ctx context.Context) (*telegramUpdate, error)
54
        Avatar(ctx context.Context, userID int) (string, error)
55
        Send(ctx context.Context, id int, text string) error
56
        BotInfo(ctx context.Context) (*botInfo, error)
57
}
58

59
// changed in tests
60
var apiPollInterval = time.Second * 5        // interval to check updates from Telegram API and answer to users
61
var expiredCleanupInterval = time.Minute * 5 // interval to check and clean up expired notification requests
62

63
// Run starts processing login requests sent in Telegram
64
// Blocks caller
65
func (th *TelegramHandler) Run(ctx context.Context) error {
6✔
66
        // Initialization
6✔
67
        atomic.AddInt32(&th.run, 1)
6✔
68
        info, err := th.Telegram.BotInfo(ctx)
6✔
69
        if err != nil {
6✔
70
                return fmt.Errorf("failed to fetch bot info: %w", err)
×
71
        }
×
72
        th.username = info.Username
6✔
73

6✔
74
        th.requests.Lock()
6✔
75
        th.requests.data = make(map[string]tgAuthRequest)
6✔
76
        th.requests.Unlock()
6✔
77

6✔
78
        processUpdatedTicker := time.NewTicker(apiPollInterval)
6✔
79
        cleanupTicker := time.NewTicker(expiredCleanupInterval)
6✔
80

6✔
81
        for {
35✔
82
                select {
29✔
83
                case <-ctx.Done():
6✔
84
                        processUpdatedTicker.Stop()
6✔
85
                        cleanupTicker.Stop()
6✔
86
                        atomic.AddInt32(&th.run, -1)
6✔
87
                        return ctx.Err()
6✔
88
                case <-processUpdatedTicker.C:
20✔
89
                        updates, err := th.Telegram.GetUpdates(ctx)
20✔
90
                        if err != nil {
20✔
91
                                th.Logf("Error while getting telegram updates: %v", err)
×
92
                                continue
×
93
                        }
94
                        th.processUpdates(ctx, updates)
20✔
95
                case <-cleanupTicker.C:
3✔
96
                        now := time.Now()
3✔
97
                        th.requests.Lock()
3✔
98
                        for key, req := range th.requests.data {
4✔
99
                                if now.After(req.expires) {
2✔
100
                                        delete(th.requests.data, key)
1✔
101
                                }
1✔
102
                        }
103
                        th.requests.Unlock()
3✔
104
                }
105
        }
106
}
107

108
// telegramUpdate contains update information, which is used from whole telegram API response
109
type telegramUpdate struct {
110
        Result []struct {
111
                UpdateID int `json:"update_id"`
112
                Message  struct {
113
                        Chat struct {
114
                                ID   int    `json:"id"`
115
                                Name string `json:"first_name"`
116
                                Type string `json:"type"`
117
                        } `json:"chat"`
118
                        Text string `json:"text"`
119
                } `json:"message"`
120
        } `json:"result"`
121
}
122

123
// ProcessUpdate is alternative to Run, it processes provided plain text update from Telegram
124
// so that caller could get updates and send it not only there but to multiple sources
125
func (th *TelegramHandler) ProcessUpdate(ctx context.Context, textUpdate string) error {
4✔
126
        if atomic.LoadInt32(&th.run) != 0 {
5✔
127
                return fmt.Errorf("Run goroutine should not be used with ProcessUpdate")
1✔
128
        }
1✔
129
        defer func() {
6✔
130
                // as Run goroutine is not running, clean up old requests on each update
3✔
131
                // even if we hit json decode error
3✔
132
                now := time.Now()
3✔
133
                th.requests.Lock()
3✔
134
                for key, req := range th.requests.data {
7✔
135
                        if now.After(req.expires) {
6✔
136
                                delete(th.requests.data, key)
2✔
137
                        }
2✔
138
                }
139
                th.requests.Unlock()
3✔
140
        }()
141
        // initialize requests.data as usually it's initialized in Run
142
        th.requests.Lock()
3✔
143
        if th.requests.data == nil {
4✔
144
                th.requests.data = make(map[string]tgAuthRequest)
1✔
145
        }
1✔
146
        th.requests.Unlock()
3✔
147
        var updates telegramUpdate
3✔
148
        if err := json.Unmarshal([]byte(textUpdate), &updates); err != nil {
5✔
149
                return fmt.Errorf("failed to decode provided telegram update: %w", err)
2✔
150
        }
2✔
151
        th.processUpdates(ctx, &updates)
1✔
152
        return nil
1✔
153
}
154

155
// processUpdates processes a batch of updates from telegram servers
156
// Returns offset for subsequent calls
157
func (th *TelegramHandler) processUpdates(ctx context.Context, updates *telegramUpdate) {
21✔
158
        for _, update := range updates.Result {
24✔
159
                if update.Message.Chat.Type != "private" {
3✔
160
                        continue
×
161
                }
162

163
                if !strings.HasPrefix(update.Message.Text, "/start ") {
3✔
164
                        continue
×
165
                }
166

167
                token := strings.TrimPrefix(update.Message.Text, "/start ")
3✔
168

3✔
169
                th.requests.RLock()
3✔
170
                authRequest, ok := th.requests.data[token]
3✔
171
                if !ok { // No such token
3✔
172
                        th.requests.RUnlock()
×
173
                        err := th.Telegram.Send(ctx, update.Message.Chat.ID, th.ErrorMsg)
×
174
                        if err != nil {
×
175
                                th.Logf("failed to notify telegram peer: %v", err)
×
176
                        }
×
177
                        continue
×
178
                }
179
                th.requests.RUnlock()
3✔
180

3✔
181
                avatarURL, err := th.Telegram.Avatar(ctx, update.Message.Chat.ID)
3✔
182
                if err != nil {
3✔
183
                        th.Logf("failed to get user avatar: %v", err)
×
184
                        continue
×
185
                }
186

187
                id := th.ProviderName + "_" + authtoken.HashID(sha1.New(), fmt.Sprint(update.Message.Chat.ID))
3✔
188

3✔
189
                authRequest.confirmed = true
3✔
190
                authRequest.user = &authtoken.User{
3✔
191
                        ID:      id,
3✔
192
                        Name:    update.Message.Chat.Name,
3✔
193
                        Picture: avatarURL,
3✔
194
                }
3✔
195

3✔
196
                th.requests.Lock()
3✔
197
                th.requests.data[token] = authRequest
3✔
198
                th.requests.Unlock()
3✔
199

3✔
200
                err = th.Telegram.Send(ctx, update.Message.Chat.ID, th.SuccessMsg)
3✔
201
                if err != nil {
3✔
202
                        th.Logf("failed to notify telegram peer: %v", err)
×
203
                }
×
204
        }
205
}
206

207
// addToken adds token
208
func (th *TelegramHandler) addToken(token string, expires time.Time) error {
10✔
209
        th.requests.Lock()
10✔
210
        if th.requests.data == nil {
11✔
211
                th.requests.Unlock()
1✔
212
                return fmt.Errorf("run goroutine is not running")
1✔
213
        }
1✔
214
        th.requests.data[token] = tgAuthRequest{
9✔
215
                expires: expires,
9✔
216
        }
9✔
217
        th.requests.Unlock()
9✔
218
        return nil
9✔
219
}
220

221
// checkToken verifies incoming token, returns the user address if it's confirmed and empty string otherwise
222
func (th *TelegramHandler) checkToken(token string) (*authtoken.User, error) {
11✔
223
        th.requests.RLock()
11✔
224
        authRequest, ok := th.requests.data[token]
11✔
225
        th.requests.RUnlock()
11✔
226

11✔
227
        if !ok {
13✔
228
                return nil, fmt.Errorf("request is not found")
2✔
229
        }
2✔
230

231
        if time.Now().After(authRequest.expires) {
11✔
232
                th.requests.Lock()
2✔
233
                delete(th.requests.data, token)
2✔
234
                th.requests.Unlock()
2✔
235
                return nil, fmt.Errorf("request expired")
2✔
236
        }
2✔
237

238
        if !authRequest.confirmed {
11✔
239
                return nil, fmt.Errorf("request is not verified yet")
4✔
240
        }
4✔
241

242
        return authRequest.user, nil
3✔
243
}
244

245
// Name of the provider
246
func (th *TelegramHandler) Name() string { return th.ProviderName }
6✔
247

248
// String representation of the provider
249
func (th *TelegramHandler) String() string { return th.Name() }
1✔
250

251
// Default token lifetime. Changed in tests
252
var tgAuthRequestLifetime = time.Minute * 10
253

254
// LoginHandler generates and verifies login requests
255
func (th *TelegramHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
9✔
256
        queryToken := r.URL.Query().Get("token")
9✔
257
        if queryToken == "" {
13✔
258
                // GET /login (No token supplied)
4✔
259
                // Generate and send token
4✔
260
                token, err := randToken()
4✔
261
                if err != nil {
4✔
262
                        rest.SendErrorJSON(w, r, th.L, http.StatusInternalServerError, err, "failed to generate code")
×
263
                        return
×
264
                }
×
265

266
                err = th.addToken(token, time.Now().Add(tgAuthRequestLifetime))
4✔
267
                if err != nil {
5✔
268
                        rest.SendErrorJSON(w, r, th.L, http.StatusInternalServerError, err, "failed to process login request")
1✔
269
                        return
1✔
270
                }
1✔
271

272
                // verify that we have a username, which is not set if Run was not used
273
                if th.username == "" {
4✔
274
                        info, err := th.Telegram.BotInfo(r.Context())
1✔
275
                        if err != nil {
1✔
276
                                rest.SendErrorJSON(w, r, th.L, http.StatusInternalServerError, err, "failed to fetch bot username")
×
277
                                return
×
278
                        }
×
279
                        th.username = info.Username
1✔
280
                }
281

282
                rest.RenderJSON(w, struct {
3✔
283
                        Token string `json:"token"`
3✔
284
                        Bot   string `json:"bot"`
3✔
285
                }{token, th.username})
3✔
286

3✔
287
                return
3✔
288
        }
289

290
        // GET /login?token=blah
291
        authUser, err := th.checkToken(queryToken)
5✔
292
        if err != nil {
9✔
293
                rest.SendErrorJSON(w, r, nil, http.StatusNotFound, err, err.Error())
4✔
294
                return
4✔
295
        }
4✔
296

297
        u, err := setAvatar(th.AvatarSaver, *authUser, &http.Client{Timeout: 5 * time.Second})
1✔
298
        if err != nil {
1✔
299
                rest.SendErrorJSON(w, r, th.L, http.StatusInternalServerError, err, "failed to save avatar to proxy")
×
300
                return
×
301
        }
×
302

303
        claims := authtoken.Claims{
1✔
304
                User: &u,
1✔
305
                StandardClaims: jwt.StandardClaims{
1✔
306
                        Audience:  r.URL.Query().Get("site"),
1✔
307
                        Id:        queryToken,
1✔
308
                        Issuer:    th.ProviderName,
1✔
309
                        ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
1✔
310
                        NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
1✔
311
                },
1✔
312
                SessionOnly: false, // TODO review?
1✔
313
        }
1✔
314

1✔
315
        if _, err := th.TokenService.Set(w, claims); err != nil {
1✔
316
                rest.SendErrorJSON(w, r, th.L, http.StatusInternalServerError, err, "failed to set token")
×
317
                return
×
318
        }
×
319

320
        rest.RenderJSON(w, claims.User)
1✔
321

1✔
322
        // Delete request
1✔
323
        th.requests.Lock()
1✔
324
        defer th.requests.Unlock()
1✔
325
        delete(th.requests.data, queryToken)
1✔
326
}
327

328
// AuthHandler does nothing since we don't have any callbacks
329
func (th *TelegramHandler) AuthHandler(_ http.ResponseWriter, _ *http.Request) {}
×
330

331
// LogoutHandler - GET /logout
332
func (th *TelegramHandler) LogoutHandler(w http.ResponseWriter, _ *http.Request) {
1✔
333
        th.TokenService.Reset(w)
1✔
334
}
1✔
335

336
// tgAPI implements TelegramAPI
337
type tgAPI struct {
338
        logger.L
339
        token  string
340
        client *http.Client
341

342
        // Identifier of the first update to be requested.
343
        // Should be equal to LastSeenUpdateID + 1
344
        // See https://core.telegram.org/bots/api#getupdates
345
        updateOffset int
346
}
347

348
// NewTelegramAPI returns initialized TelegramAPI implementation
349
func NewTelegramAPI(token string, client *http.Client) TelegramAPI {
5✔
350
        return &tgAPI{
5✔
351
                client: client,
5✔
352
                token:  token,
5✔
353
        }
5✔
354
}
5✔
355

356
// GetUpdates fetches incoming updates
357
func (tg *tgAPI) GetUpdates(ctx context.Context) (*telegramUpdate, error) {
3✔
358
        url := `getUpdates?allowed_updates=["message"]`
3✔
359
        if tg.updateOffset != 0 {
4✔
360
                url += fmt.Sprintf("&offset=%d", tg.updateOffset)
1✔
361
        }
1✔
362

363
        var result telegramUpdate
3✔
364

3✔
365
        err := tg.request(ctx, url, &result)
3✔
366
        if err != nil {
4✔
367
                return nil, fmt.Errorf("failed to fetch updates: %w", err)
1✔
368
        }
1✔
369

370
        for _, u := range result.Result {
4✔
371
                if u.UpdateID >= tg.updateOffset {
3✔
372
                        tg.updateOffset = u.UpdateID + 1
1✔
373
                }
1✔
374
        }
375

376
        return &result, err
2✔
377
}
378

379
// Send sends a message to telegram peer
380
func (tg *tgAPI) Send(ctx context.Context, id int, msg string) error {
1✔
381
        url := fmt.Sprintf("sendMessage?chat_id=%d&text=%s", id, neturl.PathEscape(msg))
1✔
382
        return tg.request(ctx, url, &struct{}{})
1✔
383
}
1✔
384

385
// Avatar returns URL to user avatar
386
func (tg *tgAPI) Avatar(ctx context.Context, id int) (string, error) {
1✔
387
        // Get profile pictures
1✔
388
        url := fmt.Sprintf(`getUserProfilePhotos?user_id=%d`, id)
1✔
389

1✔
390
        var profilePhotos = struct {
1✔
391
                Result struct {
1✔
392
                        Photos [][]struct {
1✔
393
                                ID string `json:"file_id"`
1✔
394
                        } `json:"photos"`
1✔
395
                } `json:"result"`
1✔
396
        }{}
1✔
397

1✔
398
        if err := tg.request(ctx, url, &profilePhotos); err != nil {
1✔
399
                return "", err
×
400
        }
×
401

402
        // User does not have profile picture set or it is hidden in privacy settings
403
        if len(profilePhotos.Result.Photos) == 0 || len(profilePhotos.Result.Photos[0]) == 0 {
1✔
404
                return "", nil
×
405
        }
×
406

407
        // Get max possible picture size
408
        last := len(profilePhotos.Result.Photos[0]) - 1
1✔
409
        fileID := profilePhotos.Result.Photos[0][last].ID
1✔
410
        url = fmt.Sprintf(`getFile?file_id=%s`, fileID)
1✔
411

1✔
412
        var fileMetadata = struct {
1✔
413
                Result struct {
1✔
414
                        Path string `json:"file_path"`
1✔
415
                } `json:"result"`
1✔
416
        }{}
1✔
417

1✔
418
        if err := tg.request(ctx, url, &fileMetadata); err != nil {
1✔
419
                return "", err
×
420
        }
×
421

422
        avatarURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", tg.token, fileMetadata.Result.Path)
1✔
423

1✔
424
        return avatarURL, nil
1✔
425
}
426

427
// botInfo structure contains information about telegram bot, which is used from whole telegram API response
428
type botInfo struct {
429
        Username string `json:"username"`
430
}
431

432
// BotInfo returns info about configured bot
433
func (tg *tgAPI) BotInfo(ctx context.Context) (*botInfo, error) {
×
434
        var resp = struct {
×
435
                Result *botInfo `json:"result"`
×
436
        }{}
×
437

×
438
        err := tg.request(ctx, "getMe", &resp)
×
439
        if err != nil {
×
440
                return nil, err
×
441
        }
×
442
        if resp.Result == nil {
×
443
                return nil, fmt.Errorf("received empty result")
×
444
        }
×
445

446
        return resp.Result, nil
×
447
}
448

449
func (tg *tgAPI) request(ctx context.Context, method string, data interface{}) error {
6✔
450
        return repeater.NewDefault(3, time.Millisecond*50).Do(ctx, func() error {
14✔
451
                url := fmt.Sprintf("https://api.telegram.org/bot%s/%s", tg.token, method)
8✔
452

8✔
453
                req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody)
8✔
454
                if err != nil {
8✔
455
                        return fmt.Errorf("failed to create request: %w", err)
×
456
                }
×
457

458
                resp, err := tg.client.Do(req)
8✔
459
                if err != nil {
8✔
460
                        return fmt.Errorf("failed to send request: %w", err)
×
461
                }
×
462
                defer resp.Body.Close() //nolint gosec // we don't care about response body
8✔
463

8✔
464
                if resp.StatusCode != http.StatusOK {
11✔
465
                        return tg.parseError(resp.Body, resp.StatusCode)
3✔
466
                }
3✔
467

468
                if err = json.NewDecoder(resp.Body).Decode(data); err != nil {
5✔
469
                        return fmt.Errorf("failed to decode json response: %w", err)
×
470
                }
×
471

472
                return nil
5✔
473
        })
474
}
475

476
func (tg *tgAPI) parseError(r io.Reader, statusCode int) error {
3✔
477
        tgErr := struct {
3✔
478
                Description string `json:"description"`
3✔
479
        }{}
3✔
480
        if err := json.NewDecoder(r).Decode(&tgErr); err != nil {
3✔
481
                return fmt.Errorf("unexpected telegram API status code %d", statusCode)
×
482
        }
×
483
        return fmt.Errorf("unexpected telegram API status code %d, error: %q", statusCode, tgErr.Description)
3✔
484
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc