package inbound import ( "bytes" "context" "crypto/md5" "encoding/base64" "encoding/json" "fmt" "log" "net" "net/http" "strings" "time" cmpp "github.com/bigwhite/gocmpp" cmpputils "github.com/bigwhite/gocmpp/utils" ) const defaultHTTPTimeout = 10 * time.Second type Server struct { Addr string APIBaseURL string HTTPClient *http.Client } type authRequest struct { Account string `json:"account"` AuthSource string `json:"authSource"` Timestamp uint32 `json:"timestamp"` RemoteIP string `json:"remoteIp,omitempty"` } type submitRequest struct { Account string `json:"account"` PhoneNumber string `json:"phoneNumber"` Content string `json:"content"` SrcID string `json:"srcId,omitempty"` DestID string `json:"destId,omitempty"` SequenceID uint32 `json:"sequenceId,omitempty"` RemoteIP string `json:"remoteIp,omitempty"` } type submitResponse struct { Accepted bool `json:"accepted"` MessageID string `json:"messageId"` } type authResponse struct { PasswordCipher string `json:"passwordCipher"` } func (s Server) ListenAndServe() error { addr := s.Addr if addr == "" { addr = ":17890" } return cmpp.ListenAndServe(addr, cmpp.V30, 30*time.Second, 3, nil, cmpp.HandlerFunc(s.handleLogin), cmpp.HandlerFunc(s.handleSubmit), ) } 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 } resp := response.Packer.(*cmpp.Cmpp3ConnRspPkt) resp.Version = 0x30 account := strings.TrimRight(req.SrcAddr, "\x00") if account == "" { resp.Status = uint32(cmpp.ErrnoConnInvalidSrcAddr) return false, cmpp.ConnRspStatusErrMap[cmpp.ErrnoConnInvalidSrcAddr] } auth, err := s.authenticate(packet.Conn.Conn.RemoteAddr(), account, req.AuthSrc, req.Timestamp) if err != nil { logger.Printf("cmpp inbound auth failed account=%s remote=%s err=%v", account, packet.Conn.Conn.RemoteAddr(), err) resp.Status = uint32(cmpp.ErrnoConnAuthFailed) return false, cmpp.ConnRspStatusErrMap[cmpp.ErrnoConnAuthFailed] } authSource := []byte(req.AuthSrc) authISMG := md5.Sum(bytes.Join([][]byte{{byte(resp.Status)}, authSource, []byte(auth.PasswordCipher)}, nil)) resp.AuthIsmg = string(authISMG[:]) logger.Printf("cmpp inbound account=%s login ok remote=%s", account, packet.Conn.Conn.RemoteAddr()) return false, nil } func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logger *log.Logger) (bool, error) { req, ok := packet.Packer.(*cmpp.Cmpp3SubmitReqPkt) if !ok { return true, nil } resp := response.Packer.(*cmpp.Cmpp3SubmitRspPkt) account := strings.TrimRight(req.MsgSrc, "\x00") phone := "" if len(req.DestTerminalId) > 0 { phone = strings.TrimRight(req.DestTerminalId[0], "\x00") } content, err := decodeContent(req.MsgFmt, req.MsgContent) if err != nil { logger.Printf("cmpp inbound decode submit failed account=%s seq=%d err=%v", account, req.SeqId, err) resp.Result = 9 return false, nil } result, err := s.submit(packet.Conn.Conn.RemoteAddr(), submitRequest{ Account: account, PhoneNumber: phone, Content: content, SrcID: req.SrcId, DestID: phone, SequenceID: req.SeqId, RemoteIP: remoteIP(packet.Conn.Conn.RemoteAddr()), }) if err != nil || !result.Accepted { logger.Printf("cmpp inbound submit rejected account=%s phone=%s seq=%d err=%v", account, phone, req.SeqId, err) resp.Result = 9 return false, nil } resp.MsgId = messageIDFrom(result.MessageID, req.SeqId) resp.Result = 0 return false, nil } func (s Server) authenticate(remote net.Addr, account string, authSource string, timestamp uint32) (authResponse, error) { payload := authRequest{ Account: account, AuthSource: base64.StdEncoding.EncodeToString([]byte(authSource)), Timestamp: timestamp, RemoteIP: remoteIP(remote), } var result authResponse err := s.post(context.Background(), "/gateway/events/inbound/authenticate", payload, &result) return result, err } func (s Server) submit(remote net.Addr, payload submitRequest) (submitResponse, error) { payload.RemoteIP = remoteIP(remote) var result submitResponse err := s.post(context.Background(), "/gateway/events/inbound/submit", payload, &result) return result, err } func (s Server) post(ctx context.Context, path string, payload any, result any) error { client := s.HTTPClient if client == nil { client = &http.Client{Timeout: defaultHTTPTimeout} } body, err := json.Marshal(payload) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(apiBaseURL(s.APIBaseURL), "/")+path, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("api returned %s", resp.Status) } if result != nil { return json.NewDecoder(resp.Body).Decode(result) } return nil } func decodeContent(format uint8, content string) (string, error) { switch format { case 8: return cmpputils.Ucs2ToUtf8(content) case 15: return cmpputils.GB18030ToUtf8(content) default: return content, nil } } func apiBaseURL(value string) string { if value == "" { return "http://127.0.0.1:3000/api" } return value } func remoteIP(addr net.Addr) string { if tcp, ok := addr.(*net.TCPAddr); ok { return tcp.IP.String() } host, _, err := net.SplitHostPort(addr.String()) if err == nil { return host } return addr.String() } func messageIDFrom(value string, seq uint32) uint64 { hash := md5.Sum([]byte(value)) result := uint64(seq) for _, item := range hash[:6] { result = (result << 8) + uint64(item) } if result == 0 { return uint64(time.Now().UnixNano()) } return result }