mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
feat: improve login flow
This commit is contained in:
@ -19,12 +19,9 @@ package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-appdir"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
@ -73,11 +70,7 @@ func newConfig(appName, version, revision, cacheVersion string, appDirs, appDirs
|
||||
apiConfig: &pmapi.ClientConfig{
|
||||
AppVersion: strings.Title(appName) + "_" + version,
|
||||
ClientID: appName,
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
},
|
||||
SentryDSN: "https://bacfb56338a7471a9fede610046afdda:ab437b0d13f54602a0f5feb684e6d319@api.protonmail.ch/reports/sentry/8",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,11 +118,18 @@ type Auth struct {
|
||||
TwoFA *TwoFactorInfo `json:"2FA,omitempty"`
|
||||
}
|
||||
|
||||
// UID returns the session UID from the Auth.
|
||||
// Only Auths generated from the /auth route will have the UID.
|
||||
// Auths generated from /auth/refresh are not required to.
|
||||
func (s *Auth) UID() string {
|
||||
return s.uid
|
||||
}
|
||||
|
||||
func (s *Auth) GenToken() string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v:%v", s.UID(), s.RefreshToken)
|
||||
}
|
||||
|
||||
@ -147,7 +154,9 @@ type AuthRes struct {
|
||||
|
||||
AccessToken string
|
||||
TokenType string
|
||||
UID string
|
||||
|
||||
// UID is the session UID. This is only present in an initial Auth (/auth), not in a refreshed Auth (/auth/refresh).
|
||||
UID string
|
||||
|
||||
ServerProof string
|
||||
}
|
||||
@ -196,18 +205,21 @@ type AuthRefreshReq struct {
|
||||
}
|
||||
|
||||
func (c *Client) sendAuth(auth *Auth) {
|
||||
go func() {
|
||||
c.log.Debug("Client is sending auth to ClientManager")
|
||||
c.log.Debug("Client is sending auth to ClientManager")
|
||||
|
||||
if auth != nil {
|
||||
// UID is only provided in the initial /auth, not during /auth/refresh
|
||||
if auth.UID() != "" {
|
||||
c.uid = auth.UID()
|
||||
}
|
||||
c.accessToken = auth.accessToken
|
||||
}
|
||||
|
||||
go func() {
|
||||
c.cm.getClientAuthChannel() <- ClientAuth{
|
||||
UserID: c.userID,
|
||||
Auth: auth,
|
||||
}
|
||||
|
||||
if auth != nil {
|
||||
c.uid = auth.UID()
|
||||
c.accessToken = auth.accessToken
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@ -446,6 +458,7 @@ func (c *Client) AuthRefresh(uidAndRefreshToken string) (auth *Auth, err error)
|
||||
}
|
||||
|
||||
auth = res.getAuth()
|
||||
|
||||
c.sendAuth(auth)
|
||||
|
||||
return auth, err
|
||||
@ -456,6 +469,8 @@ func (c *Client) Logout() {
|
||||
c.cm.LogoutClient(c.userID)
|
||||
}
|
||||
|
||||
// TODO: Need a method like IsConnected() to be able to detect whether a client is logged in or not.
|
||||
|
||||
// logout logs the current user out.
|
||||
func (c *Client) logout() (err error) {
|
||||
req, err := c.NewRequest("DELETE", "/auth", nil)
|
||||
|
||||
@ -279,6 +279,7 @@ func TestClient_AuthRefresh(t *testing.T) {
|
||||
|
||||
exp := &Auth{}
|
||||
*exp = *testAuth
|
||||
exp.uid = "" // AuthRefresh will not return UID (only Auth returns the UID).
|
||||
exp.accessToken = testAccessToken
|
||||
exp.KeySalt = ""
|
||||
exp.EventID = ""
|
||||
@ -329,12 +330,20 @@ func TestClient_Logout(t *testing.T) {
|
||||
},
|
||||
)
|
||||
defer finish()
|
||||
|
||||
c.uid = testUID
|
||||
c.accessToken = testAccessToken
|
||||
|
||||
c.Logout()
|
||||
|
||||
// TODO: Check that the client is logged out and sensitive data is cleared eventually.
|
||||
r.Eventually(t, func() bool {
|
||||
// TODO: Use a method like IsConnected() which returns whether the client was logged out or not.
|
||||
return c.accessToken == "" &&
|
||||
c.uid == "" &&
|
||||
c.kr == nil &&
|
||||
c.addresses == nil &&
|
||||
c.user == nil
|
||||
}, 10*time.Second, 10*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestClient_DoUnauthorized(t *testing.T) {
|
||||
@ -354,7 +363,6 @@ func TestClient_DoUnauthorized(t *testing.T) {
|
||||
|
||||
c.uid = testUID
|
||||
c.accessToken = testAccessTokenOld
|
||||
c.expiresAt = aLongTimeAgo
|
||||
c.cm.tokens[c.userID] = testUID + ":" + testRefreshToken
|
||||
|
||||
req, err := c.NewRequest("GET", "/", nil)
|
||||
|
||||
@ -79,11 +79,6 @@ type ClientConfig struct {
|
||||
// The sentry DSN.
|
||||
SentryDSN string
|
||||
|
||||
// Transport specifies the mechanism by which individual HTTP requests are made.
|
||||
// If nil, http.DefaultTransport is used.
|
||||
// TODO: This could be removed entirely and set in the client manager via SetClientRoundTripper.
|
||||
Transport http.RoundTripper
|
||||
|
||||
// Timeout specifies the timeout from request to getting response headers to our API.
|
||||
// Passed to http.Client, empty means no timeout.
|
||||
Timeout time.Duration
|
||||
@ -120,7 +115,7 @@ type Client struct {
|
||||
func newClient(cm *ClientManager, userID string) *Client {
|
||||
return &Client{
|
||||
cm: cm,
|
||||
hc: getHTTPClient(cm.GetConfig()),
|
||||
hc: getHTTPClient(cm.GetConfig(), cm.GetRoundTripper()),
|
||||
userID: userID,
|
||||
requestLocker: &sync.Mutex{},
|
||||
keyLocker: &sync.Mutex{},
|
||||
@ -128,32 +123,12 @@ func newClient(cm *ClientManager, userID string) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
// getHTTPClient returns a http client configured by the given client config.
|
||||
func getHTTPClient(cfg *ClientConfig) (hc *http.Client) {
|
||||
hc = &http.Client{Timeout: cfg.Timeout}
|
||||
|
||||
if cfg.Transport == nil {
|
||||
if defaultTransport != nil {
|
||||
hc.Transport = defaultTransport
|
||||
}
|
||||
return
|
||||
// getHTTPClient returns a http client configured by the given client config and using the given transport.
|
||||
func getHTTPClient(cfg *ClientConfig, rt http.RoundTripper) (hc *http.Client) {
|
||||
return &http.Client{
|
||||
Timeout: cfg.Timeout,
|
||||
Transport: rt,
|
||||
}
|
||||
|
||||
// In future use Clone here.
|
||||
// https://go-review.googlesource.com/c/go/+/174597/
|
||||
if cfgTransport, ok := cfg.Transport.(*http.Transport); ok {
|
||||
transport := &http.Transport{}
|
||||
*transport = *cfgTransport //nolint
|
||||
if transport.Proxy == nil {
|
||||
transport.Proxy = http.ProxyFromEnvironment
|
||||
}
|
||||
hc.Transport = transport
|
||||
return
|
||||
}
|
||||
|
||||
hc.Transport = cfg.Transport
|
||||
|
||||
return hc
|
||||
}
|
||||
|
||||
// Do makes an API request. It does not check for HTTP status code errors.
|
||||
|
||||
@ -36,9 +36,8 @@ var testClientConfig = &ClientConfig{
|
||||
MinSpeed: 256,
|
||||
}
|
||||
|
||||
func newTestClient() *Client {
|
||||
c := newClient(NewClientManager(testClientConfig), "tester")
|
||||
return c
|
||||
func newTestClient(cm *ClientManager) *Client {
|
||||
return cm.GetClient("tester")
|
||||
}
|
||||
|
||||
func TestClient_Do(t *testing.T) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package pmapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@ -10,27 +11,31 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var proxyUseDuration = 24 * time.Hour
|
||||
var defaultProxyUseDuration = 24 * time.Hour
|
||||
|
||||
// ClientManager is a manager of clients.
|
||||
type ClientManager struct {
|
||||
config *ClientConfig
|
||||
config *ClientConfig
|
||||
roundTripper http.RoundTripper
|
||||
|
||||
clients map[string]*Client
|
||||
clientsLocker sync.Locker
|
||||
|
||||
tokens map[string]string
|
||||
tokenExpirations map[string]*tokenExpiration
|
||||
tokensLocker sync.Locker
|
||||
tokens map[string]string
|
||||
tokensLocker sync.Locker
|
||||
|
||||
url string
|
||||
urlLocker sync.Locker
|
||||
expirations map[string]*tokenExpiration
|
||||
expirationsLocker sync.Locker
|
||||
|
||||
host, scheme string
|
||||
hostLocker sync.Locker
|
||||
|
||||
bridgeAuths chan ClientAuth
|
||||
clientAuths chan ClientAuth
|
||||
|
||||
allowProxy bool
|
||||
proxyProvider *proxyProvider
|
||||
allowProxy bool
|
||||
proxyProvider *proxyProvider
|
||||
proxyUseDuration time.Duration
|
||||
}
|
||||
|
||||
type ClientAuth struct {
|
||||
@ -50,22 +55,27 @@ func NewClientManager(config *ClientConfig) (cm *ClientManager) {
|
||||
}
|
||||
|
||||
cm = &ClientManager{
|
||||
config: config,
|
||||
config: config,
|
||||
roundTripper: http.DefaultTransport,
|
||||
|
||||
clients: make(map[string]*Client),
|
||||
clientsLocker: &sync.Mutex{},
|
||||
|
||||
tokens: make(map[string]string),
|
||||
tokenExpirations: make(map[string]*tokenExpiration),
|
||||
tokensLocker: &sync.Mutex{},
|
||||
tokens: make(map[string]string),
|
||||
tokensLocker: &sync.Mutex{},
|
||||
|
||||
url: RootURL,
|
||||
urlLocker: &sync.Mutex{},
|
||||
expirations: make(map[string]*tokenExpiration),
|
||||
expirationsLocker: &sync.Mutex{},
|
||||
|
||||
host: RootURL,
|
||||
scheme: RootScheme,
|
||||
hostLocker: &sync.Mutex{},
|
||||
|
||||
bridgeAuths: make(chan ClientAuth),
|
||||
clientAuths: make(chan ClientAuth),
|
||||
|
||||
proxyProvider: newProxyProvider(dohProviders, proxyQuery),
|
||||
proxyProvider: newProxyProvider(dohProviders, proxyQuery),
|
||||
proxyUseDuration: defaultProxyUseDuration,
|
||||
}
|
||||
|
||||
go cm.forwardClientAuths()
|
||||
@ -73,10 +83,14 @@ func NewClientManager(config *ClientConfig) (cm *ClientManager) {
|
||||
return
|
||||
}
|
||||
|
||||
// SetClientRoundTripper sets the roundtripper used by clients created by this client manager.
|
||||
func (cm *ClientManager) SetClientRoundTripper(rt http.RoundTripper) {
|
||||
logrus.Info("Setting client roundtripper")
|
||||
cm.config.Transport = rt
|
||||
// SetRoundTripper sets the roundtripper used by clients created by this client manager.
|
||||
func (cm *ClientManager) SetRoundTripper(rt http.RoundTripper) {
|
||||
cm.roundTripper = rt
|
||||
}
|
||||
|
||||
// GetRoundTripper sets the roundtripper used by clients created by this client manager.
|
||||
func (cm *ClientManager) GetRoundTripper() (rt http.RoundTripper) {
|
||||
return cm.roundTripper
|
||||
}
|
||||
|
||||
// GetClient returns a client for the given userID.
|
||||
@ -91,6 +105,17 @@ func (cm *ClientManager) GetClient(userID string) *Client {
|
||||
return cm.clients[userID]
|
||||
}
|
||||
|
||||
// GetAnonymousClient returns an anonymous client. It replaces any anonymous client that was already created.
|
||||
func (cm *ClientManager) GetAnonymousClient() *Client {
|
||||
if client, ok := cm.clients[""]; ok {
|
||||
client.Logout()
|
||||
}
|
||||
|
||||
cm.clients[""] = newClient(cm, "")
|
||||
|
||||
return cm.clients[""]
|
||||
}
|
||||
|
||||
// LogoutClient logs out the client with the given userID and ensures its sensitive data is successfully cleared.
|
||||
func (cm *ClientManager) LogoutClient(userID string) {
|
||||
client, ok := cm.clients[userID]
|
||||
@ -104,7 +129,6 @@ func (cm *ClientManager) LogoutClient(userID string) {
|
||||
go func() {
|
||||
if err := client.logout(); err != nil {
|
||||
// TODO: Try again! This should loop until it succeeds (might fail the first time due to internet).
|
||||
logrus.WithError(err).Error("Client logout failed, not trying again")
|
||||
}
|
||||
client.clearSensitiveData()
|
||||
cm.clearToken(userID)
|
||||
@ -113,52 +137,69 @@ func (cm *ClientManager) LogoutClient(userID string) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetRootURL returns the root URL to make requests to.
|
||||
// It does not include the protocol i.e. no "https://".
|
||||
func (cm *ClientManager) GetRootURL() string {
|
||||
cm.urlLocker.Lock()
|
||||
defer cm.urlLocker.Unlock()
|
||||
// GetHost returns the host to make requests to.
|
||||
// It does not include the protocol i.e. no "https://" (use GetScheme for that).
|
||||
func (cm *ClientManager) GetHost() string {
|
||||
cm.hostLocker.Lock()
|
||||
defer cm.hostLocker.Unlock()
|
||||
|
||||
return cm.url
|
||||
return cm.host
|
||||
}
|
||||
|
||||
// GetScheme returns the scheme with which to make requests to the host.
|
||||
func (cm *ClientManager) GetScheme() string {
|
||||
cm.hostLocker.Lock()
|
||||
defer cm.hostLocker.Unlock()
|
||||
|
||||
return cm.scheme
|
||||
}
|
||||
|
||||
// GetRootURL returns the full root URL (scheme+host).
|
||||
func (cm *ClientManager) GetRootURL() string {
|
||||
cm.hostLocker.Lock()
|
||||
defer cm.hostLocker.Unlock()
|
||||
|
||||
return fmt.Sprintf("%v://%v", cm.scheme, cm.host)
|
||||
}
|
||||
|
||||
// IsProxyAllowed returns whether the user has allowed us to switch to a proxy if need be.
|
||||
func (cm *ClientManager) IsProxyAllowed() bool {
|
||||
cm.urlLocker.Lock()
|
||||
defer cm.urlLocker.Unlock()
|
||||
cm.hostLocker.Lock()
|
||||
defer cm.hostLocker.Unlock()
|
||||
|
||||
return cm.allowProxy
|
||||
}
|
||||
|
||||
// AllowProxy allows the client manager to switch clients over to a proxy if need be.
|
||||
func (cm *ClientManager) AllowProxy() {
|
||||
cm.urlLocker.Lock()
|
||||
defer cm.urlLocker.Unlock()
|
||||
cm.hostLocker.Lock()
|
||||
defer cm.hostLocker.Unlock()
|
||||
|
||||
cm.allowProxy = true
|
||||
}
|
||||
|
||||
// DisallowProxy prevents the client manager from switching clients over to a proxy if need be.
|
||||
func (cm *ClientManager) DisallowProxy() {
|
||||
cm.urlLocker.Lock()
|
||||
defer cm.urlLocker.Unlock()
|
||||
cm.hostLocker.Lock()
|
||||
defer cm.hostLocker.Unlock()
|
||||
|
||||
cm.allowProxy = false
|
||||
cm.url = RootURL
|
||||
cm.host = RootURL
|
||||
}
|
||||
|
||||
// IsProxyEnabled returns whether we are currently proxying requests.
|
||||
func (cm *ClientManager) IsProxyEnabled() bool {
|
||||
cm.urlLocker.Lock()
|
||||
defer cm.urlLocker.Unlock()
|
||||
cm.hostLocker.Lock()
|
||||
defer cm.hostLocker.Unlock()
|
||||
|
||||
return cm.url != RootURL
|
||||
return cm.host != RootURL
|
||||
}
|
||||
|
||||
// FindProxy returns a usable proxy server.
|
||||
// SwitchToProxy returns a usable proxy server.
|
||||
// TODO: Perhaps the name could be better -- we aren't only switching to a proxy but also to the standard API.
|
||||
func (cm *ClientManager) SwitchToProxy() (proxy string, err error) {
|
||||
cm.urlLocker.Lock()
|
||||
defer cm.urlLocker.Unlock()
|
||||
cm.hostLocker.Lock()
|
||||
defer cm.hostLocker.Unlock()
|
||||
|
||||
logrus.Info("Attempting to switch to a proxy")
|
||||
|
||||
@ -169,9 +210,16 @@ func (cm *ClientManager) SwitchToProxy() (proxy string, err error) {
|
||||
|
||||
logrus.WithField("proxy", proxy).Info("Switching to a proxy")
|
||||
|
||||
cm.url = proxy
|
||||
// If the host is currently the RootURL, it's the first time we are enabling a proxy.
|
||||
// This means we want to disable it again in 24 hours.
|
||||
if cm.host == RootURL {
|
||||
go func() {
|
||||
<-time.After(cm.proxyUseDuration)
|
||||
cm.host = RootURL
|
||||
}()
|
||||
}
|
||||
|
||||
// TODO: Disable again after 24 hours.
|
||||
cm.host = proxy
|
||||
|
||||
return
|
||||
}
|
||||
@ -183,6 +231,9 @@ func (cm *ClientManager) GetConfig() *ClientConfig {
|
||||
|
||||
// GetToken returns the token for the given userID.
|
||||
func (cm *ClientManager) GetToken(userID string) string {
|
||||
cm.tokensLocker.Lock()
|
||||
defer cm.tokensLocker.Unlock()
|
||||
|
||||
return cm.tokens[userID]
|
||||
}
|
||||
|
||||
@ -208,6 +259,11 @@ func (cm *ClientManager) forwardClientAuths() {
|
||||
|
||||
// setToken sets the token for the given userID with the given expiration time.
|
||||
func (cm *ClientManager) setToken(userID, token string, expiration time.Duration) {
|
||||
// We don't want to set tokens of anonymous clients.
|
||||
if userID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cm.tokensLocker.Lock()
|
||||
defer cm.tokensLocker.Unlock()
|
||||
|
||||
@ -221,12 +277,15 @@ func (cm *ClientManager) setToken(userID, token string, expiration time.Duration
|
||||
// setTokenExpiration will ensure the token is refreshed if it expires.
|
||||
// If the token already has an expiration time set, it is replaced.
|
||||
func (cm *ClientManager) setTokenExpiration(userID string, expiration time.Duration) {
|
||||
if exp, ok := cm.tokenExpirations[userID]; ok {
|
||||
cm.expirationsLocker.Lock()
|
||||
defer cm.expirationsLocker.Unlock()
|
||||
|
||||
if exp, ok := cm.expirations[userID]; ok {
|
||||
exp.timer.Stop()
|
||||
close(exp.cancel)
|
||||
}
|
||||
|
||||
cm.tokenExpirations[userID] = &tokenExpiration{
|
||||
cm.expirations[userID] = &tokenExpiration{
|
||||
timer: time.NewTimer(expiration),
|
||||
cancel: make(chan struct{}),
|
||||
}
|
||||
@ -262,7 +321,7 @@ func (cm *ClientManager) handleClientAuth(ca ClientAuth) {
|
||||
}
|
||||
|
||||
func (cm *ClientManager) watchTokenExpiration(userID string) {
|
||||
expiration := cm.tokenExpirations[userID]
|
||||
expiration := cm.expirations[userID]
|
||||
|
||||
select {
|
||||
case <-expiration.timer.C:
|
||||
@ -270,6 +329,6 @@ func (cm *ClientManager) watchTokenExpiration(userID string) {
|
||||
cm.clients[userID].AuthRefresh(cm.tokens[userID])
|
||||
|
||||
case <-expiration.cancel:
|
||||
logrus.WithField("userID", userID).Info("Auth was refreshed before it expired, cancelling this watcher")
|
||||
logrus.WithField("userID", userID).Info("Auth was refreshed before it expired")
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,8 +26,12 @@ import (
|
||||
//
|
||||
// This can be changed using build flags: pmapi_local for "localhost/api", pmapi_dev or pmapi_prod.
|
||||
// Default is pmapi_prod.
|
||||
//
|
||||
// It should not contain the protocol! The protocol should be in RootScheme.
|
||||
var RootURL = "api.protonmail.ch" //nolint[gochecknoglobals]
|
||||
|
||||
var RootScheme = "https"
|
||||
|
||||
// CurrentUserAgent is the default User-Agent for go-pmapi lib. This can be changed to program
|
||||
// version and email client.
|
||||
// e.g. Bridge/1.0.4 (Windows) MicrosoftOutlook/16.0.9330.2087
|
||||
|
||||
@ -21,4 +21,5 @@ package pmapi
|
||||
|
||||
func init() {
|
||||
RootURL = "dev.protonmail.com/api"
|
||||
RootScheme = "https"
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ func init() {
|
||||
// Use port above 1000 which doesn't need root access to start anything on it.
|
||||
// Now the port is rounded pi. :-)
|
||||
RootURL = "127.0.0.1:3142/api"
|
||||
RootScheme = "http"
|
||||
|
||||
// TLS certificate is self-signed
|
||||
defaultTransport = &http.Transport{
|
||||
|
||||
@ -654,7 +654,7 @@ var testCardsCleartext = []Card{
|
||||
}
|
||||
|
||||
func TestClient_Encrypt(t *testing.T) {
|
||||
c := newTestClient()
|
||||
c := newTestClient(NewClientManager(testClientConfig))
|
||||
c.kr = testPrivateKeyRing
|
||||
|
||||
cardEncrypted, err := c.EncryptAndSignCards(testCardsCleartext)
|
||||
@ -668,7 +668,7 @@ func TestClient_Encrypt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClient_Decrypt(t *testing.T) {
|
||||
c := newTestClient()
|
||||
c := newTestClient(NewClientManager(testClientConfig))
|
||||
c.kr = testPrivateKeyRing
|
||||
|
||||
cardCleartext, err := c.DecryptAndVerifyCards(testCardsEncrypted)
|
||||
|
||||
@ -297,7 +297,7 @@ func (p *DialerWithPinning) dialWithProxyFallback(network, address string) (conn
|
||||
// If DoH is not allowed, give up. Or, if we are dialing something other than the API
|
||||
// (e.g. we dial protonmail.com/... to check for updates), there's also no point in
|
||||
// continuing since a proxy won't help us reach that.
|
||||
if !p.cm.IsProxyAllowed() || host != p.cm.GetRootURL() {
|
||||
if !p.cm.IsProxyAllowed() || host != p.cm.GetHost() {
|
||||
p.log.WithField("address", address).Debug("Aborting dial, cannot switch to a proxy")
|
||||
return
|
||||
}
|
||||
|
||||
@ -23,26 +23,27 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const liveAPI = "https://api.protonmail.ch"
|
||||
const liveAPI = "api.protonmail.ch"
|
||||
|
||||
var testLiveConfig = &ClientConfig{
|
||||
AppVersion: "Bridge_1.2.4-test",
|
||||
ClientID: "Bridge",
|
||||
}
|
||||
|
||||
func newTestDialerWithPinning() (*int, *DialerWithPinning) {
|
||||
func setTestDialerWithPinning(cm *ClientManager) (*int, *DialerWithPinning) {
|
||||
called := 0
|
||||
p := NewPMAPIPinning(testLiveConfig.AppVersion)
|
||||
p := NewDialerWithPinning(cm, testLiveConfig.AppVersion)
|
||||
p.ReportCertIssueLocal = func() { called++ }
|
||||
testLiveConfig.Transport = p.TransportWithPinning()
|
||||
cm.SetRoundTripper(p.TransportWithPinning())
|
||||
return &called, p
|
||||
}
|
||||
|
||||
func TestTLSPinValid(t *testing.T) {
|
||||
called, _ := newTestDialerWithPinning()
|
||||
|
||||
RootURL = liveAPI
|
||||
client := newClient(NewClientManager(testLiveConfig), "pmapi"+t.Name())
|
||||
cm := NewClientManager(testLiveConfig)
|
||||
cm.host = liveAPI
|
||||
RootScheme = "https"
|
||||
called, _ := setTestDialerWithPinning(cm)
|
||||
client := cm.GetClient("pmapi" + t.Name())
|
||||
|
||||
_, err := client.AuthInfo("this.address.is.disabled")
|
||||
Ok(t, err)
|
||||
@ -51,12 +52,13 @@ func TestTLSPinValid(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTLSPinBackup(t *testing.T) {
|
||||
called, p := newTestDialerWithPinning()
|
||||
cm := NewClientManager(testLiveConfig)
|
||||
cm.host = liveAPI
|
||||
called, p := setTestDialerWithPinning(cm)
|
||||
p.report.KnownPins[1] = p.report.KnownPins[0]
|
||||
p.report.KnownPins[0] = ""
|
||||
|
||||
RootURL = liveAPI
|
||||
client := newClient(NewClientManager(testLiveConfig), "pmapi"+t.Name())
|
||||
client := cm.GetClient("pmapi" + t.Name())
|
||||
|
||||
_, err := client.AuthInfo("this.address.is.disabled")
|
||||
Ok(t, err)
|
||||
@ -65,19 +67,21 @@ func TestTLSPinBackup(t *testing.T) {
|
||||
}
|
||||
|
||||
func _TestTLSPinNoMatch(t *testing.T) { // nolint[unused]
|
||||
called, p := newTestDialerWithPinning()
|
||||
cm := NewClientManager(testLiveConfig)
|
||||
cm.host = liveAPI
|
||||
|
||||
called, p := setTestDialerWithPinning(cm)
|
||||
for i := 0; i < len(p.report.KnownPins); i++ {
|
||||
p.report.KnownPins[i] = "testing"
|
||||
}
|
||||
|
||||
RootURL = liveAPI
|
||||
client := newClient(NewClientManager(testLiveConfig), "pmapi"+t.Name())
|
||||
client := cm.GetClient("pmapi" + t.Name())
|
||||
|
||||
_, err := client.AuthInfo("this.address.is.disabled")
|
||||
Ok(t, err)
|
||||
|
||||
// check that it will be called only once per session
|
||||
client = newClient(NewClientManager(testLiveConfig), "pmapi"+t.Name())
|
||||
client = cm.GetClient("pmapi" + t.Name())
|
||||
_, err = client.AuthInfo("this.address.is.disabled")
|
||||
Ok(t, err)
|
||||
|
||||
@ -85,20 +89,22 @@ func _TestTLSPinNoMatch(t *testing.T) { // nolint[unused]
|
||||
}
|
||||
|
||||
func _TestTLSPinInvalid(t *testing.T) { // nolint[unused]
|
||||
cm := NewClientManager(testLiveConfig)
|
||||
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSONResponsefromFile(t, w, "/auth/info/post_response.json", 0)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
called, _ := newTestDialerWithPinning()
|
||||
called, _ := setTestDialerWithPinning(cm)
|
||||
|
||||
client := newClient(NewClientManager(testLiveConfig), "pmapi"+t.Name())
|
||||
client := cm.GetClient("pmapi" + t.Name())
|
||||
|
||||
RootURL = liveAPI
|
||||
cm.host = liveAPI
|
||||
_, err := client.AuthInfo("this.address.is.disabled")
|
||||
Ok(t, err)
|
||||
|
||||
RootURL = ts.URL
|
||||
cm.host = ts.URL
|
||||
_, err = client.AuthInfo("this.address.is.disabled")
|
||||
Assert(t, err != nil, "error is expected but have %v", err)
|
||||
|
||||
@ -106,20 +112,23 @@ func _TestTLSPinInvalid(t *testing.T) { // nolint[unused]
|
||||
}
|
||||
|
||||
func _TestTLSSignedCertWrongPublicKey(t *testing.T) { // nolint[unused]
|
||||
_, dialer := newTestDialerWithPinning()
|
||||
cm := NewClientManager(testLiveConfig)
|
||||
_, dialer := setTestDialerWithPinning(cm)
|
||||
_, err := dialer.dialAndCheckFingerprints("tcp", "rsa4096.badssl.com:443")
|
||||
Assert(t, err != nil, "expected dial to fail because of wrong public key: ", err.Error())
|
||||
}
|
||||
|
||||
func _TestTLSSignedCertTrustedPublicKey(t *testing.T) { // nolint[unused]
|
||||
_, dialer := newTestDialerWithPinning()
|
||||
cm := NewClientManager(testLiveConfig)
|
||||
_, dialer := setTestDialerWithPinning(cm)
|
||||
dialer.report.KnownPins = append(dialer.report.KnownPins, `pin-sha256="W8/42Z0ffufwnHIOSndT+eVzBJSC0E8uTIC8O6mEliQ="`)
|
||||
_, err := dialer.dialAndCheckFingerprints("tcp", "rsa4096.badssl.com:443")
|
||||
Assert(t, err == nil, "expected dial to succeed because public key is known and cert is signed by CA: ", err.Error())
|
||||
}
|
||||
|
||||
func _TestTLSSelfSignedCertTrustedPublicKey(t *testing.T) { // nolint[unused]
|
||||
_, dialer := newTestDialerWithPinning()
|
||||
cm := NewClientManager(testLiveConfig)
|
||||
_, dialer := setTestDialerWithPinning(cm)
|
||||
dialer.report.KnownPins = append(dialer.report.KnownPins, `pin-sha256="9SLklscvzMYj8f+52lp5ze/hY0CFHyLSPQzSpYYIBm8="`)
|
||||
_, err := dialer.dialAndCheckFingerprints("tcp", "self-signed.badssl.com:443")
|
||||
Assert(t, err == nil, "expected dial to succeed because public key is known despite cert being self-signed: ", err.Error())
|
||||
|
||||
@ -72,8 +72,9 @@ func newProxyProvider(providers []string, query string) (p *proxyProvider) { //
|
||||
return
|
||||
}
|
||||
|
||||
// findProxy returns a new proxy domain which is not equal to the current RootURL.
|
||||
// findProxy returns a new working proxy domain. This includes the standard API.
|
||||
// It returns an error if the process takes longer than ProxySearchTime.
|
||||
// TODO: Perhaps the name can be better -- we might also return the standard API.
|
||||
func (p *proxyProvider) findProxy() (proxy string, err error) {
|
||||
if time.Now().Before(p.lastLookup.Add(proxyLookupWait)) {
|
||||
return "", errors.New("not looking for a proxy, too soon")
|
||||
@ -88,6 +89,12 @@ func (p *proxyProvider) findProxy() (proxy string, err error) {
|
||||
logrus.WithError(err).Warn("Failed to refresh proxy cache, cache may be out of date")
|
||||
}
|
||||
|
||||
// We want to switch back to the RootURL if possible.
|
||||
if p.canReach(RootURL) {
|
||||
proxyResult <- RootURL
|
||||
return
|
||||
}
|
||||
|
||||
for _, proxy := range p.proxyCache {
|
||||
if p.canReach(proxy) {
|
||||
proxyResult <- proxy
|
||||
@ -114,6 +121,7 @@ func (p *proxyProvider) findProxy() (proxy string, err error) {
|
||||
}
|
||||
|
||||
// refreshProxyCache loads the latest proxies from the known providers.
|
||||
// It includes the standard API.
|
||||
func (p *proxyProvider) refreshProxyCache() error {
|
||||
logrus.Info("Refreshing proxy cache")
|
||||
|
||||
@ -121,9 +129,6 @@ func (p *proxyProvider) refreshProxyCache() error {
|
||||
if proxies, err := p.dohLookup(p.query, provider); err == nil {
|
||||
p.proxyCache = proxies
|
||||
|
||||
// We also want to allow bridge to switch back to the standard API at any time.
|
||||
p.proxyCache = append(p.proxyCache, RootURL)
|
||||
|
||||
logrus.WithField("proxies", proxies).Info("Available proxies")
|
||||
|
||||
return nil
|
||||
|
||||
@ -122,23 +122,27 @@ func TestProxyProvider_UseProxy(t *testing.T) {
|
||||
blockAPI()
|
||||
defer unblockAPI()
|
||||
|
||||
cm := NewClientManager(testClientConfig)
|
||||
|
||||
proxy := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer proxy.Close()
|
||||
|
||||
p := newProxyProvider([]string{"not used"}, "not used")
|
||||
cm.proxyProvider = p
|
||||
|
||||
p.dohLookup = func(q, p string) ([]string, error) { return []string{proxy.URL}, nil }
|
||||
|
||||
url, err := p.findProxy()
|
||||
url, err := cm.SwitchToProxy()
|
||||
require.NoError(t, err)
|
||||
|
||||
p.useProxy(url)
|
||||
require.Equal(t, proxy.URL, GlobalGetRootURL())
|
||||
require.Equal(t, proxy.URL, url)
|
||||
require.Equal(t, proxy.URL, cm.GetHost())
|
||||
}
|
||||
|
||||
func TestProxyProvider_UseProxy_MultipleTimes(t *testing.T) {
|
||||
blockAPI()
|
||||
defer unblockAPI()
|
||||
|
||||
cm := NewClientManager(testClientConfig)
|
||||
|
||||
proxy1 := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer proxy1.Close()
|
||||
proxy2 := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
@ -147,101 +151,106 @@ func TestProxyProvider_UseProxy_MultipleTimes(t *testing.T) {
|
||||
defer proxy3.Close()
|
||||
|
||||
p := newProxyProvider([]string{"not used"}, "not used")
|
||||
cm.proxyProvider = p
|
||||
|
||||
p.dohLookup = func(q, p string) ([]string, error) { return []string{proxy1.URL}, nil }
|
||||
url, err := p.findProxy()
|
||||
url, err := cm.SwitchToProxy()
|
||||
require.NoError(t, err)
|
||||
p.useProxy(url)
|
||||
require.Equal(t, proxy1.URL, GlobalGetRootURL())
|
||||
require.Equal(t, proxy1.URL, url)
|
||||
require.Equal(t, proxy1.URL, cm.GetHost())
|
||||
|
||||
// Have to wait so as to not get rejected.
|
||||
time.Sleep(proxyLookupWait)
|
||||
|
||||
p.dohLookup = func(q, p string) ([]string, error) { return []string{proxy2.URL}, nil }
|
||||
url, err = p.findProxy()
|
||||
url, err = cm.SwitchToProxy()
|
||||
require.NoError(t, err)
|
||||
p.useProxy(url)
|
||||
require.Equal(t, proxy2.URL, GlobalGetRootURL())
|
||||
require.Equal(t, proxy2.URL, url)
|
||||
require.Equal(t, proxy2.URL, cm.GetHost())
|
||||
|
||||
// Have to wait so as to not get rejected.
|
||||
time.Sleep(proxyLookupWait)
|
||||
|
||||
p.dohLookup = func(q, p string) ([]string, error) { return []string{proxy3.URL}, nil }
|
||||
url, err = p.findProxy()
|
||||
url, err = cm.SwitchToProxy()
|
||||
require.NoError(t, err)
|
||||
p.useProxy(url)
|
||||
require.Equal(t, proxy3.URL, GlobalGetRootURL())
|
||||
require.Equal(t, proxy3.URL, url)
|
||||
require.Equal(t, proxy3.URL, cm.GetHost())
|
||||
}
|
||||
|
||||
func TestProxyProvider_UseProxy_RevertAfterTime(t *testing.T) {
|
||||
blockAPI()
|
||||
defer unblockAPI()
|
||||
|
||||
cm := NewClientManager(testClientConfig)
|
||||
|
||||
proxy := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer proxy.Close()
|
||||
|
||||
p := newProxyProvider([]string{"not used"}, "not used")
|
||||
p.useDuration = time.Second
|
||||
p.dohLookup = func(q, p string) ([]string, error) { return []string{proxy.URL}, nil }
|
||||
cm.proxyProvider = p
|
||||
cm.proxyUseDuration = time.Second
|
||||
|
||||
url, err := p.findProxy()
|
||||
p.dohLookup = func(q, p string) ([]string, error) { return []string{proxy.URL}, nil }
|
||||
url, err := cm.SwitchToProxy()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, proxy.URL, url)
|
||||
|
||||
p.useProxy(url)
|
||||
require.Equal(t, proxy.URL, GlobalGetRootURL())
|
||||
require.Equal(t, proxy.URL, cm.GetHost())
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
require.Equal(t, globalOriginalURL, GlobalGetRootURL())
|
||||
require.Equal(t, RootURL, cm.GetHost())
|
||||
}
|
||||
|
||||
func TestProxyProvider_UseProxy_RevertIfProxyStopsWorkingAndOriginalAPIIsReachable(t *testing.T) {
|
||||
// Don't block the API here because we want it to be working so the test can find it.
|
||||
blockAPI()
|
||||
defer unblockAPI()
|
||||
|
||||
cm := NewClientManager(testClientConfig)
|
||||
|
||||
proxy := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer proxy.Close()
|
||||
|
||||
p := newProxyProvider([]string{"not used"}, "not used")
|
||||
p.dohLookup = func(q, p string) ([]string, error) { return []string{proxy.URL}, nil }
|
||||
cm.proxyProvider = p
|
||||
|
||||
url, err := p.findProxy()
|
||||
p.dohLookup = func(q, p string) ([]string, error) { return []string{proxy.URL}, nil }
|
||||
url, err := cm.SwitchToProxy()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, proxy.URL, url)
|
||||
require.Equal(t, proxy.URL, cm.GetHost())
|
||||
|
||||
p.useProxy(url)
|
||||
require.Equal(t, proxy.URL, GlobalGetRootURL())
|
||||
|
||||
// Simulate that the proxy stops working.
|
||||
// Simulate that the proxy stops working and that the standard api is reachable again.
|
||||
proxy.Close()
|
||||
unblockAPI()
|
||||
time.Sleep(proxyLookupWait)
|
||||
|
||||
// We should now find the original API URL if it is working again.
|
||||
url, err = p.findProxy()
|
||||
url, err = cm.SwitchToProxy()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, globalOriginalURL, url)
|
||||
|
||||
p.useProxy(url)
|
||||
require.Equal(t, globalOriginalURL, GlobalGetRootURL())
|
||||
require.Equal(t, RootURL, url)
|
||||
require.Equal(t, RootURL, cm.GetHost())
|
||||
}
|
||||
|
||||
func TestProxyProvider_UseProxy_FindSecondAlternativeIfFirstFailsAndAPIIsStillBlocked(t *testing.T) {
|
||||
blockAPI()
|
||||
defer unblockAPI()
|
||||
|
||||
cm := NewClientManager(testClientConfig)
|
||||
|
||||
proxy1 := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer proxy1.Close()
|
||||
proxy2 := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer proxy2.Close()
|
||||
|
||||
p := newProxyProvider([]string{"not used"}, "not used")
|
||||
p.dohLookup = func(q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }
|
||||
cm.proxyProvider = p
|
||||
|
||||
// Find a proxy.
|
||||
url, err := p.findProxy()
|
||||
p.dohLookup = func(q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }
|
||||
url, err := cm.SwitchToProxy()
|
||||
require.NoError(t, err)
|
||||
p.useProxy(url)
|
||||
require.Equal(t, proxy1.URL, GlobalGetRootURL())
|
||||
require.Equal(t, proxy1.URL, url)
|
||||
require.Equal(t, proxy1.URL, cm.GetHost())
|
||||
|
||||
// Have to wait so as to not get rejected.
|
||||
time.Sleep(proxyLookupWait)
|
||||
@ -250,10 +259,10 @@ func TestProxyProvider_UseProxy_FindSecondAlternativeIfFirstFailsAndAPIIsStillBl
|
||||
proxy1.Close()
|
||||
|
||||
// Should switch to the second proxy because both the first proxy and the protonmail API are blocked.
|
||||
url, err = p.findProxy()
|
||||
url, err = cm.SwitchToProxy()
|
||||
require.NoError(t, err)
|
||||
p.useProxy(url)
|
||||
require.Equal(t, proxy2.URL, GlobalGetRootURL())
|
||||
require.Equal(t, proxy2.URL, url)
|
||||
require.Equal(t, proxy2.URL, cm.GetHost())
|
||||
}
|
||||
|
||||
func TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
|
||||
@ -289,16 +298,14 @@ func TestProxyProvider_DoHLookup_FindProxyFirstProviderUnreachable(t *testing.T)
|
||||
}
|
||||
|
||||
// testAPIURLBackup is used to hold the globalOriginalURL because we clear it for test purposes and need to restore it.
|
||||
var testAPIURLBackup = globalOriginalURL
|
||||
var testAPIURLBackup = RootURL
|
||||
|
||||
// blockAPI prevents tests from reaching the standard API, forcing them to find a proxy.
|
||||
func blockAPI() {
|
||||
globalSetRootURL("")
|
||||
globalOriginalURL = ""
|
||||
RootURL = ""
|
||||
}
|
||||
|
||||
// unblockAPI allow tests to reach the standard API again.
|
||||
func unblockAPI() {
|
||||
globalOriginalURL = testAPIURLBackup
|
||||
globalSetRootURL(globalOriginalURL)
|
||||
RootURL = testAPIURLBackup
|
||||
}
|
||||
|
||||
@ -28,7 +28,8 @@ import (
|
||||
// NewRequest creates a new request.
|
||||
func (c *Client) NewRequest(method, path string, body io.Reader) (req *http.Request, err error) {
|
||||
// TODO: Support other protocols (localhost needs http not https).
|
||||
req, err = http.NewRequest(method, "https://"+c.cm.GetRootURL()+path, body)
|
||||
req, err = http.NewRequest(method, c.cm.GetRootURL()+path, body)
|
||||
|
||||
if req != nil {
|
||||
req.Header.Set("User-Agent", CurrentUserAgent)
|
||||
}
|
||||
|
||||
@ -25,7 +25,8 @@ import (
|
||||
)
|
||||
|
||||
func TestSentryCrashReport(t *testing.T) {
|
||||
c := newClient(NewClientManager(testClientConfig), "bridgetest")
|
||||
cm := NewClientManager(testClientConfig)
|
||||
c := cm.GetClient("bridgetest")
|
||||
if err := c.ReportSentryCrash(errors.New("Testing crash report - api proxy; goroutines with threads, find origin")); err != nil {
|
||||
t.Fatal("Expected no error while report, but have", err)
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@ -72,15 +73,24 @@ func Equals(tb testing.TB, exp, act interface{}) {
|
||||
// newTestServer is old function and should be replaced everywhere by newTestServerCallbacks.
|
||||
func newTestServer(h http.Handler) (*httptest.Server, *Client) {
|
||||
s := httptest.NewServer(h)
|
||||
RootURL = s.URL
|
||||
|
||||
return s, newTestClient()
|
||||
serverURL, err := url.Parse(s.URL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cm := NewClientManager(testClientConfig)
|
||||
cm.host = serverURL.Host
|
||||
cm.scheme = serverURL.Scheme
|
||||
|
||||
return s, newTestClient(cm)
|
||||
}
|
||||
|
||||
func newTestServerCallbacks(tb testing.TB, callbacks ...func(testing.TB, http.ResponseWriter, *http.Request) string) (func(), *Client) {
|
||||
reqNum := 0
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
file = filepath.Base(file)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reqNum++
|
||||
if reqNum > len(callbacks) {
|
||||
@ -95,7 +105,12 @@ func newTestServerCallbacks(tb testing.TB, callbacks ...func(testing.TB, http.Re
|
||||
writeJSONResponsefromFile(tb, w, response, reqNum-1)
|
||||
}
|
||||
}))
|
||||
RootURL = server.URL
|
||||
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
finish := func() {
|
||||
server.CloseClientConnections() // Closing without waiting for finishing requests.
|
||||
if reqNum != len(callbacks) {
|
||||
@ -106,7 +121,12 @@ func newTestServerCallbacks(tb testing.TB, callbacks ...func(testing.TB, http.Re
|
||||
tb.Error("server failed")
|
||||
}
|
||||
}
|
||||
return finish, newTestClient()
|
||||
|
||||
cm := NewClientManager(testClientConfig)
|
||||
cm.host = serverURL.Host
|
||||
cm.scheme = serverURL.Scheme
|
||||
|
||||
return finish, newTestClient(cm)
|
||||
}
|
||||
|
||||
func checkMethodAndPath(r *http.Request, method, path string) error {
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
"AccessToken": "de0423049b44243afeec7d9c1d99be7b46da1e8a",
|
||||
"ExpiresIn": 360000,
|
||||
"TokenType": "Bearer",
|
||||
"Uid": "differentUID",
|
||||
"UID": "differentUID",
|
||||
"Uid": "729ad6012421d67ad26950dc898bebe3a6e3caa2",
|
||||
"UID": "729ad6012421d67ad26950dc898bebe3a6e3caa2",
|
||||
"Scope": "full mail payments reset keys",
|
||||
"RefreshToken": "b894b4c4f20003f12d486900d8b88c7d68e67235"
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +121,7 @@ func (c *Client) UpdateUser() (user *User, err error) {
|
||||
return user, err
|
||||
}
|
||||
|
||||
// CurrentUser return currently active user or user will be updated.
|
||||
// CurrentUser returns currently active user or user will be updated.
|
||||
func (c *Client) CurrentUser() (user *User, err error) {
|
||||
if c.user != nil && len(c.addresses) != 0 {
|
||||
user = c.user
|
||||
|
||||
Reference in New Issue
Block a user