Files

153 lines
5.7 KiB
Go

package inbound
import (
"context"
"encoding/base64"
"fmt"
cmpp "github.com/bigwhite/gocmpp"
"log"
"net"
"strings"
"sync"
"sync/atomic"
"time"
)
type authRequest struct {
Account string `json:"account"`
AuthSource string `json:"authSource"`
Timestamp uint32 `json:"timestamp"`
RemoteIP string `json:"remoteIp,omitempty"`
Version string `json:"version"`
RequestedVersion uint8 `json:"requestedVersion"`
}
type authResponse struct {
PasswordCipher string `json:"passwordCipher"`
ApplicationID string `json:"applicationId"`
TenantID string `json:"tenantId"`
Account string `json:"account"`
EnterpriseCode string `json:"enterpriseCode"`
MaxConnections int `json:"maxConnections"`
WindowSize int `json:"windowSize"`
}
func (s Server) handleLogin(response *cmpp.Response, packet *cmpp.Packet, logger *log.Logger) (bool, error) {
req, ok := packet.Packer.(*cmpp.CmppConnReqPkt)
if !ok {
return true, nil
}
account := strings.TrimRight(req.SrcAddr, "\x00")
if account == "" {
go s.reportSecurityEvent("cmpp_protocol_abuse", remoteIP(packet.Conn.Conn.RemoteAddr()), "EMPTY_SOURCE_ADDRESS", cmppVersionName(req.Version))
setInboundConnectResponse(response.Packer, cmpp.ErrnoConnInvalidSrcAddr, req.AuthSrc, "", req.Version)
return false, cmpp.ConnRspStatusErrMap[cmpp.ErrnoConnInvalidSrcAddr]
}
if req.Version != cmpp.V20 && req.Version != cmpp.V21 && req.Version != cmpp.V30 {
go s.reportSecurityEvent("cmpp_protocol_abuse", remoteIP(packet.Conn.Conn.RemoteAddr()), "UNSUPPORTED_VERSION", cmppVersionName(req.Version))
setInboundConnectResponse(response.Packer, cmpp.ErrnoConnVerTooHigh, req.AuthSrc, "", cmpp.V30)
return false, cmpp.ConnRspStatusErrMap[cmpp.ErrnoConnVerTooHigh]
}
auth, err := s.authenticate(packet.Conn.Conn.RemoteAddr(), account, req.AuthSrc, req.Timestamp, req.Version)
if err != nil {
logger.Printf("cmpp inbound auth failed account=%s remote=%s err=%v", account, packet.Conn.Conn.RemoteAddr(), err)
setInboundConnectResponse(response.Packer, cmpp.ErrnoConnAuthFailed, req.AuthSrc, "", req.Version)
return false, cmpp.ConnRspStatusErrMap[cmpp.ErrnoConnAuthFailed]
}
now := time.Now().UTC()
session := &downstreamSession{
account: strings.TrimSpace(defaultString(auth.Account, account)),
tenantID: strings.TrimSpace(auth.TenantID),
applicationID: strings.TrimSpace(auth.ApplicationID),
enterpriseCode: strings.TrimSpace(auth.EnterpriseCode),
protocol: cmppVersionName(req.Version),
windowSize: boundedSubmitWindow(auth.WindowSize, s.MaxSubmitConcurrency),
submitInFlight: &atomic.Int64{},
srcID: strings.TrimSpace(auth.Account),
remoteIP: remoteIP(packet.Conn.Conn.RemoteAddr()),
connectedAt: now,
connectionID: fmt.Sprintf("%s-%d", s.gatewayInstanceID(), now.UnixNano()),
conn: packet.Conn,
mu: &sync.Mutex{},
presence: s.PresenceStore,
instanceID: s.gatewayInstanceID(),
report: s.reportConnection,
deliveryReport: s.reportDownstreamDelivery,
protocolLog: s.emitProtocolLog,
}
if !rememberAccount(session, auth.MaxConnections) {
logger.Printf("cmpp inbound auth failed account=%s remote=%s err=connection limit exceeded max=%d", account, packet.Conn.Conn.RemoteAddr(), auth.MaxConnections)
setInboundConnectResponse(response.Packer, cmpp.ErrnoConnOthers, req.AuthSrc, "", req.Version)
return false, cmpp.ConnRspStatusErrMap[cmpp.ErrnoConnOthers]
}
setInboundConnectResponse(response.Packer, 0, req.AuthSrc, auth.PasswordCipher, req.Version)
go s.reportConnectionOrDisconnect(session, "connected", "")
response.AfterSend = func(sendErr error) {
if sendErr == nil {
go s.flushPending(defaultString(auth.Account, account), logger)
}
}
logger.Printf(
"cmpp inbound event=login_accepted protocol=%s requested_version=0x%02x response_version=0x%02x account=%s remote=%s",
cmppVersionName(req.Version), uint8(req.Version), uint8(req.Version), account, packet.Conn.Conn.RemoteAddr(),
)
return false, nil
}
func setInboundConnectResponse(packet any, status uint8, authSource string, secret string, version cmpp.Type) {
switch resp := packet.(type) {
case *cmpp.Cmpp2ConnRspPkt:
resp.Status = status
resp.AuthSrc = authSource
resp.Secret = secret
resp.Version = version
case *cmpp.Cmpp3ConnRspPkt:
resp.Status = uint32(status)
resp.AuthSrc = authSource
resp.Secret = secret
resp.Version = version
}
}
func cmppVersionName(version cmpp.Type) string {
switch version {
case cmpp.V20:
return "cmpp20"
case cmpp.V21:
return "cmpp21"
case cmpp.V30:
return "cmpp30"
default:
return fmt.Sprintf("unknown_0x%02x", uint8(version))
}
}
func (s Server) authenticate(remote net.Addr, account string, authSource string, timestamp uint32, version cmpp.Type) (authResponse, error) {
payload := authRequest{
Account: account,
AuthSource: base64.StdEncoding.EncodeToString([]byte(authSource)),
Timestamp: timestamp,
RemoteIP: remoteIP(remote),
Version: cmppVersionName(version),
RequestedVersion: uint8(version),
}
var result authResponse
err := s.post(context.Background(), "/gateway/events/inbound/authenticate", payload, &result)
return result, err
}
func (s Server) reportSecurityEvent(ruleCode, sourceIP, resultCode, protocol string) {
if net.ParseIP(sourceIP) == nil {
return
}
payload := map[string]any{
"ruleCode": ruleCode, "sourceIp": sourceIP, "resultCode": resultCode, "protocol": protocol,
}
var result struct {
Accepted bool `json:"accepted"`
}
if err := s.post(context.Background(), "/gateway/events/security-detection", payload, &result); err != nil {
log.Printf("security event report failed rule=%s remote=%s err=%v", ruleCode, sourceIP, err)
}
}