From 642022cb0a283e8a619271b741317eed3b594634 Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Tue, 9 Nov 2021 18:08:46 +0200 Subject: [PATCH] fix support for SSB URIs on Android Chrome --- go.mod | 1 + go.sum | 2 ++ web/handlers/aliases.go | 18 +++++++++-- web/handlers/aliases_test.go | 60 ++++++++++++++++++++++++++++++++++++ web/handlers/auth/withssb.go | 23 ++++++++++++-- web/handlers/auth_test.go | 49 +++++++++++++++++++++++++++++ web/handlers/invites.go | 27 +++++++++++----- web/handlers/invites_test.go | 54 ++++++++++++++++++++++++++++++++ 8 files changed, 221 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 4441b89..46f7b3d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 06fa0c0..4c7308a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/web/handlers/aliases.go b/web/handlers/aliases.go index c778797..10e8176 100644 --- a/web/handlers/aliases.go +++ b/web/handlers/aliases.go @@ -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 diff --git a/web/handlers/aliases_test.go b/web/handlers/aliases_test.go index c641ab8..a9273b3 100644 --- a/web/handlers/aliases_test.go +++ b/web/handlers/aliases_test.go @@ -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) +} diff --git a/web/handlers/auth/withssb.go b/web/handlers/auth/withssb.go index 35c65a6..7880b77 100644 --- a/web/handlers/auth/withssb.go +++ b/web/handlers/auth/withssb.go @@ -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) diff --git a/web/handlers/auth_test.go b/web/handlers/auth_test.go index d9f75c5..b6a9062 100644 --- a/web/handlers/auth_test.go +++ b/web/handlers/auth_test.go @@ -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) +} diff --git a/web/handlers/invites.go b/web/handlers/invites.go index 6d2f606..ce13a9a 100644 --- a/web/handlers/invites.go +++ b/web/handlers/invites.go @@ -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) diff --git a/web/handlers/invites_test.go b/web/handlers/invites_test.go index 3e8f314..3862d56 100644 --- a/web/handlers/invites_test.go +++ b/web/handlers/invites_test.go @@ -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)