fix: better `IsStatusSharedURL` (#4280)

* fix: better `IsStatusSharedURL`
* refactor `ParseSharedURL`
This commit is contained in:
Igor Sirotin 2023-11-10 15:00:03 +00:00 committed by GitHub
parent c3687acc84
commit b36d95d84d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 147 additions and 50 deletions

View File

@ -162,7 +162,7 @@ func (m *Messenger) UnfurlURLs(httpClient *http.Client, urls []string) (UnfurlUR
for _, url := range urls { for _, url := range urls {
m.logger.Debug("unfurling", zap.String("url", url)) m.logger.Debug("unfurling", zap.String("url", url))
if m.IsStatusSharedURL(url) { if IsStatusSharedURL(url) {
unfurler := NewStatusUnfurler(url, m, m.logger) unfurler := NewStatusUnfurler(url, m, m.logger)
preview, err := unfurler.Unfurl() preview, err := unfurler.Unfurl()
if err != nil { if err != nil {

View File

@ -48,8 +48,22 @@ type URLDataResponse struct {
} }
const baseShareURL = "https://status.app" const baseShareURL = "https://status.app"
const userPath = "u#"
const userWithDataPath = "u/"
const communityPath = "c#"
const communityWithDataPath = "c/"
const channelPath = "cc/"
const sharedURLUserPrefix = baseShareURL + "/" + userPath
const sharedURLUserPrefixWithData = baseShareURL + "/" + userWithDataPath
const sharedURLCommunityPrefix = baseShareURL + "/" + communityPath
const sharedURLCommunityPrefixWithData = baseShareURL + "/" + communityWithDataPath
const sharedURLChannelPrefixWithData = baseShareURL + "/" + channelPath
const channelUUIDRegExp = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$" const channelUUIDRegExp = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$"
var channelRegExp = regexp.MustCompile(channelUUIDRegExp)
func (m *Messenger) SerializePublicKey(compressedKey types.HexBytes) (string, error) { func (m *Messenger) SerializePublicKey(compressedKey types.HexBytes) (string, error) {
return utils.SerializePublicKey(compressedKey) return utils.SerializePublicKey(compressedKey)
} }
@ -511,51 +525,68 @@ func (m *Messenger) parseUserURLWithData(data string, chatKey string) (*URLDataR
}, nil }, nil
} }
func (m *Messenger) IsStatusSharedURL(url string) bool { func IsStatusSharedURL(url string) bool {
return strings.HasPrefix(url, baseShareURL) return strings.HasPrefix(url, sharedURLUserPrefix) ||
strings.HasPrefix(url, sharedURLUserPrefixWithData) ||
strings.HasPrefix(url, sharedURLCommunityPrefix) ||
strings.HasPrefix(url, sharedURLCommunityPrefixWithData) ||
strings.HasPrefix(url, sharedURLChannelPrefixWithData)
}
func splitSharedURLData(data string) (string, string, error) {
const count = 2
contents := strings.SplitN(data, "#", count)
if len(contents) != count {
return "", "", fmt.Errorf("url should contain at least one `#` separator")
}
return contents[0], contents[1], nil
} }
func (m *Messenger) ParseSharedURL(url string) (*URLDataResponse, error) { func (m *Messenger) ParseSharedURL(url string) (*URLDataResponse, error) {
if !m.IsStatusSharedURL(url) {
return nil, fmt.Errorf("url should start with '%s'", baseShareURL) if strings.HasPrefix(url, sharedURLUserPrefix) {
chatKey := strings.TrimPrefix(url, sharedURLUserPrefix)
if strings.HasPrefix(chatKey, "zQ3sh") {
return m.parseUserURLWithChatKey(chatKey)
}
return m.parseUserURLWithENS(chatKey)
} }
urlContents := regexp.MustCompile(`\#`).Split(strings.TrimPrefix(url, baseShareURL+"/"), 2) if strings.HasPrefix(url, sharedURLUserPrefixWithData) {
if len(urlContents) != 2 { trimmedURL := strings.TrimPrefix(url, sharedURLUserPrefixWithData)
return nil, fmt.Errorf("url should contain at least one `#` separator") encodedData, chatKey, err := splitSharedURLData(trimmedURL)
}
if urlContents[0] == "c" {
return m.parseCommunityURLWithChatKey(urlContents[1])
}
if strings.HasPrefix(urlContents[0], "c/") {
return m.parseCommunityURLWithData(strings.TrimPrefix(urlContents[0], "c/"), urlContents[1])
}
if strings.HasPrefix(urlContents[0], "cc/") {
first := strings.TrimPrefix(urlContents[0], "cc/")
isChannel, err := regexp.MatchString(channelUUIDRegExp, first)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if isChannel { return m.parseUserURLWithData(encodedData, chatKey)
return m.parseCommunityChannelURLWithChatKey(first, urlContents[1]) }
if strings.HasPrefix(url, sharedURLCommunityPrefix) {
chatKey := strings.TrimPrefix(url, sharedURLCommunityPrefix)
return m.parseCommunityURLWithChatKey(chatKey)
}
if strings.HasPrefix(url, sharedURLCommunityPrefixWithData) {
trimmedURL := strings.TrimPrefix(url, sharedURLCommunityPrefixWithData)
encodedData, chatKey, err := splitSharedURLData(trimmedURL)
if err != nil {
return nil, err
} }
return m.parseCommunityChannelURLWithData(first, urlContents[1]) return m.parseCommunityURLWithData(encodedData, chatKey)
} }
if urlContents[0] == "u" { if strings.HasPrefix(url, sharedURLChannelPrefixWithData) {
if strings.HasPrefix(urlContents[1], "zQ3sh") { trimmedURL := strings.TrimPrefix(url, sharedURLChannelPrefixWithData)
return m.parseUserURLWithChatKey(urlContents[1]) encodedData, chatKey, err := splitSharedURLData(trimmedURL)
if err != nil {
return nil, err
} }
return m.parseUserURLWithENS(urlContents[1])
if channelRegExp.MatchString(encodedData) {
return m.parseCommunityChannelURLWithChatKey(encodedData, chatKey)
}
return m.parseCommunityChannelURLWithData(encodedData, chatKey)
} }
if strings.HasPrefix(urlContents[0], "u/") { return nil, fmt.Errorf("not a status shared url")
return m.parseUserURLWithData(strings.TrimPrefix(urlContents[0], "u/"), urlContents[1])
}
return nil, fmt.Errorf("unhandled shared url: %s", url)
} }

View File

@ -17,6 +17,15 @@ import (
"github.com/status-im/status-go/protocol/urls" "github.com/status-im/status-go/protocol/urls"
) )
const (
userURL = "https://status.app/u#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"
userURLWithData = "https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj"
communityURL = "https://status.app/c#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
communityURLWithData = "https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
channelURL = "https://status.app/cc/003cdcd5-e065-48f9-b166-b1a94ac75a11#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
channelURLWithData = "https://status.app/cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
)
func TestMessengerShareUrlsSuite(t *testing.T) { func TestMessengerShareUrlsSuite(t *testing.T) {
suite.Run(t, new(MessengerShareUrlsSuite)) suite.Run(t, new(MessengerShareUrlsSuite))
} }
@ -134,23 +143,85 @@ func (s *MessengerShareUrlsSuite) TestDeserializePublicKey() {
} }
func (s *MessengerShareUrlsSuite) TestParseWrongUrls() { func (s *MessengerShareUrlsSuite) TestParseWrongUrls() {
urls := map[string]string{ const notStatusSharedURLError = "not a status shared url"
"https://status.appc/#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11": "unhandled shared url", badURLs := map[string]string{
"https://status.app/cc#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11": "unhandled shared url", "https://status.appc/#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11": notStatusSharedURLError,
"https://status.app/a#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11": "unhandled shared url", "https://status.app/cc#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11": notStatusSharedURLError,
"https://status.app/a#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11": notStatusSharedURLError,
"https://status.im/u#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11": notStatusSharedURLError,
"https://status.app/u/": "url should contain at least one `#` separator", "https://status.app/u/": "url should contain at least one `#` separator",
"https://status.im/u#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11": "url should start with 'https://status.app'",
} }
for url, expectedError := range urls { for url, expectedError := range badURLs {
urlData, err := s.m.ParseSharedURL(url) urlData, err := s.m.ParseSharedURL(url)
s.Require().Error(err) s.Require().Error(err)
s.Require().Equal(err.Error(), expectedError)
s.Require().True(strings.HasPrefix(err.Error(), expectedError))
s.Require().Nil(urlData) s.Require().Nil(urlData)
} }
} }
func (s *MessengerShareUrlsSuite) TestIsStatusSharedUrl() {
testCases := []struct {
Name string
URL string
Result bool
}{
{
Name: "Direct website link",
URL: "https://status.app",
Result: false,
},
{
Name: "Website page link",
URL: "https://status.app/features/messenger",
Result: false,
},
{
// starts with `/c`, but no `#` after
Name: "Website page link",
URL: "https://status.app/communities",
Result: false,
},
{
Name: "User link",
URL: userURL,
Result: true,
},
{
Name: "User link with data",
URL: userURLWithData,
Result: true,
},
{
Name: "Community link",
URL: communityURL,
Result: true,
},
{
Name: "Community link with data",
URL: communityURLWithData,
Result: true,
},
{
Name: "Channel link",
URL: channelURL,
Result: true,
},
{
Name: "Channel link with data",
URL: channelURLWithData,
Result: true,
},
}
for _, tc := range testCases {
s.Run(tc.Name, func() {
result := IsStatusSharedURL(tc.URL)
s.Require().Equal(tc.Result, result)
})
}
}
func (s *MessengerShareUrlsSuite) TestShareCommunityURLWithChatKey() { func (s *MessengerShareUrlsSuite) TestShareCommunityURLWithChatKey() {
community := s.createCommunity() community := s.createCommunity()
@ -198,9 +269,7 @@ func (s *MessengerShareUrlsSuite) TestShareCommunityURLWithData() {
} }
func (s *MessengerShareUrlsSuite) TestParseCommunityURLWithData() { func (s *MessengerShareUrlsSuite) TestParseCommunityURLWithData() {
url := "https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11" urlData, err := s.m.ParseSharedURL(communityURLWithData)
urlData, err := s.m.ParseSharedURL(url)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(urlData) s.Require().NotNil(urlData)
@ -290,9 +359,7 @@ func (s *MessengerShareUrlsSuite) TestShareCommunityChannelURLWithData() {
} }
func (s *MessengerShareUrlsSuite) TestParseCommunityChannelURLWithData() { func (s *MessengerShareUrlsSuite) TestParseCommunityChannelURLWithData() {
url := "https://status.app/cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11" urlData, err := s.m.ParseSharedURL(channelURLWithData)
urlData, err := s.m.ParseSharedURL(url)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(urlData) s.Require().NotNil(urlData)
@ -392,8 +459,7 @@ func (s *MessengerShareUrlsSuite) TestShareUserURLWithENS() {
// } // }
func (s *MessengerShareUrlsSuite) TestParseUserURLWithData() { func (s *MessengerShareUrlsSuite) TestParseUserURLWithData() {
url := "https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#zQ3shwQPhRuDJSjVGVBnTjCdgXy5i9WQaeVPdGJD6yTarJQSj" urlData, err := s.m.ParseSharedURL(userURLWithData)
urlData, err := s.m.ParseSharedURL(url)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(urlData) s.Require().NotNil(urlData)