H-162: separate SWT and XST site identifiers (#40)
Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
@@ -47,6 +47,9 @@ func TestManagerRestoresOneSupervisorAndDoesNotLogoutOnShutdown(t *testing.T) {
|
||||
if heartbeat.cursor != (swt.Cursor{MaxWordID: 42, MaxOTick: 9, MaxTmpID: 7}) {
|
||||
t.Fatalf("restored cursor = %#v", heartbeat.cursor)
|
||||
}
|
||||
if heartbeat.session.SessionID != account.SessionID || heartbeat.session.SiteID != "99917999" {
|
||||
t.Fatalf("restored session identifiers = %#v", heartbeat.session)
|
||||
}
|
||||
waitSignal(t, protocol.presences, "presence restore")
|
||||
if err := manager.WakeInbox(ctx, account.GochatInboxID); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -223,6 +226,7 @@ type fakeProtocol struct {
|
||||
}
|
||||
|
||||
type heartbeatCall struct {
|
||||
session swt.Session
|
||||
currentSID string
|
||||
typingSID string
|
||||
cursor swt.Cursor
|
||||
@@ -242,7 +246,7 @@ func (f *fakeProtocol) Login(context.Context, swt.Credentials, swt.Presence, str
|
||||
return swt.Session{BaseURL: "http://example.test/", SiteID: "99917999", LoginName: "agent", MAToken: "new-token"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeProtocol) Heartbeat(_ context.Context, _ swt.Session, cursor swt.Cursor, currentSID, typingSID string) (swt.HeartbeatResult, error) {
|
||||
func (f *fakeProtocol) Heartbeat(_ context.Context, session swt.Session, cursor swt.Cursor, currentSID, typingSID string) (swt.HeartbeatResult, error) {
|
||||
f.mu.Lock()
|
||||
if f.heartbeatPanics > 0 {
|
||||
f.heartbeatPanics--
|
||||
@@ -250,7 +254,7 @@ func (f *fakeProtocol) Heartbeat(_ context.Context, _ swt.Session, cursor swt.Cu
|
||||
panic("heartbeat panic")
|
||||
}
|
||||
f.mu.Unlock()
|
||||
f.heartbeats <- heartbeatCall{currentSID: currentSID, typingSID: typingSID, cursor: cursor}
|
||||
f.heartbeats <- heartbeatCall{session: session, currentSID: currentSID, typingSID: typingSID, cursor: cursor}
|
||||
return swt.HeartbeatResult{Status: swt.HeartbeatOK}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -326,7 +326,10 @@ func (s *supervisor) restoreSession(account *dbgen.Account) bool {
|
||||
return false
|
||||
}
|
||||
s.gate.Lock()
|
||||
s.session = swt.Session{BaseURL: *account.BaseUrl, SiteID: *account.SiteID, LoginName: *account.LoginName, MAToken: *account.MaToken}
|
||||
s.session = swt.Session{
|
||||
BaseURL: *account.BaseUrl, SessionID: account.SessionID, SiteID: *account.SiteID,
|
||||
LoginName: *account.LoginName, MAToken: *account.MaToken,
|
||||
}
|
||||
s.sessionVersion++
|
||||
s.gate.Unlock()
|
||||
return true
|
||||
|
||||
@@ -199,6 +199,78 @@ func TestXSTReceptionSyncRetryDoesNotRepeatPrimarySend(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestXSTRouteInvalidatesReceptionSyncWhenRouteIdentityChanges(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
database, account := deliveryDatabase(t, ctx)
|
||||
seedXSTRoute(t, database, account)
|
||||
outbound, _ := NewOutbound(database, sessionStub{}, senderStub{}, &resultRecorder{}, nil, 1)
|
||||
message := &dbgen.OutboundMessage{AccountID: account.ID, SwtSid: "visitor"}
|
||||
|
||||
route, err := outbound.xstRoute(ctx, message)
|
||||
if err != nil || route.synced || route.KFName != "口腔客服2" {
|
||||
t.Fatalf("initial route = %#v, %v", route, err)
|
||||
}
|
||||
if rows, err := database.Writer().MarkConversationXSTSynced(ctx, dbgen.MarkConversationXSTSyncedParams{
|
||||
XstSyncKey: &route.syncKey, AccountID: account.ID, SwtSid: "visitor", XstToken: &route.Token,
|
||||
}); err != nil || rows != 1 {
|
||||
t.Fatalf("mark synced = %d, %v", rows, err)
|
||||
}
|
||||
route, err = outbound.xstRoute(ctx, message)
|
||||
if err != nil || !route.synced {
|
||||
t.Fatalf("cached route = %#v, %v", route, err)
|
||||
}
|
||||
|
||||
dedicatedName := "百度专用客服名"
|
||||
if _, err := database.Writer().UpsertConversationXSTRoute(ctx, dbgen.UpsertConversationXSTRouteParams{
|
||||
AccountID: account.ID, SwtSid: "visitor", GochatContactSourceID: "visitor",
|
||||
XstRequired: 1, XstKfname: &dedicatedName,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
route, err = outbound.xstRoute(ctx, message)
|
||||
if err != nil || !route.synced || route.KFName != dedicatedName {
|
||||
t.Fatalf("dedicated-name route = %#v, %v", route, err)
|
||||
}
|
||||
|
||||
newToken := "xst-route-token-2"
|
||||
if _, err := database.Writer().UpsertConversationXSTRoute(ctx, dbgen.UpsertConversationXSTRouteParams{
|
||||
AccountID: account.ID, SwtSid: "visitor", GochatContactSourceID: "visitor",
|
||||
XstRequired: 1, XstToken: &newToken,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
route, err = outbound.xstRoute(ctx, message)
|
||||
if err != nil || route.synced || route.Token != newToken {
|
||||
t.Fatalf("new-token route = %#v, %v", route, err)
|
||||
}
|
||||
if rows, err := database.Writer().MarkConversationXSTSynced(ctx, dbgen.MarkConversationXSTSyncedParams{
|
||||
XstSyncKey: &route.syncKey, AccountID: account.ID, SwtSid: "visitor", XstToken: &route.Token,
|
||||
}); err != nil || rows != 1 {
|
||||
t.Fatalf("mark new token synced = %d, %v", rows, err)
|
||||
}
|
||||
|
||||
if _, err := database.PersistHeartbeat(ctx, account, []swt.HeartbeatEvent{{
|
||||
SessionID: "visitor", Kind: 6, Text: "口腔客服2", SeqID: 100,
|
||||
Timestamp: time.Now().Format(time.RFC3339Nano), RawLine: "visitor 6 |口腔客服2 100 timestamp",
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
route, err = outbound.xstRoute(ctx, message)
|
||||
if err != nil || route.synced {
|
||||
t.Fatalf("new-operator-sequence route = %#v, %v", route, err)
|
||||
}
|
||||
|
||||
if _, err := database.PersistHeartbeat(ctx, account, []swt.HeartbeatEvent{{
|
||||
SessionID: "visitor", Kind: 6, Text: "其他客服", SeqID: 101,
|
||||
Timestamp: time.Now().Format(time.RFC3339Nano), RawLine: "visitor 6 |其他客服 101 timestamp",
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if route, err = outbound.xstRoute(ctx, message); err == nil || !strings.Contains(err.Error(), "not assigned to the current operator") {
|
||||
t.Fatalf("mismatched-operator route = %#v, %v", route, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXSTRejectThenEchoRetriesOnlyXST(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
database, account := deliveryDatabase(t, ctx)
|
||||
|
||||
@@ -124,6 +124,7 @@ func (c *Client) Login(ctx context.Context, credentials Credentials, presence Pr
|
||||
}
|
||||
return Session{
|
||||
BaseURL: baseURL,
|
||||
SessionID: credentials.SessionID,
|
||||
SiteID: credentials.SessionID[3:],
|
||||
LoginName: credentials.Username,
|
||||
MAToken: result.MAToken(),
|
||||
@@ -190,7 +191,7 @@ func (c *Client) SendText(ctx context.Context, session Session, sid, text string
|
||||
}
|
||||
|
||||
func (c *Client) SendXSTText(ctx context.Context, session Session, route XSTRoute, text string) (SendResult, error) {
|
||||
if err := session.Validate(); err != nil {
|
||||
if err := session.ValidateXST(); err != nil {
|
||||
return SendResult{}, err
|
||||
}
|
||||
if err := route.Validate(); err != nil {
|
||||
@@ -201,7 +202,7 @@ func (c *Client) SendXSTText(ctx context.Context, session Session, route XSTRout
|
||||
query := url.Values{
|
||||
"t": {strconv.FormatInt(ticks, 10)},
|
||||
"sign": {xstSignature(ticks, session, route, wireContent, xstClientVersion)},
|
||||
"swtidhead": {session.SiteID},
|
||||
"swtidhead": {session.SessionID},
|
||||
"onamehead": {session.LoginName},
|
||||
"kfversionhead": {xstClientVersion},
|
||||
}
|
||||
@@ -212,7 +213,7 @@ func (c *Client) SendXSTText(ctx context.Context, session Session, route XSTRout
|
||||
form := url.Values{
|
||||
"html": {wireContent},
|
||||
"RESET": {""},
|
||||
"siteid": {session.SiteID},
|
||||
"siteid": {session.SessionID},
|
||||
"oname": {session.LoginName},
|
||||
"cid": {route.CID},
|
||||
"sid": {route.SID},
|
||||
@@ -227,7 +228,7 @@ func (c *Client) SendXSTText(ctx context.Context, session Session, route XSTRout
|
||||
}
|
||||
|
||||
func (c *Client) SyncXSTReception(ctx context.Context, session Session, route XSTRoute) (SendResult, error) {
|
||||
if err := session.Validate(); err != nil {
|
||||
if err := session.ValidateXST(); err != nil {
|
||||
return SendResult{}, err
|
||||
}
|
||||
if err := route.Validate(); err != nil {
|
||||
@@ -238,7 +239,7 @@ func (c *Client) SyncXSTReception(ctx context.Context, session Session, route XS
|
||||
query := url.Values{
|
||||
"t": {strconv.FormatInt(ticks, 10)},
|
||||
"sign": {xstReceptionSignature(ticks, session, route, deviceID, xstClientVersion)},
|
||||
"swtidhead": {session.SiteID},
|
||||
"swtidhead": {session.SessionID},
|
||||
"onamehead": {session.LoginName},
|
||||
"kfversionhead": {xstClientVersion},
|
||||
}
|
||||
@@ -256,7 +257,7 @@ func (c *Client) SyncXSTReception(ctx context.Context, session Session, route XS
|
||||
"accounttype": {"2"},
|
||||
"state": {route.State},
|
||||
"oname": {session.LoginName},
|
||||
"siteid": {session.SiteID},
|
||||
"siteid": {session.SessionID},
|
||||
"kfversion": {xstClientVersion},
|
||||
}
|
||||
return c.sendXSTForm(ctx, target+"?"+query.Encode(), form, "sync_xst_reception", "xst_sync_rejected", true, session, route)
|
||||
@@ -283,7 +284,7 @@ func (c *Client) sendXSTForm(ctx context.Context, target string, form url.Values
|
||||
if strings.EqualFold(result.Status, "ok") {
|
||||
return result, nil
|
||||
}
|
||||
detail := sanitizeXSTMessage(payload.Message, session.MAToken, route.Token, route.SID, route.CID, session.SiteID, session.LoginName)
|
||||
detail := sanitizeXSTMessage(payload.Message, session.MAToken, route.Token, route.SID, route.CID, session.SessionID, session.SiteID, session.LoginName)
|
||||
var detailErr error
|
||||
if detail != "" {
|
||||
detailErr = errors.New(detail)
|
||||
@@ -310,7 +311,7 @@ func nativeXSTTicks(now time.Time) int64 {
|
||||
func xstSignature(ticks int64, session Session, route XSTRoute, content, version string) string {
|
||||
separator := string(rune(26))
|
||||
plain := strings.Join([]string{
|
||||
strconv.FormatInt(ticks+10, 10), session.SiteID, session.LoginName, route.CID, route.SID,
|
||||
strconv.FormatInt(ticks+10, 10), session.SessionID, session.LoginName, route.CID, route.SID,
|
||||
route.Token, route.State, route.KFName, "1", content, version, "",
|
||||
}, separator)
|
||||
digest := md5.Sum([]byte(plain))
|
||||
@@ -320,7 +321,7 @@ func xstSignature(ticks int64, session Session, route XSTRoute, content, version
|
||||
func xstReceptionSignature(ticks int64, session Session, route XSTRoute, deviceID, version string) string {
|
||||
separator := string(rune(26))
|
||||
plain := strings.Join([]string{
|
||||
strconv.FormatInt(ticks+10, 10), session.SiteID, "2", "3", deviceID, route.State,
|
||||
strconv.FormatInt(ticks+10, 10), session.SessionID, "2", "3", deviceID, route.State,
|
||||
"2", session.LoginName, route.CID, route.SID, version, "",
|
||||
}, separator)
|
||||
digest := md5.Sum([]byte(plain))
|
||||
@@ -328,7 +329,7 @@ func xstReceptionSignature(ticks int64, session Session, route XSTRoute, deviceI
|
||||
}
|
||||
|
||||
func xstDeviceID(session Session) string {
|
||||
digest := sha1.Sum([]byte("gochat-shangwutong\x1a" + session.SiteID + "\x1a" + session.LoginName))
|
||||
digest := sha1.Sum([]byte("gochat-shangwutong\x1a" + session.SessionID + "\x1a" + session.LoginName))
|
||||
return strings.ToUpper(hex.EncodeToString(digest[:]))
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,14 @@ func TestClientLogin(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if session.MAToken != "token" || session.SiteID != "99917999" || session.Purview == nil || *session.Purview != 8388610 || !session.AllowsJoiningOtherOperatorDialogue() {
|
||||
if session.MAToken != "token" || session.SessionID != "BYT99917999" || session.SiteID != "99917999" || session.Purview == nil || *session.Purview != 8388610 || !session.AllowsJoiningOtherOperatorDialogue() {
|
||||
t.Fatalf("unexpected session: %#v", session)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXSTSignatureKnownVector(t *testing.T) {
|
||||
route := XSTRoute{SID: "sid-1", CID: "cid-1", Token: "xst-1", State: "5", KFName: "客服"}
|
||||
if got := xstSignature(638900000000000000, testSession(), route, "hello", xstClientVersion); got != "92872732c501789da6bf1c882f3ede40" {
|
||||
if got := xstSignature(638900000000000000, testSession(), route, "hello", xstClientVersion); got != "891ea08d6f894bfb228f9ed2f5bb43d8" {
|
||||
t.Fatalf("signature = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -46,10 +46,10 @@ func TestXSTSignatureKnownVector(t *testing.T) {
|
||||
func TestXSTReceptionSignatureKnownVector(t *testing.T) {
|
||||
route := XSTRoute{SID: "sid-1", CID: "cid-1", Token: "xst-1", State: "5", KFName: "客服"}
|
||||
deviceID := xstDeviceID(testSession())
|
||||
if deviceID != "3352A6B5AE0FA4ED75745D33D454E3E61A1E860B" {
|
||||
if deviceID != "97CC54346B0198753408C3FC5D788EF23197C93A" {
|
||||
t.Fatalf("device ID = %q", deviceID)
|
||||
}
|
||||
if got := xstReceptionSignature(638900000000000000, testSession(), route, deviceID, xstClientVersion); got != "81a5ace9d65e85f519af28482ba6a200" {
|
||||
if got := xstReceptionSignature(638900000000000000, testSession(), route, deviceID, xstClientVersion); got != "28236364a57ae28d0dd07ea633ebec7d" {
|
||||
t.Fatalf("signature = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -68,14 +68,14 @@ func TestClientSendXSTTextMatchesNativeWireProtocol(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
query := request.URL.Query()
|
||||
if request.URL.Path != "/api/swtsynckfmsg.ashx" || query.Get("sign") == "" || query.Get("t") == "" ||
|
||||
query.Get("swtidhead") != "99917999" || query.Get("onamehead") != "agent" || query.Get("kfversionhead") != xstClientVersion {
|
||||
query.Get("swtidhead") != "BYT99917999" || query.Get("onamehead") != "agent" || query.Get("kfversionhead") != xstClientVersion {
|
||||
t.Fatalf("xst request URL = %s", request.URL.String())
|
||||
}
|
||||
if err := request.ParseForm(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for key, want := range map[string]string{
|
||||
"siteid": "99917999", "oname": "agent", "cid": "cid-1", "sid": "sid-1", "xst": "xst-1",
|
||||
"siteid": "BYT99917999", "oname": "agent", "cid": "cid-1", "sid": "sid-1", "xst": "xst-1",
|
||||
"state": "5", "kfname": "客服", "html": "hello *~", "content": "hello+*%7E", "msgkind": "1", "kfversion": xstClientVersion, "RESET": "",
|
||||
} {
|
||||
if got := request.Form.Get(key); got != want {
|
||||
@@ -96,7 +96,7 @@ func TestClientSyncXSTReceptionMatchesNativeWireProtocol(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
query := request.URL.Query()
|
||||
if request.URL.Path != "/api/SwtSyncKfInfo.ashx" || query.Get("sign") == "" || query.Get("t") == "" ||
|
||||
query.Get("swtidhead") != "99917999" || query.Get("onamehead") != "agent" || query.Get("kfversionhead") != xstClientVersion {
|
||||
query.Get("swtidhead") != "BYT99917999" || query.Get("onamehead") != "agent" || query.Get("kfversionhead") != xstClientVersion {
|
||||
t.Fatalf("XST reception sync URL = %s", request.URL.String())
|
||||
}
|
||||
if err := request.ParseForm(); err != nil {
|
||||
@@ -104,8 +104,8 @@ func TestClientSyncXSTReceptionMatchesNativeWireProtocol(t *testing.T) {
|
||||
}
|
||||
for key, want := range map[string]string{
|
||||
"cid": "cid-1", "sid": "sid-1", "clienttype": "2", "clientkind": "3",
|
||||
"imeiaddress": "3352A6B5AE0FA4ED75745D33D454E3E61A1E860B", "accounttype": "2",
|
||||
"state": "5", "oname": "agent", "siteid": "99917999", "kfversion": xstClientVersion, "RESET": "",
|
||||
"imeiaddress": "97CC54346B0198753408C3FC5D788EF23197C93A", "accounttype": "2",
|
||||
"state": "5", "oname": "agent", "siteid": "BYT99917999", "kfversion": xstClientVersion, "RESET": "",
|
||||
} {
|
||||
if got := request.Form.Get(key); got != want {
|
||||
t.Fatalf("form[%s] = %q, want %q", key, got, want)
|
||||
@@ -138,6 +138,17 @@ func TestClientSendXSTTextRejectsNonOK(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSendXSTTextRejectsMissingFullSessionID(t *testing.T) {
|
||||
session := testSession()
|
||||
session.SessionID = ""
|
||||
_, err := NewClient(nil).SendXSTText(context.Background(), session, XSTRoute{
|
||||
SID: "sid-1", CID: "cid-1", Token: "xst-1", State: "5", KFName: "客服",
|
||||
}, "hello")
|
||||
if err == nil || !strings.Contains(err.Error(), "XST session_id") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientHeartbeatParsesEvents(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
response.Header().Set("r", "ok")
|
||||
@@ -159,6 +170,9 @@ func TestClientSendTextEscapesHTML(t *testing.T) {
|
||||
if err := request.ParseForm(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := request.Form.Get("siteid"); got != "99917999" {
|
||||
t.Fatalf("siteid = %q", got)
|
||||
}
|
||||
if got := request.Form.Get("html"); got != "<b>x</b><BR>next" {
|
||||
t.Fatalf("html = %q", got)
|
||||
}
|
||||
@@ -252,7 +266,7 @@ func TestClientSendExplicitServerErrorIsRetryableNotUncertain(t *testing.T) {
|
||||
}
|
||||
|
||||
func testSession() Session {
|
||||
return Session{BaseURL: "http://byt.yiaitao.com.cn/", SiteID: "99917999", LoginName: "agent", MAToken: "token"}
|
||||
return Session{BaseURL: "http://byt.yiaitao.com.cn/", SessionID: "BYT99917999", SiteID: "99917999", LoginName: "agent", MAToken: "token"}
|
||||
}
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
@@ -85,7 +85,8 @@ func BuildBaseURL(sessionID string) (string, error) {
|
||||
|
||||
type Session struct {
|
||||
BaseURL string
|
||||
SiteID string
|
||||
SessionID string // Full configured identifier, e.g. DUT88090694; required by XST APIs.
|
||||
SiteID string // Numeric suffix, e.g. 88090694; required by legacy SWT APIs.
|
||||
LoginName string
|
||||
MAToken string
|
||||
Purview *uint64
|
||||
@@ -114,6 +115,16 @@ func (s Session) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Session) ValidateXST() error {
|
||||
if err := s.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !sessionIDPattern.MatchString(s.SessionID) {
|
||||
return errors.New("XST session_id must contain exactly 11 ASCII letters or digits")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Session) AllowsJoiningOtherOperatorDialogue() bool {
|
||||
return s.Purview != nil && (*s.Purview&purviewAdmin != 0 || *s.Purview&purviewProhibitJoinOtherDialogue == 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user