fix support for SSB URIs on Android Chrome

This commit is contained in:
Andre Staltz 2021-11-09 18:08:46 +02:00 committed by André Staltz
parent f6fca892ce
commit 642022cb0a
8 changed files with 221 additions and 13 deletions

1
go.mod
View File

@ -20,6 +20,7 @@ require (
github.com/mattevans/pwned-passwords v0.3.0
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0
github.com/mileusna/useragent v1.0.2 // indirect
github.com/nicksnyder/go-i18n/v2 v2.1.2
github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351

2
go.sum
View File

@ -150,6 +150,8 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0 h1:8E6DrFvII6QR4eJ3PkFvV+lc03P+2qwqTPLm1ax7694=
github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0/go.mod h1:fcEyUyXZXoV4Abw8DX0t7wyL8mCDxXyU4iAFZfT3IHw=
github.com/mileusna/useragent v1.0.2 h1:DgVKtiPnjxlb73z9bCwgdUvU2nQNQ97uhgfO8l9uz/w=
github.com/mileusna/useragent v1.0.2/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/miolini/datacounter v0.0.0-20171104152933-fd4e42a1d5e0/go.mod h1:P6fDJzlxN+cWYR09KbE9/ta+Y6JofX9tAUhJpWkWPaM=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=

View File

@ -13,6 +13,7 @@ import (
"net/http"
"net/url"
ua "github.com/mileusna/useragent"
"github.com/gorilla/mux"
"go.mindeco.de/http/render"
@ -159,12 +160,23 @@ func (html aliasHTMLResponder) SendConfirmation(alias roomdb.Alias) {
// html.multiservAddr
ssbURI := url.URL{
Scheme: "ssb",
Opaque: "experimental",
Scheme: "ssb",
Opaque: "experimental",
RawQuery: queryParams.Encode(),
}
// Special treatment for Android Chrome for issue #135
// https://github.com/ssb-ngi-pointer/go-ssb-room/issues/135
browser := ua.Parse(html.req.UserAgent())
if browser.IsAndroid() && browser.IsChrome() {
ssbURI = url.URL{
Scheme: "intent",
Opaque: "//experimental",
RawQuery: queryParams.Encode(),
Fragment: "Intent;scheme=ssb;package=se.manyver;end;",
}
}
err := html.renderer.Render(html.rw, html.req, "alias.tmpl", http.StatusOK, struct {
Alias roomdb.Alias

View File

@ -100,3 +100,63 @@ func TestAliasResolve(t *testing.T) {
html, resp = ts.Client.GetHTML(htmlURL)
a.Equal(http.StatusInternalServerError, resp.Code)
}
func TestAliasResolveOnAndroidChrome(t *testing.T) {
ts := setup(t)
a := assert.New(t)
r := require.New(t)
var testAlias = roomdb.Alias{
ID: 54321,
Name: "test-name",
Feed: refs.FeedRef{
ID: bytes.Repeat([]byte{'F'}, 32),
Algo: "test",
},
Signature: bytes.Repeat([]byte{'S'}, 32),
}
ts.AliasesDB.ResolveReturns(testAlias, nil)
// Mimic Android Chrome
var uaHeader = make(http.Header)
uaHeader.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MOB30H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.133 Mobile Safari/537.36")
ts.Client.SetHeaders(uaHeader)
// to construct the /alias/{name} url we need to bypass urlTo
// (which builds ?alias=name)
routes := router.CompleteApp()
// default is HTML
htmlURL, err := routes.Get(router.CompleteAliasResolve).URL("alias", testAlias.Name)
r.NoError(err)
t.Log("resolving", htmlURL.String())
html, resp := ts.Client.GetHTML(htmlURL)
a.Equal(http.StatusOK, resp.Code)
a.Equal(testAlias.Name, html.Find("title").Text())
// ssb-uri in href
aliasHref, ok := html.Find("#alias-uri").Attr("href")
a.True(ok)
aliasURI, err := url.Parse(aliasHref)
r.NoError(err)
a.Equal("intent", aliasURI.Scheme)
a.Equal("experimental", aliasURI.Host)
params := aliasURI.Query()
a.Equal("consume-alias", params.Get("action"))
a.Equal(testAlias.Name, params.Get("alias"))
a.Equal(testAlias.Feed.Ref(), params.Get("userId"))
sigData, err := base64.StdEncoding.DecodeString(params.Get("signature"))
r.NoError(err)
a.Equal(testAlias.Signature, sigData)
a.Equal(ts.NetworkInfo.RoomID.Ref(), params.Get("roomId"))
a.Equal(ts.NetworkInfo.MultiserverAddress(), params.Get("multiserverAddress"))
frag := aliasURI.Fragment
a.Equal("Intent;scheme=ssb;package=se.manyver;end;", frag)
}

View File

@ -20,6 +20,7 @@ import (
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
ua "github.com/mileusna/useragent"
"github.com/skip2/go-qrcode"
"go.cryptoscope.co/muxrpc/v2"
"go.mindeco.de/http/render"
@ -255,7 +256,7 @@ func (h WithSSBHandler) DecideMethod(w http.ResponseWriter, req *http.Request) {
// assume server-init sse dance
sc := queryVals.Get("sc") // is non-empty when a remote device sends the solution
data, err := h.serverInitiated(sc)
data, err := h.serverInitiated(sc, req.UserAgent())
if err != nil {
h.render.Error(w, req, http.StatusInternalServerError, err)
return
@ -348,7 +349,7 @@ type templateData struct {
ServerChallenge string
}
func (h WithSSBHandler) serverInitiated(sc string) (templateData, error) {
func (h WithSSBHandler) serverInitiated(sc string, userAgent string) (templateData, error) {
isSolvingRemotely := true
if sc == "" {
isSolvingRemotely = false
@ -363,11 +364,27 @@ func (h WithSSBHandler) serverInitiated(sc string) (templateData, error) {
queryParams.Set("sc", sc)
queryParams.Set("multiserverAddress", h.netInfo.MultiserverAddress())
var startAuthURI url.URL
startAuthURI := url.URL{
Scheme: "ssb",
Opaque: "experimental",
RawQuery: queryParams.Encode(),
}
startAuthURI.Scheme = "ssb"
startAuthURI.Opaque = "experimental"
startAuthURI.RawQuery = queryParams.Encode()
// Special treatment for Android Chrome for issue #135
// https://github.com/ssb-ngi-pointer/go-ssb-room/issues/135
browser := ua.Parse(userAgent)
if browser.IsAndroid() && browser.IsChrome() {
startAuthURI = url.URL{
Scheme: "intent",
Opaque: "//experimental",
RawQuery: queryParams.Encode(),
Fragment: "Intent;scheme=ssb;package=se.manyver;end;",
}
}
var qrURI string
if !isSolvingRemotely {
urlTo := web.NewURLTo(router.Auth(h.router), h.netInfo)

View File

@ -551,3 +551,52 @@ func TestAuthWithSSBServerInitWrongSolution(t *testing.T) {
resp = ts.Client.GetBody(finalizeURL)
a.Equal(http.StatusForbidden, resp.Result().StatusCode)
}
func TestAuthWithSSBServerOnAndroidChrome(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
// the keypair for our client
testMember := roomdb.Member{ID: 1234}
client, err := keys.NewKeyPair(nil)
r.NoError(err)
testMember.PubKey = client.Feed
// Mimic Android Chrome
var uaHeader = make(http.Header)
uaHeader.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MOB30H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.133 Mobile Safari/537.36")
ts.Client.SetHeaders(uaHeader)
// setup the mocked database
ts.MembersDB.GetByFeedReturns(testMember, nil)
// prepare the url
signInStartURL := ts.URLTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
)
r.NotNil(signInStartURL)
html, resp := ts.Client.GetHTML(signInStartURL)
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
t.Log(html.Find("body").Text())
}
serverChallenge, has := html.Find("#challenge").Attr("data-sc")
a.True(has, "should have server challenge")
a.NotEqual("", serverChallenge)
ssbURI, has := html.Find("#start-auth-uri").Attr("href")
a.True(has, "should have an Android Intent URI")
a.True(strings.HasPrefix(ssbURI, "intent://experimental"), "not an Android Intent URI? %s", ssbURI)
parsedURI, err := url.Parse(ssbURI)
r.NoError(err)
a.Equal("intent", parsedURI.Scheme)
a.Equal("experimental", parsedURI.Host)
qry := parsedURI.Query()
a.Equal("start-http-auth", qry.Get("action"))
frag := parsedURI.Fragment
a.Equal("Intent;scheme=ssb;package=se.manyver;end;", frag)
}

View File

@ -15,6 +15,7 @@ import (
"net/url"
"github.com/gorilla/csrf"
ua "github.com/mileusna/useragent"
"github.com/skip2/go-qrcode"
"go.mindeco.de/http/render"
"go.mindeco.de/log/level"
@ -39,11 +40,7 @@ type inviteHandler struct {
deniedKeys roomdb.DeniedKeysService
}
func (h inviteHandler) buildJoinRoomURI(token string) template.URL {
var joinRoomURI url.URL
joinRoomURI.Scheme = "ssb"
joinRoomURI.Opaque = "experimental"
func (h inviteHandler) buildJoinRoomURI(token string, userAgent string) template.URL {
queryVals := make(url.Values)
queryVals.Set("action", "claim-http-invite")
queryVals.Set("invite", token)
@ -51,7 +48,23 @@ func (h inviteHandler) buildJoinRoomURI(token string) template.URL {
submissionURL := h.urlTo(router.CompleteInviteConsume)
queryVals.Set("postTo", submissionURL.String())
joinRoomURI.RawQuery = queryVals.Encode()
joinRoomURI := url.URL{
Scheme: "ssb",
Opaque: "experimental",
RawPath: queryVals.Encode(),
}
// Special treatment for Android Chrome for issue #135
// https://github.com/ssb-ngi-pointer/go-ssb-room/issues/135
browser := ua.Parse(userAgent)
if browser.IsAndroid() && browser.IsChrome() {
joinRoomURI = url.URL{
Scheme: "intent",
Opaque: "//experimental",
RawQuery: queryVals.Encode(),
Fragment: "Intent;scheme=ssb;package=se.manyver;end;",
}
}
return template.URL(joinRoomURI.String())
}
@ -118,7 +131,7 @@ func (h inviteHandler) presentFacadeAsHTML(rw http.ResponseWriter, req *http.Req
return nil, fmt.Errorf("failed to find room's description: %w", err)
}
joinRoomURI := h.buildJoinRoomURI(token)
joinRoomURI := h.buildJoinRoomURI(token, req.UserAgent())
fallbackURL := h.urlTo(router.CompleteInviteFacadeFallback, "token", token)

View File

@ -107,6 +107,60 @@ func TestInviteShowAcceptForm(t *testing.T) {
})
}
func TestInviteShowAcceptFormOnAndroid(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
testToken := "existing-test-token"
validAcceptURL := ts.URLTo(router.CompleteInviteFacade, "token", testToken)
// Mimic Android Chrome
var uaHeader = make(http.Header)
uaHeader.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MOB30H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.133 Mobile Safari/537.36")
ts.Client.SetHeaders(uaHeader)
// prep the mocked db for http:200
fakeExistingInvite := roomdb.Invite{ID: 1234}
ts.InvitesDB.GetByTokenReturns(fakeExistingInvite, nil)
// request the form
doc, resp := ts.Client.GetHTML(validAcceptURL)
a.Equal(http.StatusOK, resp.Code)
// check database calls
r.EqualValues(1, ts.InvitesDB.GetByTokenCallCount())
_, tokenFromArg := ts.InvitesDB.GetByTokenArgsForCall(0)
a.Equal(testToken, tokenFromArg)
webassert.Localized(t, doc, []webassert.LocalizedElement{
{"#claim-invite-uri", "InviteFacadeJoin"},
{"title", "InviteFacadeTitle"},
})
// ssb-uri in href
joinDataHref, ok := doc.Find("#claim-invite-uri").Attr("href")
a.True(ok)
joinURI, err := url.Parse(joinDataHref)
r.NoError(err)
a.Equal("intent", joinURI.Scheme)
a.Equal("experimental", joinURI.Host)
params := joinURI.Query()
a.Equal("claim-http-invite", params.Get("action"))
inviteParam := params.Get("invite")
a.Equal(testToken, inviteParam)
postTo := params.Get("postTo")
expectedConsumeInviteURL := ts.URLTo(router.CompleteInviteConsume)
a.Equal(expectedConsumeInviteURL.String(), postTo)
frag := joinURI.Fragment
a.Equal("Intent;scheme=ssb;package=se.manyver;end;", frag)
}
func TestInviteConsumeInviteHTTP(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)