You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
220 lines
5.6 KiB
220 lines
5.6 KiB
package proto |
|
|
|
import ( |
|
"fmt" |
|
"sync" |
|
"time" |
|
) |
|
|
|
// Message identifiers are coupled to message creation time. |
|
// |
|
// https://core.telegram.org/mtproto/description#message-identifier-msg-id |
|
|
|
const ( |
|
yieldClient = 0 |
|
yieldServerResponse = 1 |
|
yieldFromServer = 3 |
|
|
|
messageIDModulo = 4 |
|
) |
|
|
|
func newMessageID(nowNano int64, yield int) int64 { |
|
const nano = 1e9 |
|
// Must approximately equal unixtime*2^32. |
|
|
|
// Important: to counter replay-attacks the lower 32 bits of msg_id |
|
// passed by the client must not be empty and must present a |
|
// fractional part of the time point when the message was created. |
|
intPart := nowNano / nano |
|
fracPart := nowNano % nano |
|
|
|
// Ensure that fracPart % 4 == 0. |
|
fracPart &= -messageIDModulo |
|
// Adding modulo 4 yield to ensure message type. |
|
fracPart += int64(yield) |
|
|
|
return (intPart << 32) | fracPart |
|
} |
|
|
|
// MessageID represents 64-bit message id. |
|
type MessageID int64 |
|
|
|
func (id MessageID) String() string { |
|
return fmt.Sprintf("%x (%s, %s)", |
|
int64(id), id.Type(), id.Time().Format(time.RFC3339), |
|
) |
|
} |
|
|
|
// MessageType is type of message determined by message id. |
|
// |
|
// A message is rejected over 300 seconds after it is created or |
|
// 30 seconds before it is created (this is needed to protect from replay attacks). |
|
// |
|
// The identifier of a message container must be strictly greater than those of |
|
// its nested messages. |
|
type MessageType byte |
|
|
|
const ( |
|
// MessageUnknown reports that message id has unknown time and probably |
|
// should be ignored. |
|
MessageUnknown MessageType = iota |
|
// MessageFromClient is client message identifiers. |
|
MessageFromClient |
|
// MessageServerResponse is a response to a client message. |
|
MessageServerResponse |
|
// MessageFromServer is a message from the server. |
|
MessageFromServer |
|
) |
|
|
|
func (m MessageType) String() string { |
|
switch m { |
|
case MessageFromClient: |
|
return "FromClient" |
|
case MessageServerResponse: |
|
return "ServerResponse" |
|
case MessageFromServer: |
|
return "FromServer" |
|
default: |
|
return "Unknown" |
|
} |
|
} |
|
|
|
// Time returns approximate time when MessageID were generated. |
|
func (id MessageID) Time() time.Time { |
|
intPart := int64(id) >> 32 |
|
fracPart := int64(int32(id)) |
|
return time.Unix(intPart, fracPart).UTC() |
|
} |
|
|
|
// Type returns message type. |
|
func (id MessageID) Type() MessageType { |
|
switch id % messageIDModulo { |
|
case yieldClient: |
|
return MessageFromClient |
|
case yieldServerResponse: |
|
return MessageServerResponse |
|
case yieldFromServer: |
|
return MessageFromServer |
|
default: |
|
return MessageUnknown |
|
} |
|
} |
|
|
|
// NewMessageID returns new message id for provided time and type. |
|
func NewMessageID(now time.Time, typ MessageType) MessageID { |
|
return NewMessageIDNano(now.UnixNano(), typ) |
|
} |
|
|
|
// NewMessageIDNano returns new message id for provided current unix |
|
// nanoseconds and type. |
|
func NewMessageIDNano(nano int64, typ MessageType) MessageID { |
|
var yield int |
|
switch typ { |
|
case MessageFromClient: |
|
yield = yieldClient |
|
case MessageFromServer: |
|
yield = yieldFromServer |
|
case MessageServerResponse: |
|
yield = yieldServerResponse |
|
default: |
|
yield = yieldClient |
|
} |
|
return MessageID(newMessageID(nano, yield)) |
|
} |
|
|
|
// MessageIDGen is message id generator that provides collision prevention. |
|
// |
|
// The main reason of such structure is that now() can return same time during |
|
// multiple calls and that leads to duplicate message id. |
|
type MessageIDGen struct { |
|
mux sync.Mutex |
|
nano int64 |
|
now func() time.Time |
|
} |
|
|
|
// New generates new message id for provided type, protecting from collisions |
|
// that are caused by low system time resolution. |
|
func (g *MessageIDGen) New(t MessageType) int64 { |
|
g.mux.Lock() |
|
defer g.mux.Unlock() |
|
|
|
// Minimum resolution is required because id is only approximately |
|
// equal to unix nano time, some part is replaced by message type. |
|
const minResolutionNanos = 10 |
|
|
|
nano := g.now().UnixNano() |
|
if nano > g.nano { |
|
g.nano = nano |
|
} else { |
|
g.nano += minResolutionNanos |
|
} |
|
|
|
return int64(NewMessageIDNano(g.nano, t)) |
|
} |
|
|
|
// NewMessageIDGen creates new message id generator. |
|
// |
|
// Current time will be provided by now() function. |
|
// |
|
// This generator compensates time resolution problem removing |
|
// probability of id collision. |
|
// |
|
// Such problem can be observed for relatively high RPS, sequential calls to |
|
// time.Now() will return same time which leads to equal ids. |
|
func NewMessageIDGen(now func() time.Time) *MessageIDGen { |
|
return &MessageIDGen{ |
|
now: now, |
|
} |
|
} |
|
|
|
// MessageIDBuf stores last N message ids and is used in replay attack mitigation. |
|
type MessageIDBuf struct { |
|
mux sync.Mutex |
|
buf []int64 |
|
} |
|
|
|
// NewMessageIDBuf initializes new message id buffer for last N stored values. |
|
func NewMessageIDBuf(n int) *MessageIDBuf { |
|
return &MessageIDBuf{ |
|
buf: make([]int64, n), |
|
} |
|
} |
|
|
|
// Consume returns false if message should be discarded. |
|
func (b *MessageIDBuf) Consume(newID int64) bool { |
|
// In addition, the identifiers (msg_id) of the last N messages received |
|
// from the other side must be stored, and if a message comes in with an |
|
// msg_id lower than all or equal to any of the stored values, that message |
|
// is to be ignored. Otherwise, the new message msg_id is added to the set, |
|
// and, if the number of stored msg_id values is greater than N, the oldest |
|
// (i. e. the lowest) is discarded. |
|
// |
|
// https://core.telegram.org/mtproto/security_guidelines#checking-msg-id |
|
|
|
b.mux.Lock() |
|
defer b.mux.Unlock() |
|
|
|
var ( |
|
minIDx int |
|
minID int64 |
|
) |
|
for i, id := range b.buf { |
|
if id == newID { |
|
// Equal to stored value. |
|
return false |
|
} |
|
// Searching for minimum value. |
|
if id < minID { |
|
minIDx = i |
|
minID = id |
|
} |
|
} |
|
if newID < minID { |
|
// Lower than all stored values. |
|
return false |
|
} |
|
|
|
// Message is accepted. Replacing lowest message id with new id. |
|
b.buf[minIDx] = newID |
|
return true |
|
}
|
|
|