diff --git a/mtproto/consts.go b/mtproto/consts.go index a8330a7..1ce67e1 100644 --- a/mtproto/consts.go +++ b/mtproto/consts.go @@ -25,8 +25,4 @@ var ( HandshakeSenderPID = []byte("IPIPPRPDTIME") HandshakePeerPID = []byte("IPIPPRPDTIME") - - HandshakeRequest = append(TagHandshake, - append(HandshakeFlags, - append(HandshakeSenderPID, HandshakePeerPID...)...)...) ) diff --git a/mtproto/rpc/handshake_request.go b/mtproto/rpc/handshake_request.go new file mode 100644 index 0000000..48b02f4 --- /dev/null +++ b/mtproto/rpc/handshake_request.go @@ -0,0 +1,7 @@ +package rpc + +import "github.com/9seconds/mtg/mtproto" + +var HandshakeRequest = append(mtproto.TagHandshake, + append(mtproto.HandshakeFlags, + append(mtproto.HandshakeSenderPID, mtproto.HandshakePeerPID...)...)...) diff --git a/mtproto/rpc/handshake_response.go b/mtproto/rpc/handshake_response.go new file mode 100644 index 0000000..effe63f --- /dev/null +++ b/mtproto/rpc/handshake_response.go @@ -0,0 +1,55 @@ +package rpc + +import ( + "bytes" + "errors" + "fmt" + + "github.com/9seconds/mtg/mtproto" +) + +type HandshakeResponse struct { + Type []byte + Flags []byte + SenderPID []byte + PeerPID []byte +} + +// Bytes returns a serialized handshake response. +func (r *HandshakeResponse) Bytes() []byte { + buf := &bytes.Buffer{} + + buf.Write(r.Type) // nolint: gosec + buf.Write(r.Flags) // nolint: gosec + buf.Write(r.SenderPID) // nolint: gosec + buf.Write(r.PeerPID) // nolint: gosec + + return buf.Bytes() +} + +// Valid checks that handshake response compliments request. +func (r *HandshakeResponse) Valid() error { + if !bytes.Equal(r.Type, mtproto.TagHandshake) { + return errors.New("Unexpected handshake tag") + } + if !bytes.Equal(r.PeerPID, mtproto.HandshakeSenderPID) { + return errors.New("Incorrect sender PID") + } + + return nil +} + +// NewHandshakeResponse constructs new handshake response from the given +// data. +func NewHandshakeResponse(data []byte) (*HandshakeResponse, error) { + if len(data) != 32 { + return nil, fmt.Errorf("Incorrect handshake response length %d", len(data)) + } + + return &HandshakeResponse{ + Type: data[:4], + Flags: data[4:8], + SenderPID: data[8:20], + PeerPID: data[20:], + }, nil +} diff --git a/mtproto/rpc/nonce_request.go b/mtproto/rpc/nonce_request.go new file mode 100644 index 0000000..6d3b976 --- /dev/null +++ b/mtproto/rpc/nonce_request.go @@ -0,0 +1,51 @@ +package rpc + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "fmt" + "time" + + "github.com/9seconds/mtg/mtproto" +) + +type NonceRequest struct { + KeySelector []byte + CryptoTS []byte + Nonce []byte +} + +// Bytes returns serialized nonce request. +func (r *NonceRequest) Bytes() []byte { + buf := &bytes.Buffer{} + + buf.Write(mtproto.TagNonce) // nolint: gosec + buf.Write(r.KeySelector) // nolint: gosec + buf.Write(mtproto.NonceCryptoAES) // nolint: gosec + buf.Write(r.CryptoTS) // nolint: gosec + buf.Write(r.Nonce) // nolint: gosec + + return buf.Bytes() +} + +// NewNonceRequest builds new none request based on proxy secret. +func NewNonceRequest(proxySecret []byte) (*NonceRequest, error) { + nonce := make([]byte, 16) + keySelector := make([]byte, 4) + cryptoTS := make([]byte, 4) + + if _, err := rand.Read(nonce); err != nil { + return nil, fmt.Errorf("cannot generate nonce: %w", err) + } + copy(keySelector, proxySecret) + + timestamp := time.Now().Truncate(time.Second).Unix() % 4294967296 // 256 ^ 4 - do not know how to name + binary.LittleEndian.PutUint32(cryptoTS, uint32(timestamp)) + + return &NonceRequest{ + KeySelector: keySelector, + CryptoTS: cryptoTS, + Nonce: nonce, + }, nil +} diff --git a/mtproto/rpc/nonce_response.go b/mtproto/rpc/nonce_response.go new file mode 100644 index 0000000..5e78be5 --- /dev/null +++ b/mtproto/rpc/nonce_response.go @@ -0,0 +1,60 @@ +package rpc + +import ( + "bytes" + "errors" + "fmt" + + "github.com/9seconds/mtg/mtproto" +) + +type NonceResponse struct { + NonceRequest + + Type []byte + Crypto []byte +} + +// Bytes returns serialized form of the nonce response. +func (r *NonceResponse) Bytes() []byte { + buf := &bytes.Buffer{} + + buf.Write(r.Type) // nolint: gosec + buf.Write(r.KeySelector) // nolint: gosec + buf.Write(r.Crypto) // nolint: gosec + buf.Write(r.CryptoTS) // nolint: gosec + buf.Write(r.Nonce) // nolint: gosec + + return buf.Bytes() +} + +func (r *NonceResponse) Valid(req *NonceRequest) error { + if !bytes.Equal(r.Type, mtproto.TagNonce) { + return errors.New("Unexpected RPC type") + } + if !bytes.Equal(r.Crypto, mtproto.NonceCryptoAES) { + return errors.New("Unexpected crypto type") + } + if !bytes.Equal(r.KeySelector, req.KeySelector) { + return errors.New("Unexpected key selector") + } + + return nil +} + +// NewNonceResponse build new nonce response based on the given data. +func NewNonceResponse(data []byte) (*NonceResponse, error) { + if len(data) != 32 { + return nil, fmt.Errorf("Unexpected message length %d", len(data)) + } + + return &NonceResponse{ + NonceRequest: NonceRequest{ + KeySelector: data[4:8], + CryptoTS: data[12:16], + Nonce: data[16:], + }, + Type: data[:4], + Crypto: data[8:12], + }, nil +} diff --git a/mtproto/proxy_flags.go b/mtproto/rpc/proxy_flags.go similarity index 98% rename from mtproto/proxy_flags.go rename to mtproto/rpc/proxy_flags.go index bd7cd69..c59d33f 100644 --- a/mtproto/proxy_flags.go +++ b/mtproto/rpc/proxy_flags.go @@ -1,4 +1,4 @@ -package mtproto +package rpc import ( "encoding/binary" diff --git a/telegram/middle.go b/telegram/middle.go index 3ffc3b1..fdc06e0 100644 --- a/telegram/middle.go +++ b/telegram/middle.go @@ -3,13 +3,19 @@ package telegram import ( "context" "fmt" + "net" "sync" + "time" + + "go.uber.org/zap" "github.com/9seconds/mtg/conntypes" "github.com/9seconds/mtg/telegram/api" "github.com/9seconds/mtg/wrappers" ) +const middleTelegramBackgroundUpdateEvery = time.Hour + type middleTelegram struct { baseTelegram @@ -44,6 +50,15 @@ func (m *middleTelegram) update() error { return nil } +func (m *middleTelegram) backgroundUpdate() { + logger := zap.S().Named("telegram") + for range time.Tick(middleTelegramBackgroundUpdateEvery) { + if err := m.update(); err != nil { + logger.Warnw("Cannot update Telegram proxies", "error", err) + } + } +} + func (m *middleTelegram) Dial(ctx context.Context, cancel context.CancelFunc, dc conntypes.DC, @@ -52,5 +67,22 @@ func (m *middleTelegram) Dial(ctx context.Context, dc = conntypes.DCDefaultIdx } + m.mutex.RLock() + defer m.mutex.RUnlock() + return m.baseTelegram.dial(ctx, cancel, dc, protocol) } + +func NewMiddleTelegram() Telegram { + tg := &middleTelegram{ + baseTelegram: baseTelegram{ + dialer: net.Dialer{Timeout: telegramDialTimeout}, + }, + } + if err := tg.update(); err != nil { + panic(err) + } + go tg.backgroundUpdate() + + return tg +}