support one-time password

This commit is contained in:
c9s 2020-12-11 17:07:19 +08:00
parent f4ef19e5d6
commit deb9a29521
2 changed files with 148 additions and 43 deletions

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"image/png"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
@ -15,6 +16,7 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/pquerna/otp"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
@ -137,14 +139,8 @@ func runConfig(basectx context.Context, userConfig *bbgo.Config) error {
// for telegram // for telegram
telegramBotToken := viper.GetString("telegram-bot-token") telegramBotToken := viper.GetString("telegram-bot-token")
telegramBotAuthToken := viper.GetString("telegram-bot-auth-token") telegramBotAuthToken := viper.GetString("telegram-bot-auth-token")
if len(telegramBotToken) > 0 && len(telegramBotAuthToken) > 0 { if len(telegramBotToken) > 0 {
log.Infof("setting up telegram notifier...") log.Infof("initializing telegram bot...")
key, err := service.NewDefaultTotpKey()
if err != nil {
return errors.Wrapf(err, "failed to setup totp (time-based one time password) key")
}
_ = key
bot, err := tb.NewBot(tb.Settings{ bot, err := tb.NewBot(tb.Settings{
// You can also set custom API URL. // You can also set custom API URL.
@ -158,25 +154,67 @@ func runConfig(basectx context.Context, userConfig *bbgo.Config) error {
return err return err
} }
var store = bbgo.NewMemoryService().NewStore("bbgo", "telegram") var persistence bbgo.PersistenceService = bbgo.NewMemoryService()
if environ.PersistenceServiceFacade != nil { var sessionStore = persistence.NewStore("bbgo", "telegram")
tt := strings.Split(bot.Token, ":") tt := strings.Split(bot.Token, ":")
telegramID := tt[0] telegramID := tt[0]
if environ.PersistenceServiceFacade != nil {
if environ.PersistenceServiceFacade.Redis != nil { if environ.PersistenceServiceFacade.Redis != nil {
store = environ.PersistenceServiceFacade.Redis.NewStore("bbgo", "telegram", telegramID) persistence = environ.PersistenceServiceFacade.Redis
sessionStore = persistence.NewStore("bbgo", "telegram", telegramID)
} }
} }
interaction := telegramnotifier.NewInteraction(bot, store) interaction := telegramnotifier.NewInteraction(bot, sessionStore)
if len(telegramBotAuthToken) > 0 {
log.Infof("telegram bot auth token is set, using fixed token for authorization...")
interaction.SetAuthToken(telegramBotAuthToken) interaction.SetAuthToken(telegramBotAuthToken)
go interaction.Start() log.Infof("send the following command to the bbgo bot you created to enable the notification")
log.Infof("")
log.Infof("send the following command to the bbgo bot you created to enable the notification...")
log.Infof("===========================================")
log.Infof("") log.Infof("")
log.Infof(" /auth %s", telegramBotAuthToken) log.Infof(" /auth %s", telegramBotAuthToken)
log.Infof("") log.Infof("")
log.Infof("===========================================") log.Infof("")
}
var session telegramnotifier.Session
if err := sessionStore.Load(&session); err != nil || session.Owner == nil {
log.Warnf("session not found, generating new one-time password key for new session...")
key, err := service.NewDefaultTotpKey()
if err != nil {
return errors.Wrapf(err, "failed to setup totp (time-based one time password) key")
}
displayOTPKey(key)
qrcodeImagePath := fmt.Sprintf("otp-%s.png", telegramID)
err = writeOTPKeyAsQRCodePNG(key, qrcodeImagePath)
log.Infof("To scan your OTP QR code, please run the following command:")
log.Infof("")
log.Infof("")
log.Infof(" open %s", qrcodeImagePath)
log.Infof("")
log.Infof("")
log.Infof("send the auth command with the generated one-time password to the bbgo bot you created to enable the notification")
log.Infof("")
log.Infof("")
log.Infof(" /auth {code}")
log.Infof("")
log.Infof("")
session = telegramnotifier.NewSession(key)
if err := sessionStore.Save(&session); err != nil {
return errors.Wrap(err, "failed to save session")
}
}
go interaction.Start(session)
var notifier = telegramnotifier.New(interaction) var notifier = telegramnotifier.New(interaction)
notification.AddNotifier(notifier) notification.AddNotifier(notifier)
} }
@ -391,3 +429,34 @@ func buildAndRun(ctx context.Context, userConfig *bbgo.Config, goOS, goArch stri
runCmd.Stderr = os.Stderr runCmd.Stderr = os.Stderr
return runCmd, runCmd.Start() return runCmd, runCmd.Start()
} }
func writeOTPKeyAsQRCodePNG(key *otp.Key, imagePath string) error {
// Convert TOTP key into a PNG
var buf bytes.Buffer
img, err := key.Image(512, 512)
if err != nil {
return err
}
if err := png.Encode(&buf, img); err != nil {
return err
}
if err := ioutil.WriteFile(imagePath, buf.Bytes(), 0644); err != nil {
return err
}
return nil
}
func displayOTPKey(key *otp.Key) {
log.Infof("")
log.Infof("====================PLEASE STORE YOUR OTP KEY=======================")
log.Infof("")
log.Infof("Issuer: %s", key.Issuer())
log.Infof("AccountName: %s", key.AccountName())
log.Infof("Secret: %s", key.Secret())
log.Infof("")
log.Infof("====================================================================")
log.Infof("")
}

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/pquerna/otp" "github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gopkg.in/tucnak/telebot.v2" "gopkg.in/tucnak/telebot.v2"
@ -12,15 +13,27 @@ import (
var log = logrus.WithField("service", "telegram") var log = logrus.WithField("service", "telegram")
type Session struct {
Owner *telebot.User `json:"owner"`
OneTimePasswordKey *otp.Key `json:"otpKey"`
}
func NewSession(key *otp.Key) Session {
return Session{
Owner: nil,
OneTimePasswordKey: key,
}
}
//go:generate callbackgen -type Interaction //go:generate callbackgen -type Interaction
type Interaction struct { type Interaction struct {
store bbgo.Store store bbgo.Store
bot *telebot.Bot bot *telebot.Bot
AuthToken string AuthToken string
OneTimePasswordKey *otp.Key
Owner *telebot.User session *Session
StartCallbacks []func() StartCallbacks []func()
AuthCallbacks []func(user *telebot.User) AuthCallbacks []func(user *telebot.User)
@ -42,18 +55,22 @@ func (it *Interaction) SetAuthToken(token string) {
it.AuthToken = token it.AuthToken = token
} }
func (it *Interaction) Session() *Session {
return it.session
}
func (it *Interaction) HandleInfo(m *telebot.Message) { func (it *Interaction) HandleInfo(m *telebot.Message) {
if it.Owner == nil { if it.session.Owner == nil {
return return
} }
if m.Sender.ID != it.Owner.ID { if m.Sender.ID != it.session.Owner.ID {
log.Warningf("incorrect user tried to access bot! sender: %+v", m.Sender) log.Warningf("incorrect user tried to access bot! sender: %+v", m.Sender)
} else { } else {
if _, err := it.bot.Send(it.Owner, if _, err := it.bot.Send(it.session.Owner,
fmt.Sprintf("Welcome! your username: %s, user ID: %d", fmt.Sprintf("Welcome! your username: %s, user ID: %d",
it.Owner.Username, it.session.Owner.Username,
it.Owner.ID, it.session.Owner.ID,
)); err != nil { )); err != nil {
log.WithError(err).Error("failed to send telegram message") log.WithError(err).Error("failed to send telegram message")
} }
@ -61,11 +78,11 @@ func (it *Interaction) HandleInfo(m *telebot.Message) {
} }
func (it *Interaction) SendToOwner(message string) { func (it *Interaction) SendToOwner(message string) {
if it.Owner == nil { if it.session.Owner == nil {
return return
} }
if _, err := it.bot.Send(it.Owner, message); err != nil { if _, err := it.bot.Send(it.session.Owner, message); err != nil {
log.WithError(err).Error("failed to send message to the owner") log.WithError(err).Error("failed to send message to the owner")
} }
} }
@ -73,7 +90,7 @@ func (it *Interaction) SendToOwner(message string) {
func (it *Interaction) HandleHelp(m *telebot.Message) { func (it *Interaction) HandleHelp(m *telebot.Message) {
message := ` message := `
help - show this help message help - show this help message
auth - authorize current telegram user to access telegram bot with authToken. ex. /auth my-token auth - authorize current telegram user to access telegram bot with authentication token or one-time password. ex. /auth my-token
info - show information about current chat info - show information about current chat
` `
if _, err := it.bot.Send(m.Sender, message); err != nil { if _, err := it.bot.Send(m.Sender, message); err != nil {
@ -82,17 +99,39 @@ info - show information about current chat
} }
func (it *Interaction) HandleAuth(m *telebot.Message) { func (it *Interaction) HandleAuth(m *telebot.Message) {
if m.Payload == it.AuthToken { if len(it.AuthToken) > 0 && m.Payload == it.AuthToken {
it.Owner = m.Sender it.session.Owner = m.Sender
if _, err := it.bot.Send(m.Sender, fmt.Sprintf("Hi %s, I know you, I will send you the notifications!", m.Sender.Username)); err != nil { if _, err := it.bot.Send(m.Sender, fmt.Sprintf("Hi %s, I know you, I will send you the notifications!", m.Sender.Username)); err != nil {
log.WithError(err).Error("telegram send error") log.WithError(err).Error("telegram send error")
} }
if err := it.store.Save(it.Owner); err != nil { if err := it.store.Save(it.session); err != nil {
log.WithError(err).Error("can not persist telegram chat user") log.WithError(err).Error("can not persist telegram chat user")
} }
it.EmitAuth(m.Sender) it.EmitAuth(m.Sender)
} else if it.session != nil && it.session.OneTimePasswordKey != nil {
if totp.Validate(m.Payload, it.session.OneTimePasswordKey.Secret()) {
it.session.Owner = m.Sender
if _, err := it.bot.Send(m.Sender, fmt.Sprintf("Hi %s, I know you, I will send you the notifications!", m.Sender.Username)); err != nil {
log.WithError(err).Error("telegram send error")
}
if err := it.store.Save(it.session); err != nil {
log.WithError(err).Error("can not persist telegram chat user")
}
it.EmitAuth(m.Sender)
} else {
if _, err := it.bot.Send(m.Sender, "Authorization failed. please check your auth token"); err != nil {
log.WithError(err).Error("telegram send error")
}
}
} else { } else {
if _, err := it.bot.Send(m.Sender, "Authorization failed. please check your auth token"); err != nil { if _, err := it.bot.Send(m.Sender, "Authorization failed. please check your auth token"); err != nil {
log.WithError(err).Error("telegram send error") log.WithError(err).Error("telegram send error")
@ -100,16 +139,13 @@ func (it *Interaction) HandleAuth(m *telebot.Message) {
} }
} }
func (it *Interaction) Start() { func (it *Interaction) Start(session Session) {
// load user data from persistence layer it.session = &session
var owner telebot.User
if err := it.store.Load(&owner); err == nil { if it.session.Owner != nil {
if _, err := it.bot.Send(it.Owner, fmt.Sprintf("Hi %s, I'm back", it.Owner.Username)); err != nil { if _, err := it.bot.Send(it.session.Owner, fmt.Sprintf("Hi %s, I'm back", it.session.Owner.Username)); err != nil {
log.WithError(err).Error("failed to send telegram message") log.WithError(err).Error("failed to send telegram message")
} }
it.Owner = &owner
} }
it.bot.Start() it.bot.Start()