Compare commits
17 commits
Author | SHA1 | Date | |
---|---|---|---|
|
afc43a0068 | ||
|
a82a4e7290 | ||
|
4f074b181c | ||
|
4f97d7761f | ||
|
f090a034de | ||
|
f04a01a5a0 | ||
|
aac7ef713e | ||
|
f244dab036 | ||
|
b86687a0c5 | ||
|
48c74350e0 | ||
|
9c057a5319 | ||
|
08d57db466 | ||
|
448892cd82 | ||
|
9046be7c4f | ||
|
0c22de5186 | ||
|
4843b91ce7 | ||
|
4dfacd8826 |
42 changed files with 1223 additions and 394 deletions
2
.github/workflows/build-test-release.yml
vendored
2
.github/workflows/build-test-release.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: 1.18
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
/sql.db
|
/sql.db
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
/wallet-sync-server
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
|
||||||
# Make sure to check the documentation at https://goreleaser.com
|
|
||||||
project_name: wallet-sync-server
|
project_name: wallet-sync-server
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
|
|
15
LICENSE
Normal file
15
LICENSE
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 LBRY Inc
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
33
README.md
33
README.md
|
@ -1,13 +1,25 @@
|
||||||
# Running
|
This software is in the pre-testing phase as of now.
|
||||||
|
|
||||||
Install Golang, at least version 1.17. (Please report any dependencies we seemed to have forgotten)
|
# Getting The Binary
|
||||||
|
|
||||||
|
## Prebuilt
|
||||||
|
|
||||||
|
Linux Only. Get the latest [release](https://github.com/lbryio/wallet-sync-server/releases).
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Only tried on Linux. Might work for Windows and Mac. No expectations.
|
||||||
|
|
||||||
|
Install Golang, at least version 1.18. (Please report any dependencies we seemed to have forgotten)
|
||||||
|
|
||||||
Check out the repo and run:
|
Check out the repo and run:
|
||||||
|
|
||||||
```
|
```
|
||||||
go run .
|
go build .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The binary should show up as `wallet-sync-server`.
|
||||||
|
|
||||||
# Account Creation Settings
|
# Account Creation Settings
|
||||||
|
|
||||||
When running the server, we should set some environmental variables. These environmental variables determine how account creation is handled. If we do not set these, no users will be able to create an account.
|
When running the server, we should set some environmental variables. These environmental variables determine how account creation is handled. If we do not set these, no users will be able to create an account.
|
||||||
|
@ -70,17 +82,10 @@ You'll get this in your Mailgun dashboard.
|
||||||
|
|
||||||
Whether your sending domain is in the EU. This is related to GDPR stuff I think. Valid values are `true` or `false`, defaulting to `false`.
|
Whether your sending domain is in the EU. This is related to GDPR stuff I think. Valid values are `true` or `false`, defaulting to `false`.
|
||||||
|
|
||||||
# You could make a script
|
# Deployment
|
||||||
|
|
||||||
For now you could store the stuff in a script:
|
A setup that works is [Caddy server](https://caddyserver.com) and Systemd.
|
||||||
|
|
||||||
```
|
Make sure Caddy is set to port 443, because the LBRY clients will expect that.
|
||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
export ACCOUNT_WHITELIST="my-email@example.com"
|
If you're using Mailgun, take care to keep the environmental vars secure. [See here](https://serverfault.com/questions/413397/how-to-set-environment-variable-in-systemd-service/910655#910655) for how to do this with systemd.
|
||||||
go run .
|
|
||||||
```
|
|
||||||
|
|
||||||
**NOTE**: If you're using Mailgun, set the file permissions on this script such that only the administrator can read it, since it will contain the Mailgun private API key
|
|
||||||
|
|
||||||
_Side note: Eventually we'll create systemd configurations, at which point we will be able to put the env vars in an `EnvironmentFile` instead of a script like this._
|
|
||||||
|
|
|
@ -27,7 +27,6 @@ const ScopeFull = AuthScope("*")
|
||||||
|
|
||||||
// For test stubs
|
// For test stubs
|
||||||
type AuthInterface interface {
|
type AuthInterface interface {
|
||||||
// TODO maybe have a "refresh token" thing if the client won't have email available all the time?
|
|
||||||
NewAuthToken(UserId, DeviceId, AuthScope) (*AuthToken, error)
|
NewAuthToken(UserId, DeviceId, AuthScope) (*AuthToken, error)
|
||||||
NewVerifyTokenString() (VerifyTokenString, error)
|
NewVerifyTokenString() (VerifyTokenString, error)
|
||||||
}
|
}
|
||||||
|
@ -46,7 +45,7 @@ const TokenLength = 32
|
||||||
|
|
||||||
func (a *Auth) NewAuthToken(userId UserId, deviceId DeviceId, scope AuthScope) (*AuthToken, error) {
|
func (a *Auth) NewAuthToken(userId UserId, deviceId DeviceId, scope AuthScope) (*AuthToken, error) {
|
||||||
b := make([]byte, TokenLength)
|
b := make([]byte, TokenLength)
|
||||||
// TODO - Is this is a secure random function? (Maybe audit)
|
// TODO - Audit: Is this is a secure random function?
|
||||||
if _, err := rand.Read(b); err != nil {
|
if _, err := rand.Read(b); err != nil {
|
||||||
return nil, fmt.Errorf("Error generating token: %+v", err)
|
return nil, fmt.Errorf("Error generating token: %+v", err)
|
||||||
}
|
}
|
||||||
|
@ -62,7 +61,7 @@ func (a *Auth) NewAuthToken(userId UserId, deviceId DeviceId, scope AuthScope) (
|
||||||
|
|
||||||
func (a *Auth) NewVerifyTokenString() (VerifyTokenString, error) {
|
func (a *Auth) NewVerifyTokenString() (VerifyTokenString, error) {
|
||||||
b := make([]byte, TokenLength)
|
b := make([]byte, TokenLength)
|
||||||
// TODO - Is this is a secure random function? (Maybe audit)
|
// TODO - Audit: Is this is a secure random function?
|
||||||
if _, err := rand.Read(b); err != nil {
|
if _, err := rand.Read(b); err != nil {
|
||||||
return "", fmt.Errorf("Error generating token: %+v", err)
|
return "", fmt.Errorf("Error generating token: %+v", err)
|
||||||
}
|
}
|
||||||
|
@ -152,6 +151,10 @@ func (c ClientSaltSeed) Validate() bool {
|
||||||
return len(c) == seedHexLength && err == nil
|
return len(c) == seedHexLength && err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p Password) Validate() bool {
|
||||||
|
return len(p) >= 8 // Should be much longer but it's a sanity check.
|
||||||
|
}
|
||||||
|
|
||||||
// TODO consider unicode. Also some providers might be case sensitive, and/or
|
// TODO consider unicode. Also some providers might be case sensitive, and/or
|
||||||
// may have other ways of having email addresses be equivalent (which we may
|
// may have other ways of having email addresses be equivalent (which we may
|
||||||
// not care about though)
|
// not care about though)
|
||||||
|
|
2
env/env.go
vendored
2
env/env.go
vendored
|
@ -5,7 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NOTE for users: If you have weird characters in your email address, please
|
// NOTE for users: If you have weird characters in your email address, please
|
||||||
|
|
2
env/env_test.go
vendored
2
env/env_test.go
vendored
|
@ -5,7 +5,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccountVerificationMode(t *testing.T) {
|
func TestAccountVerificationMode(t *testing.T) {
|
||||||
|
|
5
go.mod
5
go.mod
|
@ -1,8 +1,9 @@
|
||||||
module lbryio/lbry-id
|
module lbryio/wallet-sync-server
|
||||||
|
|
||||||
go 1.17
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/mailgun/mailgun-go/v4 v4.8.1
|
github.com/mailgun/mailgun-go/v4 v4.8.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.9
|
github.com/mattn/go-sqlite3 v1.14.9
|
||||||
github.com/prometheus/client_golang v1.11.0
|
github.com/prometheus/client_golang v1.11.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -47,6 +47,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
These are some simple instructions to get your lbry wallet sync server running. This is mostly a note to self and suggestion to others, but you can host however you want. We use supervisord and the Caddy web server. Ideally we'll probably switch to systemd and we'd include a Caddyfile.
|
|
||||||
|
|
||||||
Install the latest version of golang. Check out this repository, and run `go build .` in the repository root.
|
|
||||||
|
|
||||||
Insteall the [caddy web server](https://caddyserver.com/). The website has its own debian repos. You can also install from source (golang). Which ever (if either) you feel comfortable with. No specific recommendations here.
|
|
||||||
|
|
||||||
You'll need to adjust your firewall to allow http (port 80) for caddy to obtain an SSL cert via ACME (ZeroSSL or LetsEncrypt) and also allow https (port 443) to have caddy serve our wallet sync server. If you use `ufw`:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo ufw allow http
|
|
||||||
sudo ufw allow https
|
|
||||||
```
|
|
||||||
|
|
||||||
To avoid running Caddy as root, you'll need to allow it to serve from ports 80 and 443 ([see here](https://superuser.com/a/892391)):
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo setcap 'cap_net_bind_service=+ep' /home/lbry/caddy/cmd/caddy/caddy
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally, included are a couple quick conf scripts for supervisord to help get lbry.id running. Fill in the `{blank}`s and put under `/etc/supervisor/conf.d/`:
|
|
||||||
|
|
||||||
* `{caddy-cmd}`: The command to run caddy. If you're building from source, it will be `{caddy git repo root}/cmd/caddy/caddy`. If you're installing from a debian repo it's probably just `caddy`, but refer to the Caddy docs.
|
|
||||||
* `{lbry-user}`: The user you're using to run Caddy and the sync server.
|
|
||||||
* `{lbry-user-home}`: The home directory of `{lbry-user}`.
|
|
||||||
* `{sync-server-dir}`: The git repo root of the lbry sync server.
|
|
|
@ -1,8 +0,0 @@
|
||||||
[program:caddy]
|
|
||||||
command={caddy-cmd} reverse-proxy --from dev.lbry.id:443 --to localhost:8090
|
|
||||||
user={lbry-user}
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
stderr_logfile=/var/log/caddy.err.log
|
|
||||||
stdout_logfile=/var/log/caddy.out.log
|
|
||||||
environment=HOME={lbry-user-home}
|
|
|
@ -1,9 +0,0 @@
|
||||||
[program:lbry-id]
|
|
||||||
directory={sync-server-dir}
|
|
||||||
command={sync-server-dir}/lbry-id
|
|
||||||
user={lbry-user}
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
stderr_logfile=/var/log/lbry-id.err.log
|
|
||||||
stdout_logfile=/var/log/lbry-id.out.log
|
|
||||||
environment=ACCOUNT_VERIFICATION_MODE=AllowAll # ONLY FOR DEV ENVIRONMENTS
|
|
|
@ -8,9 +8,9 @@ import (
|
||||||
|
|
||||||
"github.com/mailgun/mailgun-go/v4"
|
"github.com/mailgun/mailgun-go/v4"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/env"
|
"lbryio/wallet-sync-server/env"
|
||||||
"lbryio/lbry-id/server/paths"
|
"lbryio/wallet-sync-server/server/paths"
|
||||||
)
|
)
|
||||||
|
|
||||||
const MAILGUN_DEBUG = false
|
const MAILGUN_DEBUG = false
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestEnv struct {
|
type TestEnv struct {
|
||||||
|
|
10
main.go
10
main.go
|
@ -3,11 +3,11 @@ package main
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/env"
|
"lbryio/wallet-sync-server/env"
|
||||||
"lbryio/lbry-id/mail"
|
"lbryio/wallet-sync-server/mail"
|
||||||
"lbryio/lbry-id/server"
|
"lbryio/wallet-sync-server/server"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func storeInit() (s store.Store) {
|
func storeInit() (s store.Store) {
|
||||||
|
|
|
@ -13,10 +13,18 @@ var (
|
||||||
// Prometheus?
|
// Prometheus?
|
||||||
Help: "Total number of requests to various endpoints",
|
Help: "Total number of requests to various endpoints",
|
||||||
},
|
},
|
||||||
[]string{"method"},
|
[]string{"method", "endpoint"},
|
||||||
|
)
|
||||||
|
ErrorsCount = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "wallet_sync_error_count",
|
||||||
|
Help: "Total number of various kinds of errors",
|
||||||
|
},
|
||||||
|
[]string{"error_type"},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
prometheus.MustRegister(RequestsCount)
|
prometheus.MustRegister(RequestsCount)
|
||||||
|
prometheus.MustRegister(ErrorsCount)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/env"
|
"lbryio/wallet-sync-server/env"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RegisterRequest struct {
|
type RegisterRequest struct {
|
||||||
|
@ -25,8 +25,8 @@ func (r *RegisterRequest) validate() error {
|
||||||
if !r.Email.Validate() {
|
if !r.Email.Validate() {
|
||||||
return fmt.Errorf("Invalid or missing 'email'")
|
return fmt.Errorf("Invalid or missing 'email'")
|
||||||
}
|
}
|
||||||
if r.Password == "" {
|
if !r.Password.Validate() {
|
||||||
return fmt.Errorf("Missing 'password'")
|
return fmt.Errorf("Invalid or missing 'password'")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !r.ClientSaltSeed.Validate() {
|
if !r.ClientSaltSeed.Validate() {
|
||||||
|
@ -122,6 +122,7 @@ modes:
|
||||||
// TODO StatusCreated also for first wallet and/or for get auth token?
|
// TODO StatusCreated also for first wallet and/or for get auth token?
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
fmt.Fprintf(w, string(response))
|
fmt.Fprintf(w, string(response))
|
||||||
|
log.Printf("User %s has registered", registerRequest.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - There's probably a struct-based solution here like with POST/PUT.
|
// TODO - There's probably a struct-based solution here like with POST/PUT.
|
||||||
|
@ -228,4 +229,8 @@ func (s *Server) verify(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(w, "Your account has been verified.")
|
fmt.Fprintf(w, "Your account has been verified.")
|
||||||
|
|
||||||
|
// if we really want to log the user's email at some point
|
||||||
|
// we can put in the effort then to fetch it
|
||||||
|
log.Printf("User has been verified with token %s", token)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"lbryio/lbry-id/server/paths"
|
"lbryio/wallet-sync-server/server/paths"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO - maybe this test could just be one of the TestServerRegisterAccountVerification tests now
|
// TODO - maybe this test could just be one of the TestServerRegisterAccountVerification tests now
|
||||||
|
@ -22,9 +22,9 @@ func TestServerRegisterSuccess(t *testing.T) {
|
||||||
}
|
}
|
||||||
testMail := TestMail{}
|
testMail := TestMail{}
|
||||||
testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234"}
|
testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234"}
|
||||||
s := Server{&testAuth, testStore, &TestEnv{env}, &testMail, TestPort}
|
s := Init(&testAuth, testStore, &TestEnv{env}, &testMail, TestPort)
|
||||||
|
|
||||||
requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
|
requestBody := []byte(`{"email": "abc@example.com", "password": "12345678", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, paths.PathRegister, bytes.NewBuffer(requestBody))
|
req := httptest.NewRequest(http.MethodPost, paths.PathRegister, bytes.NewBuffer(requestBody))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
@ -129,10 +129,10 @@ func TestServerRegisterErrors(t *testing.T) {
|
||||||
testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234", FailGenToken: tc.failGenToken}
|
testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234", FailGenToken: tc.failGenToken}
|
||||||
testMail := TestMail{SendVerificationEmailError: tc.mailError}
|
testMail := TestMail{SendVerificationEmailError: tc.mailError}
|
||||||
testStore := TestStore{Errors: tc.storeErrors}
|
testStore := TestStore{Errors: tc.storeErrors}
|
||||||
s := Server{&testAuth, &testStore, &TestEnv{env}, &testMail, TestPort}
|
s := Init(&testAuth, &testStore, &TestEnv{env}, &testMail, TestPort)
|
||||||
|
|
||||||
// Make request
|
// Make request
|
||||||
requestBody := fmt.Sprintf(`{"email": "%s", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}`, tc.email)
|
requestBody := fmt.Sprintf(`{"email": "%s", "password": "12345678", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}`, tc.email)
|
||||||
req := httptest.NewRequest(http.MethodPost, paths.PathAuthToken, bytes.NewBuffer([]byte(requestBody)))
|
req := httptest.NewRequest(http.MethodPost, paths.PathAuthToken, bytes.NewBuffer([]byte(requestBody)))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
@ -227,9 +227,9 @@ func TestServerRegisterAccountVerification(t *testing.T) {
|
||||||
testStore := &TestStore{}
|
testStore := &TestStore{}
|
||||||
testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234"}
|
testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234"}
|
||||||
testMail := TestMail{}
|
testMail := TestMail{}
|
||||||
s := Server{&testAuth, testStore, &TestEnv{tc.env}, &testMail, TestPort}
|
s := Init(&testAuth, testStore, &TestEnv{tc.env}, &testMail, TestPort)
|
||||||
|
|
||||||
requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
|
requestBody := []byte(`{"email": "abc@example.com", "password": "12345678", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, paths.PathRegister, bytes.NewBuffer(requestBody))
|
req := httptest.NewRequest(http.MethodPost, paths.PathRegister, bytes.NewBuffer(requestBody))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
@ -274,12 +274,12 @@ func TestServerRegisterAccountVerification(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerValidateRegisterRequest(t *testing.T) {
|
func TestServerValidateRegisterRequest(t *testing.T) {
|
||||||
registerRequest := RegisterRequest{Email: "joe@example.com", Password: "aoeu", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}
|
registerRequest := RegisterRequest{Email: "joe@example.com", Password: "12345678", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}
|
||||||
if registerRequest.validate() != nil {
|
if registerRequest.validate() != nil {
|
||||||
t.Errorf("Expected valid RegisterRequest to successfully validate")
|
t.Errorf("Expected valid RegisterRequest to successfully validate")
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRequest = RegisterRequest{Email: "joe-example.com", Password: "aoeu", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}
|
registerRequest = RegisterRequest{Email: "joe-example.com", Password: "12345678", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}
|
||||||
err := registerRequest.validate()
|
err := registerRequest.validate()
|
||||||
if !strings.Contains(err.Error(), "email") {
|
if !strings.Contains(err.Error(), "email") {
|
||||||
t.Errorf("Expected RegisterRequest with invalid email to return an appropriate error")
|
t.Errorf("Expected RegisterRequest with invalid email to return an appropriate error")
|
||||||
|
@ -288,13 +288,13 @@ func TestServerValidateRegisterRequest(t *testing.T) {
|
||||||
// Note that Golang's email address parser, which I use, will accept
|
// Note that Golang's email address parser, which I use, will accept
|
||||||
// "Joe <joe@example.com>" so we need to make sure to avoid accepting it. See
|
// "Joe <joe@example.com>" so we need to make sure to avoid accepting it. See
|
||||||
// the implementation.
|
// the implementation.
|
||||||
registerRequest = RegisterRequest{Email: "Joe <joe@example.com>", Password: "aoeu", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}
|
registerRequest = RegisterRequest{Email: "Joe <joe@example.com>", Password: "12345678", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}
|
||||||
err = registerRequest.validate()
|
err = registerRequest.validate()
|
||||||
if !strings.Contains(err.Error(), "email") {
|
if !strings.Contains(err.Error(), "email") {
|
||||||
t.Errorf("Expected RegisterRequest with email with unexpected formatting to return an appropriate error")
|
t.Errorf("Expected RegisterRequest with email with unexpected formatting to return an appropriate error")
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRequest = RegisterRequest{Password: "aoeu", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}
|
registerRequest = RegisterRequest{Password: "12345678", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}
|
||||||
err = registerRequest.validate()
|
err = registerRequest.validate()
|
||||||
if !strings.Contains(err.Error(), "email") {
|
if !strings.Contains(err.Error(), "email") {
|
||||||
t.Errorf("Expected RegisterRequest with missing email to return an appropriate error")
|
t.Errorf("Expected RegisterRequest with missing email to return an appropriate error")
|
||||||
|
@ -306,19 +306,19 @@ func TestServerValidateRegisterRequest(t *testing.T) {
|
||||||
t.Errorf("Expected RegisterRequest with missing password to return an appropriate error")
|
t.Errorf("Expected RegisterRequest with missing password to return an appropriate error")
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRequest = RegisterRequest{Email: "joe@example.com", Password: "aoeu"}
|
registerRequest = RegisterRequest{Email: "joe@example.com", Password: "12345678"}
|
||||||
err = registerRequest.validate()
|
err = registerRequest.validate()
|
||||||
if !strings.Contains(err.Error(), "clientSaltSeed") {
|
if !strings.Contains(err.Error(), "clientSaltSeed") {
|
||||||
t.Errorf("Expected RegisterRequest with missing clientSaltSeed to return an appropriate error")
|
t.Errorf("Expected RegisterRequest with missing clientSaltSeed to return an appropriate error")
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRequest = RegisterRequest{Email: "joe@example.com", Password: "aoeu", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234"}
|
registerRequest = RegisterRequest{Email: "joe@example.com", Password: "12345678", ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234"}
|
||||||
err = registerRequest.validate()
|
err = registerRequest.validate()
|
||||||
if !strings.Contains(err.Error(), "clientSaltSeed") {
|
if !strings.Contains(err.Error(), "clientSaltSeed") {
|
||||||
t.Errorf("Expected RegisterRequest with clientSaltSeed of wrong length to return an appropriate error")
|
t.Errorf("Expected RegisterRequest with clientSaltSeed of wrong length to return an appropriate error")
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRequest = RegisterRequest{Email: "joe@example.com", Password: "aoeu", ClientSaltSeed: "xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234"}
|
registerRequest = RegisterRequest{Email: "joe@example.com", Password: "12345678", ClientSaltSeed: "xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234"}
|
||||||
err = registerRequest.validate()
|
err = registerRequest.validate()
|
||||||
if !strings.Contains(err.Error(), "clientSaltSeed") {
|
if !strings.Contains(err.Error(), "clientSaltSeed") {
|
||||||
t.Errorf("Expected RegisterRequest with clientSaltSeed with a non-hex string to return an appropriate error")
|
t.Errorf("Expected RegisterRequest with clientSaltSeed with a non-hex string to return an appropriate error")
|
||||||
|
@ -332,7 +332,7 @@ func TestServerResendVerifyEmailSuccess(t *testing.T) {
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
"ACCOUNT_VERIFICATION_MODE": "EmailVerify",
|
"ACCOUNT_VERIFICATION_MODE": "EmailVerify",
|
||||||
}
|
}
|
||||||
s := Server{&TestAuth{}, &testStore, &TestEnv{env}, &testMail, TestPort}
|
s := Init(&TestAuth{}, &testStore, &TestEnv{env}, &testMail, TestPort)
|
||||||
|
|
||||||
requestBody := []byte(`{"email": "abc@example.com"}`)
|
requestBody := []byte(`{"email": "abc@example.com"}`)
|
||||||
req := httptest.NewRequest(http.MethodPost, paths.PathVerify, bytes.NewBuffer(requestBody))
|
req := httptest.NewRequest(http.MethodPost, paths.PathVerify, bytes.NewBuffer(requestBody))
|
||||||
|
@ -429,7 +429,7 @@ func TestServerResendVerifyEmailErrors(t *testing.T) {
|
||||||
// Set this up to fail according to specification
|
// Set this up to fail according to specification
|
||||||
testStore := TestStore{Errors: tc.storeErrors}
|
testStore := TestStore{Errors: tc.storeErrors}
|
||||||
testMail := TestMail{SendVerificationEmailError: tc.mailError}
|
testMail := TestMail{SendVerificationEmailError: tc.mailError}
|
||||||
s := Server{&TestAuth{}, &testStore, &TestEnv{env}, &testMail, TestPort}
|
s := Init(&TestAuth{}, &testStore, &TestEnv{env}, &testMail, TestPort)
|
||||||
|
|
||||||
// Make request
|
// Make request
|
||||||
var requestBody []byte
|
var requestBody []byte
|
||||||
|
@ -468,7 +468,7 @@ func TestServerResendVerifyEmailErrors(t *testing.T) {
|
||||||
|
|
||||||
func TestServerVerifyAccountSuccess(t *testing.T) {
|
func TestServerVerifyAccountSuccess(t *testing.T) {
|
||||||
testStore := TestStore{}
|
testStore := TestStore{}
|
||||||
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort}
|
s := Init(&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, paths.PathVerify, nil)
|
req := httptest.NewRequest(http.MethodGet, paths.PathVerify, nil)
|
||||||
q := req.URL.Query()
|
q := req.URL.Query()
|
||||||
|
@ -529,7 +529,7 @@ func TestServerVerifyAccountErrors(t *testing.T) {
|
||||||
|
|
||||||
// Set this up to fail according to specification
|
// Set this up to fail according to specification
|
||||||
testStore := TestStore{Errors: tc.storeErrors}
|
testStore := TestStore{Errors: tc.storeErrors}
|
||||||
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort}
|
s := Init(&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort)
|
||||||
|
|
||||||
// Make request
|
// Make request
|
||||||
req := httptest.NewRequest(http.MethodGet, paths.PathVerify, nil)
|
req := httptest.NewRequest(http.MethodGet, paths.PathVerify, nil)
|
||||||
|
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeviceId is decided by the device. UserId is decided by the server, and is
|
// DeviceId is decided by the device. UserId is decided by the server, and is
|
||||||
|
@ -21,8 +21,8 @@ func (r *AuthRequest) validate() error {
|
||||||
if !r.Email.Validate() {
|
if !r.Email.Validate() {
|
||||||
return fmt.Errorf("Invalid 'email'")
|
return fmt.Errorf("Invalid 'email'")
|
||||||
}
|
}
|
||||||
if r.Password == "" {
|
if !r.Password.Validate() {
|
||||||
return fmt.Errorf("Missing 'password'")
|
return fmt.Errorf("Invalid or missing 'password'")
|
||||||
}
|
}
|
||||||
if r.DeviceId == "" {
|
if r.DeviceId == "" {
|
||||||
return fmt.Errorf("Missing 'deviceId'")
|
return fmt.Errorf("Missing 'deviceId'")
|
||||||
|
|
|
@ -10,17 +10,17 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/server/paths"
|
"lbryio/wallet-sync-server/server/paths"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServerAuthHandlerSuccess(t *testing.T) {
|
func TestServerAuthHandlerSuccess(t *testing.T) {
|
||||||
testAuth := TestAuth{TestNewAuthTokenString: auth.AuthTokenString("seekrit")}
|
testAuth := TestAuth{TestNewAuthTokenString: auth.AuthTokenString("seekrit")}
|
||||||
testStore := TestStore{}
|
testStore := TestStore{}
|
||||||
s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort}
|
s := Init(&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort)
|
||||||
|
|
||||||
requestBody := []byte(`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`)
|
requestBody := []byte(`{"deviceId": "dev-1", "email": "abc@example.com", "password": "12345678"}`)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, paths.PathAuthToken, bytes.NewBuffer(requestBody))
|
req := httptest.NewRequest(http.MethodPost, paths.PathAuthToken, bytes.NewBuffer(requestBody))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
@ -104,11 +104,11 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
||||||
if tc.authFailGenToken { // TODO - TestAuth{Errors:authErrors}
|
if tc.authFailGenToken { // TODO - TestAuth{Errors:authErrors}
|
||||||
testAuth.FailGenToken = true
|
testAuth.FailGenToken = true
|
||||||
}
|
}
|
||||||
server := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort}
|
server := Init(&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort)
|
||||||
|
|
||||||
// Make request
|
// Make request
|
||||||
// So long as the JSON is well-formed, the content doesn't matter here since the password check will be stubbed out
|
// So long as the JSON is well-formed, the content doesn't matter here since the password check will be stubbed out
|
||||||
requestBody := fmt.Sprintf(`{"deviceId": "dev-1", "email": "%s", "password": "123"}`, tc.email)
|
requestBody := fmt.Sprintf(`{"deviceId": "dev-1", "email": "%s", "password": "12345678"}`, tc.email)
|
||||||
req := httptest.NewRequest(http.MethodPost, paths.PathAuthToken, bytes.NewBuffer([]byte(requestBody)))
|
req := httptest.NewRequest(http.MethodPost, paths.PathAuthToken, bytes.NewBuffer([]byte(requestBody)))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerValidateAuthRequest(t *testing.T) {
|
func TestServerValidateAuthRequest(t *testing.T) {
|
||||||
authRequest := AuthRequest{DeviceId: "dId", Email: "joe@example.com", Password: "aoeu"}
|
authRequest := AuthRequest{DeviceId: "dId", Email: "joe@example.com", Password: "12345678"}
|
||||||
if authRequest.validate() != nil {
|
if authRequest.validate() != nil {
|
||||||
t.Errorf("Expected valid AuthRequest to successfully validate")
|
t.Errorf("Expected valid AuthRequest to successfully validate")
|
||||||
}
|
}
|
||||||
|
@ -134,22 +134,22 @@ func TestServerValidateAuthRequest(t *testing.T) {
|
||||||
failureDescription string
|
failureDescription string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
AuthRequest{Email: "joe@example.com", Password: "aoeu"},
|
AuthRequest{Email: "joe@example.com", Password: "12345678"},
|
||||||
"deviceId",
|
"deviceId",
|
||||||
"Expected AuthRequest with missing device to not successfully validate",
|
"Expected AuthRequest with missing device to not successfully validate",
|
||||||
}, {
|
}, {
|
||||||
AuthRequest{DeviceId: "dId", Email: "joe-example.com", Password: "aoeu"},
|
AuthRequest{DeviceId: "dId", Email: "joe-example.com", Password: "12345678"},
|
||||||
"email",
|
"email",
|
||||||
"Expected AuthRequest with invalid email to not successfully validate",
|
"Expected AuthRequest with invalid email to not successfully validate",
|
||||||
}, {
|
}, {
|
||||||
// Note that Golang's email address parser, which I use, will accept
|
// Note that Golang's email address parser, which I use, will accept
|
||||||
// "Joe <joe@example.com>" so we need to make sure to avoid accepting it. See
|
// "Joe <joe@example.com>" so we need to make sure to avoid accepting it. See
|
||||||
// the implementation.
|
// the implementation.
|
||||||
AuthRequest{DeviceId: "dId", Email: "Joe <joe@example.com>", Password: "aoeu"},
|
AuthRequest{DeviceId: "dId", Email: "Joe <joe@example.com>", Password: "12345678"},
|
||||||
"email",
|
"email",
|
||||||
"Expected AuthRequest with email with unexpected formatting to not successfully validate",
|
"Expected AuthRequest with email with unexpected formatting to not successfully validate",
|
||||||
}, {
|
}, {
|
||||||
AuthRequest{DeviceId: "dId", Password: "aoeu"},
|
AuthRequest{DeviceId: "dId", Password: "12345678"},
|
||||||
"email",
|
"email",
|
||||||
"Expected AuthRequest with missing email to not successfully validate",
|
"Expected AuthRequest with missing email to not successfully validate",
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Thanks to Standard Notes. See:
|
// Thanks to Standard Notes. See:
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/server/paths"
|
"lbryio/wallet-sync-server/server/paths"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServerGetClientSalt(t *testing.T) {
|
func TestServerGetClientSalt(t *testing.T) {
|
||||||
|
@ -67,7 +67,7 @@ func TestServerGetClientSalt(t *testing.T) {
|
||||||
Errors: tc.storeErrors,
|
Errors: tc.storeErrors,
|
||||||
}
|
}
|
||||||
|
|
||||||
s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort}
|
s := Init(&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, paths.PathClientSaltSeed, nil)
|
req := httptest.NewRequest(http.MethodGet, paths.PathClientSaltSeed, nil)
|
||||||
q := req.URL.Query()
|
q := req.URL.Query()
|
||||||
|
|
|
@ -12,17 +12,16 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/server/paths"
|
"lbryio/wallet-sync-server/server/paths"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
"lbryio/lbry-id/wallet"
|
"lbryio/wallet-sync-server/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Whereas sever_test.go stubs out auth store and wallet, these will use the real thing, but test fewer paths.
|
// Whereas sever_test.go stubs out auth store and wallet, these will use the real thing, but test fewer paths.
|
||||||
|
|
||||||
// TODO - test some unhappy paths? Don't want to retest all the unit tests though.
|
|
||||||
|
|
||||||
// Integration test requires a real sqlite database
|
// Integration test requires a real sqlite database
|
||||||
func storeTestInit(t *testing.T) (s store.Store, tmpFile *os.File) {
|
func storeTestInit(t *testing.T) (s store.Store, tmpFile *os.File) {
|
||||||
s = store.Store{}
|
s = store.Store{}
|
||||||
|
@ -101,7 +100,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
"ACCOUNT_WHITELIST": "abc@example.com",
|
"ACCOUNT_WHITELIST": "abc@example.com",
|
||||||
}
|
}
|
||||||
s := Server{&auth.Auth{}, &st, &TestEnv{env}, &TestMail{}, TestPort}
|
s := Init(&auth.Auth{}, &st, &TestEnv{env}, &TestMail{}, TestPort)
|
||||||
|
|
||||||
////////////////////
|
////////////////////
|
||||||
t.Log("Request: Register email address - any device")
|
t.Log("Request: Register email address - any device")
|
||||||
|
@ -114,7 +113,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
||||||
s.register,
|
s.register,
|
||||||
paths.PathRegister,
|
paths.PathRegister,
|
||||||
®isterResponse,
|
®isterResponse,
|
||||||
`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"}`,
|
`{"email": "abc@example.com", "password": "12345678", "clientSaltSeed": "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody, http.StatusCreated)
|
checkStatusCode(t, statusCode, responseBody, http.StatusCreated)
|
||||||
|
@ -130,7 +129,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
||||||
s.getAuthToken,
|
s.getAuthToken,
|
||||||
paths.PathAuthToken,
|
paths.PathAuthToken,
|
||||||
&authToken1,
|
&authToken1,
|
||||||
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "12345678"}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
@ -158,7 +157,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
||||||
s.getAuthToken,
|
s.getAuthToken,
|
||||||
paths.PathAuthToken,
|
paths.PathAuthToken,
|
||||||
&authToken2,
|
&authToken2,
|
||||||
`{"deviceId": "dev-2", "email": "abc@example.com", "password": "123"}`,
|
`{"deviceId": "dev-2", "email": "abc@example.com", "password": "12345678"}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
@ -271,7 +270,13 @@ func TestIntegrationChangePassword(t *testing.T) {
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
"ACCOUNT_WHITELIST": "abc@example.com",
|
"ACCOUNT_WHITELIST": "abc@example.com",
|
||||||
}
|
}
|
||||||
s := Server{&auth.Auth{}, &st, &TestEnv{env}, &TestMail{}, TestPort}
|
s := Init(&auth.Auth{}, &st, &TestEnv{env}, &TestMail{}, TestPort)
|
||||||
|
|
||||||
|
// Still need to mock this until we're doing a real integration test
|
||||||
|
// where we call Serve(), which brings up the real websocket manager.
|
||||||
|
// Note that in the integration test, we're only using this for requests
|
||||||
|
// that would get blocked without it.
|
||||||
|
wsmm := wsMockManager{s: s, done: make(chan bool)}
|
||||||
|
|
||||||
////////////////////
|
////////////////////
|
||||||
t.Log("Request: Register email address")
|
t.Log("Request: Register email address")
|
||||||
|
@ -284,7 +289,7 @@ func TestIntegrationChangePassword(t *testing.T) {
|
||||||
s.register,
|
s.register,
|
||||||
paths.PathRegister,
|
paths.PathRegister,
|
||||||
®isterResponse,
|
®isterResponse,
|
||||||
`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"}`,
|
`{"email": "abc@example.com", "password": "12345678", "clientSaltSeed": "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody, http.StatusCreated)
|
checkStatusCode(t, statusCode, responseBody, http.StatusCreated)
|
||||||
|
@ -322,7 +327,7 @@ func TestIntegrationChangePassword(t *testing.T) {
|
||||||
s.getAuthToken,
|
s.getAuthToken,
|
||||||
paths.PathAuthToken,
|
paths.PathAuthToken,
|
||||||
&authToken,
|
&authToken,
|
||||||
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "12345678"}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
@ -343,6 +348,9 @@ func TestIntegrationChangePassword(t *testing.T) {
|
||||||
t.Log("Request: Change password")
|
t.Log("Request: Change password")
|
||||||
////////////////////
|
////////////////////
|
||||||
|
|
||||||
|
// Giving it a whole second of timeout because this request seems to be a bit
|
||||||
|
// slow.
|
||||||
|
go wsmm.getOneMessage(time.Second)
|
||||||
var changePasswordResponse struct{}
|
var changePasswordResponse struct{}
|
||||||
responseBody, statusCode = request(
|
responseBody, statusCode = request(
|
||||||
t,
|
t,
|
||||||
|
@ -350,8 +358,9 @@ func TestIntegrationChangePassword(t *testing.T) {
|
||||||
s.changePassword,
|
s.changePassword,
|
||||||
paths.PathPassword,
|
paths.PathPassword,
|
||||||
&changePasswordResponse,
|
&changePasswordResponse,
|
||||||
`{"email": "abc@example.com", "oldPassword": "123", "newPassword": "456", "clientSaltSeed": "8678def95678def98678def95678def98678def95678def98678def95678def9"}`,
|
`{"email": "abc@example.com", "oldPassword": "12345678", "newPassword": "45678901", "clientSaltSeed": "8678def95678def98678def95678def98678def95678def98678def95678def9"}`,
|
||||||
)
|
)
|
||||||
|
<-wsmm.done
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
|
||||||
|
@ -405,7 +414,7 @@ func TestIntegrationChangePassword(t *testing.T) {
|
||||||
s.getAuthToken,
|
s.getAuthToken,
|
||||||
paths.PathAuthToken,
|
paths.PathAuthToken,
|
||||||
&authToken,
|
&authToken,
|
||||||
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "456"}`,
|
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "45678901"}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
@ -446,6 +455,9 @@ func TestIntegrationChangePassword(t *testing.T) {
|
||||||
t.Log("Request: Change password again, this time including a wallet (since there is a wallet to update)")
|
t.Log("Request: Change password again, this time including a wallet (since there is a wallet to update)")
|
||||||
////////////////////
|
////////////////////
|
||||||
|
|
||||||
|
// Giving it a whole second of timeout because this request seems to be a bit
|
||||||
|
// slow.
|
||||||
|
go wsmm.getOneMessage(time.Second)
|
||||||
responseBody, statusCode = request(
|
responseBody, statusCode = request(
|
||||||
t,
|
t,
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
|
@ -457,11 +469,12 @@ func TestIntegrationChangePassword(t *testing.T) {
|
||||||
"sequence": 2,
|
"sequence": 2,
|
||||||
"hmac": "my-hmac-2",
|
"hmac": "my-hmac-2",
|
||||||
"email": "abc@example.com",
|
"email": "abc@example.com",
|
||||||
"oldPassword": "456",
|
"oldPassword": "45678901",
|
||||||
"newPassword": "789",
|
"newPassword": "78901234",
|
||||||
"clientSaltSeed": "0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff"
|
"clientSaltSeed": "0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff"
|
||||||
}`),
|
}`),
|
||||||
)
|
)
|
||||||
|
<-wsmm.done
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
|
||||||
|
@ -510,7 +523,7 @@ func TestIntegrationChangePassword(t *testing.T) {
|
||||||
s.getAuthToken,
|
s.getAuthToken,
|
||||||
paths.PathAuthToken,
|
paths.PathAuthToken,
|
||||||
&authToken,
|
&authToken,
|
||||||
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "789"}`,
|
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "78901234"}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
@ -562,7 +575,7 @@ func TestIntegrationVerifyAccount(t *testing.T) {
|
||||||
"ACCOUNT_VERIFICATION_MODE": "EmailVerify",
|
"ACCOUNT_VERIFICATION_MODE": "EmailVerify",
|
||||||
}
|
}
|
||||||
testMail := TestMail{}
|
testMail := TestMail{}
|
||||||
s := Server{&auth.Auth{}, &st, &TestEnv{env}, &testMail, TestPort}
|
s := Init(&auth.Auth{}, &st, &TestEnv{env}, &testMail, TestPort)
|
||||||
|
|
||||||
////////////////////
|
////////////////////
|
||||||
t.Log("Request: Register email address")
|
t.Log("Request: Register email address")
|
||||||
|
@ -575,7 +588,7 @@ func TestIntegrationVerifyAccount(t *testing.T) {
|
||||||
s.register,
|
s.register,
|
||||||
paths.PathRegister,
|
paths.PathRegister,
|
||||||
®isterResponse,
|
®isterResponse,
|
||||||
`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"}`,
|
`{"email": "abc@example.com", "password": "12345678", "clientSaltSeed": "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody, http.StatusCreated)
|
checkStatusCode(t, statusCode, responseBody, http.StatusCreated)
|
||||||
|
@ -619,7 +632,7 @@ func TestIntegrationVerifyAccount(t *testing.T) {
|
||||||
s.getAuthToken,
|
s.getAuthToken,
|
||||||
paths.PathAuthToken,
|
paths.PathAuthToken,
|
||||||
&authToken,
|
&authToken,
|
||||||
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "12345678"}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody, http.StatusUnauthorized)
|
checkStatusCode(t, statusCode, responseBody, http.StatusUnauthorized)
|
||||||
|
@ -652,7 +665,7 @@ func TestIntegrationVerifyAccount(t *testing.T) {
|
||||||
s.getAuthToken,
|
s.getAuthToken,
|
||||||
paths.PathAuthToken,
|
paths.PathAuthToken,
|
||||||
&authToken,
|
&authToken,
|
||||||
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "12345678"}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
|
|
@ -3,11 +3,16 @@ package server
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/metrics"
|
||||||
"lbryio/lbry-id/wallet"
|
"lbryio/wallet-sync-server/store"
|
||||||
|
"lbryio/wallet-sync-server/wallet"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChangePasswordRequest struct {
|
type ChangePasswordRequest struct {
|
||||||
|
@ -28,11 +33,11 @@ func (r *ChangePasswordRequest) validate() error {
|
||||||
if !r.Email.Validate() {
|
if !r.Email.Validate() {
|
||||||
return fmt.Errorf("Invalid or missing 'email'")
|
return fmt.Errorf("Invalid or missing 'email'")
|
||||||
}
|
}
|
||||||
if r.OldPassword == "" {
|
if !r.OldPassword.Validate() {
|
||||||
return fmt.Errorf("Missing 'oldPassword'")
|
return fmt.Errorf("Invalid or missing 'oldPassword'")
|
||||||
}
|
}
|
||||||
if r.NewPassword == "" {
|
if !r.NewPassword.Validate() {
|
||||||
return fmt.Errorf("Missing 'newPassword'")
|
return fmt.Errorf("Invalid or missing 'newPassword'")
|
||||||
}
|
}
|
||||||
// Too bad we can't do this so easily with clientSaltSeed
|
// Too bad we can't do this so easily with clientSaltSeed
|
||||||
if r.OldPassword == r.NewPassword {
|
if r.OldPassword == r.NewPassword {
|
||||||
|
@ -69,8 +74,9 @@ func (s *Server) changePassword(w http.ResponseWriter, req *http.Request) {
|
||||||
// unverified accounts here for simplicity.
|
// unverified accounts here for simplicity.
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
var userId auth.UserId
|
||||||
if changePasswordRequest.EncryptedWallet != "" {
|
if changePasswordRequest.EncryptedWallet != "" {
|
||||||
err = s.store.ChangePasswordWithWallet(
|
userId, err = s.store.ChangePasswordWithWallet(
|
||||||
changePasswordRequest.Email,
|
changePasswordRequest.Email,
|
||||||
changePasswordRequest.OldPassword,
|
changePasswordRequest.OldPassword,
|
||||||
changePasswordRequest.NewPassword,
|
changePasswordRequest.NewPassword,
|
||||||
|
@ -83,7 +89,7 @@ func (s *Server) changePassword(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = s.store.ChangePasswordNoWallet(
|
userId, err = s.store.ChangePasswordNoWallet(
|
||||||
changePasswordRequest.Email,
|
changePasswordRequest.Email,
|
||||||
changePasswordRequest.OldPassword,
|
changePasswordRequest.OldPassword,
|
||||||
changePasswordRequest.NewPassword,
|
changePasswordRequest.NewPassword,
|
||||||
|
@ -107,6 +113,42 @@ func (s *Server) changePassword(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO - A socket connection request using an old auth token could still
|
||||||
|
// succeed in a race condition:
|
||||||
|
// * websocket handler: checkAuth passes with token
|
||||||
|
// * password change handler: change password, invalidate token
|
||||||
|
// * password change handler: send userRemove message
|
||||||
|
// * websocket manager: process userRemove message, ending all websocket connections for user
|
||||||
|
// * websocket handler: new websocket connection is established
|
||||||
|
//
|
||||||
|
// It would require the websocket handler to be very slow, but I don't want to
|
||||||
|
// rule it out.
|
||||||
|
//
|
||||||
|
// But a much more likely scenario could happen: the buffer on the userRemove
|
||||||
|
// channel could get full and it could time out, and not boot any of the
|
||||||
|
// users' clients.
|
||||||
|
//
|
||||||
|
// These aren't horribly important now since the only message is a
|
||||||
|
// notification that a new wallet version exists, but who knows what we
|
||||||
|
// could use websockets for. Maybe we start doing something crazy like
|
||||||
|
// updating the wallet over the channel, in which case we absolutely want
|
||||||
|
// to prevent an old client from doing so after a password change on
|
||||||
|
// another client.
|
||||||
|
//
|
||||||
|
// We'd have to think a fair amount about how to make these foolproof if it
|
||||||
|
// becomes important. Maybe we just pass the auth token to the websocket
|
||||||
|
// writer, and pass it to every wallet update db call, and have it check
|
||||||
|
// the auth token within the same transaction as the wallet update.
|
||||||
|
|
||||||
|
timeout := time.NewTicker(100 * time.Millisecond)
|
||||||
|
select {
|
||||||
|
case s.userRemove <- wsClientForUser{userId, nil}:
|
||||||
|
case <-timeout.C:
|
||||||
|
metrics.ErrorsCount.With(prometheus.Labels{"error_type": "ws-user-remove"}).Inc()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timeout.Stop()
|
||||||
|
|
||||||
var changePasswordResponse struct{} // no data to respond with, but keep it JSON
|
var changePasswordResponse struct{} // no data to respond with, but keep it JSON
|
||||||
var response []byte
|
var response []byte
|
||||||
response, err = json.Marshal(changePasswordResponse)
|
response, err = json.Marshal(changePasswordResponse)
|
||||||
|
@ -118,4 +160,5 @@ func (s *Server) changePassword(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
fmt.Fprintf(w, string(response))
|
fmt.Fprintf(w, string(response))
|
||||||
|
log.Printf("User %s has changed their password", changePasswordRequest.Email)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,12 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/server/paths"
|
"lbryio/wallet-sync-server/server/paths"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
"lbryio/lbry-id/wallet"
|
"lbryio/wallet-sync-server/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServerChangePassword(t *testing.T) {
|
func TestServerChangePassword(t *testing.T) {
|
||||||
|
@ -25,6 +26,8 @@ func TestServerChangePassword(t *testing.T) {
|
||||||
// Whether we expect the call to ChangePassword*Wallet to happen
|
// Whether we expect the call to ChangePassword*Wallet to happen
|
||||||
expectChangePasswordCall bool
|
expectChangePasswordCall bool
|
||||||
|
|
||||||
|
expectWsMsg bool
|
||||||
|
|
||||||
// `new...` refers to what is being passed into the via POST request (and
|
// `new...` refers to what is being passed into the via POST request (and
|
||||||
// what we expect to get passed into SetWallet for the *non-error* cases
|
// what we expect to get passed into SetWallet for the *non-error* cases
|
||||||
// below)
|
// below)
|
||||||
|
@ -42,6 +45,7 @@ func TestServerChangePassword(t *testing.T) {
|
||||||
expectedStatusCode: http.StatusOK,
|
expectedStatusCode: http.StatusOK,
|
||||||
|
|
||||||
expectChangePasswordCall: true,
|
expectChangePasswordCall: true,
|
||||||
|
expectWsMsg: true,
|
||||||
|
|
||||||
newEncryptedWallet: "my-enc-wallet",
|
newEncryptedWallet: "my-enc-wallet",
|
||||||
newSequence: 2,
|
newSequence: 2,
|
||||||
|
@ -54,6 +58,7 @@ func TestServerChangePassword(t *testing.T) {
|
||||||
expectedStatusCode: http.StatusOK,
|
expectedStatusCode: http.StatusOK,
|
||||||
|
|
||||||
expectChangePasswordCall: true,
|
expectChangePasswordCall: true,
|
||||||
|
expectWsMsg: true,
|
||||||
|
|
||||||
email: "abc@example.com",
|
email: "abc@example.com",
|
||||||
}, {
|
}, {
|
||||||
|
@ -168,8 +173,9 @@ func TestServerChangePassword(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
testStore := TestStore{Errors: tc.storeErrors}
|
testStore := TestStore{Errors: tc.storeErrors, TestUserId: 37}
|
||||||
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort}
|
s := Init(&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort)
|
||||||
|
wsmm := wsMockManager{s: s, done: make(chan bool)}
|
||||||
|
|
||||||
// Whether we passed in wallet fields (these test cases should be passing
|
// Whether we passed in wallet fields (these test cases should be passing
|
||||||
// in all of them or none of them, so we only test EncryptedWallet). This
|
// in all of them or none of them, so we only test EncryptedWallet). This
|
||||||
|
@ -196,7 +202,15 @@ func TestServerChangePassword(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodPost, paths.PathPassword, bytes.NewBuffer(requestBody))
|
req := httptest.NewRequest(http.MethodPost, paths.PathPassword, bytes.NewBuffer(requestBody))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
go wsmm.getOneMessage(100 * time.Millisecond)
|
||||||
s.changePassword(w, req)
|
s.changePassword(w, req)
|
||||||
|
<-wsmm.done
|
||||||
|
if tc.expectWsMsg && wsmm.removedUserId != testStore.TestUserId {
|
||||||
|
t.Error("Expected websocket message to remove user id")
|
||||||
|
}
|
||||||
|
if !tc.expectWsMsg && !wsmm.noMessage {
|
||||||
|
t.Error("Expected no websocket message to remove user id")
|
||||||
|
}
|
||||||
|
|
||||||
body, _ := ioutil.ReadAll(w.Body)
|
body, _ := ioutil.ReadAll(w.Body)
|
||||||
|
|
||||||
|
@ -260,8 +274,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
}
|
}
|
||||||
if changePasswordRequest.validate() != nil {
|
if changePasswordRequest.validate() != nil {
|
||||||
|
@ -270,8 +284,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
|
|
||||||
changePasswordRequest = ChangePasswordRequest{
|
changePasswordRequest = ChangePasswordRequest{
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
}
|
}
|
||||||
if changePasswordRequest.validate() != nil {
|
if changePasswordRequest.validate() != nil {
|
||||||
|
@ -289,8 +303,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "abc-example.com",
|
Email: "abc-example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
},
|
},
|
||||||
"email",
|
"email",
|
||||||
|
@ -304,8 +318,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "Abc <abc@example.com>",
|
Email: "Abc <abc@example.com>",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
},
|
},
|
||||||
"email",
|
"email",
|
||||||
|
@ -315,8 +329,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
EncryptedWallet: "my-encrypted-wallet",
|
EncryptedWallet: "my-encrypted-wallet",
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
},
|
},
|
||||||
"email",
|
"email",
|
||||||
|
@ -327,7 +341,7 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
},
|
},
|
||||||
"oldPassword",
|
"oldPassword",
|
||||||
|
@ -338,7 +352,7 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
},
|
},
|
||||||
"newPassword",
|
"newPassword",
|
||||||
|
@ -349,8 +363,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
},
|
},
|
||||||
"clientSaltSeed",
|
"clientSaltSeed",
|
||||||
"Expected ChangePasswordRequest with missing clientSaltSeed to return an appropriate error",
|
"Expected ChangePasswordRequest with missing clientSaltSeed to return an appropriate error",
|
||||||
|
@ -360,8 +374,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234",
|
||||||
},
|
},
|
||||||
"clientSaltSeed",
|
"clientSaltSeed",
|
||||||
|
@ -372,8 +386,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234",
|
ClientSaltSeed: "xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234xxxx1234",
|
||||||
},
|
},
|
||||||
"clientSaltSeed",
|
"clientSaltSeed",
|
||||||
|
@ -383,8 +397,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
},
|
},
|
||||||
"'encryptedWallet', 'sequence', and 'hmac'", // More likely to fail when we change the error message but whatever
|
"'encryptedWallet', 'sequence', and 'hmac'", // More likely to fail when we change the error message but whatever
|
||||||
|
@ -394,8 +408,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
EncryptedWallet: "my-encrypted-wallet",
|
EncryptedWallet: "my-encrypted-wallet",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
},
|
},
|
||||||
"'encryptedWallet', 'sequence', and 'hmac'", // More likely to fail when we change the error message but whatever
|
"'encryptedWallet', 'sequence', and 'hmac'", // More likely to fail when we change the error message but whatever
|
||||||
|
@ -406,8 +420,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 0,
|
Sequence: 0,
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "456",
|
NewPassword: "45678901",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
},
|
},
|
||||||
"'encryptedWallet', 'sequence', and 'hmac'", // More likely to fail when we change the error message but whatever
|
"'encryptedWallet', 'sequence', and 'hmac'", // More likely to fail when we change the error message but whatever
|
||||||
|
@ -418,8 +432,8 @@ func TestServerValidateChangePasswordRequest(t *testing.T) {
|
||||||
Hmac: "my-hmac",
|
Hmac: "my-hmac",
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Email: "abc@example.com",
|
Email: "abc@example.com",
|
||||||
OldPassword: "123",
|
OldPassword: "12345678",
|
||||||
NewPassword: "123",
|
NewPassword: "12345678",
|
||||||
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
ClientSaltSeed: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
},
|
},
|
||||||
"should not be the same",
|
"should not be the same",
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package paths
|
package paths
|
||||||
|
|
||||||
// TODO proper doc comments!
|
|
||||||
|
|
||||||
const ApiVersion = "3"
|
const ApiVersion = "3"
|
||||||
const PathPrefix = "/api/" + ApiVersion
|
const PathPrefix = "/api/" + ApiVersion
|
||||||
|
|
||||||
|
@ -13,6 +11,10 @@ const PathVerify = PathPrefix + "/verify"
|
||||||
const PathResendVerify = PathPrefix + "/verify/resend"
|
const PathResendVerify = PathPrefix + "/verify/resend"
|
||||||
const PathClientSaltSeed = PathPrefix + "/client-salt-seed"
|
const PathClientSaltSeed = PathPrefix + "/client-salt-seed"
|
||||||
|
|
||||||
|
// Using such a generic name since, as I understand, we can do a bunch of
|
||||||
|
// different stuff over this one websocket.
|
||||||
|
const PathWebsocket = PathPrefix + "/websocket"
|
||||||
|
|
||||||
const PathUnknownEndpoint = PathPrefix + "/"
|
const PathUnknownEndpoint = PathPrefix + "/"
|
||||||
const PathWrongApiVersion = "/api/"
|
const PathWrongApiVersion = "/api/"
|
||||||
|
|
||||||
|
|
141
server/server.go
141
server/server.go
|
@ -1,38 +1,71 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/env"
|
"lbryio/wallet-sync-server/env"
|
||||||
"lbryio/lbry-id/mail"
|
"lbryio/wallet-sync-server/mail"
|
||||||
"lbryio/lbry-id/server/paths"
|
"lbryio/wallet-sync-server/server/paths"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
|
"lbryio/wallet-sync-server/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxBodySize = 100000
|
||||||
|
|
||||||
|
// Message sent from the wallet POST request handler to the websocket manager,
|
||||||
|
// indicating that a user's client should receive a (different) message that
|
||||||
|
// their wallet has an update on the server.
|
||||||
|
type walletUpdateMsg struct {
|
||||||
|
userId auth.UserId
|
||||||
|
sequence wallet.Sequence
|
||||||
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
auth auth.AuthInterface
|
auth auth.AuthInterface
|
||||||
store store.StoreInterface
|
store store.StoreInterface
|
||||||
env env.EnvInterface
|
env env.EnvInterface
|
||||||
mail mail.MailInterface
|
mail mail.MailInterface
|
||||||
port int
|
port int
|
||||||
|
|
||||||
|
clientAdd chan wsClientForUser
|
||||||
|
clientRemove chan wsClientForUser
|
||||||
|
userRemove chan wsClientForUser
|
||||||
|
walletUpdates chan walletUpdateMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO If I capitalize the `auth` `store` and `env` fields of Store{} I can
|
|
||||||
// create Store{} structs directly from main.go.
|
|
||||||
func Init(
|
func Init(
|
||||||
auth auth.AuthInterface,
|
authInterface auth.AuthInterface,
|
||||||
store store.StoreInterface,
|
storeInterface store.StoreInterface,
|
||||||
env env.EnvInterface,
|
envInterface env.EnvInterface,
|
||||||
mail mail.MailInterface,
|
mailInterface mail.MailInterface,
|
||||||
port int,
|
port int,
|
||||||
) *Server {
|
) *Server {
|
||||||
return &Server{auth, store, env, mail, port}
|
return &Server{
|
||||||
|
auth: authInterface,
|
||||||
|
store: storeInterface,
|
||||||
|
env: envInterface,
|
||||||
|
mail: mailInterface,
|
||||||
|
port: port,
|
||||||
|
|
||||||
|
// Anything that could get backed up by a lot of requests, let's just
|
||||||
|
// give it a buffer. Starting small until we start to see dashboard
|
||||||
|
// stats on this. I want a sense of how this grows with the number of
|
||||||
|
// users or whatnot.
|
||||||
|
clientAdd: make(chan wsClientForUser),
|
||||||
|
clientRemove: make(chan wsClientForUser),
|
||||||
|
userRemove: make(chan wsClientForUser, 5),
|
||||||
|
walletUpdates: make(chan walletUpdateMsg, 5),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
|
@ -75,7 +108,6 @@ func internalServiceErrorJson(w http.ResponseWriter, serverErr error, errContext
|
||||||
|
|
||||||
// Cut down on code repetition. No need to return errors since it can all be
|
// Cut down on code repetition. No need to return errors since it can all be
|
||||||
// handled here. Just return a bool to indicate success.
|
// handled here. Just return a bool to indicate success.
|
||||||
// TODO the names `getPostData` and `getGetData` don't fully describe what they do
|
|
||||||
|
|
||||||
func requestOverhead(w http.ResponseWriter, req *http.Request, method string) bool {
|
func requestOverhead(w http.ResponseWriter, req *http.Request, method string) bool {
|
||||||
if req.Method != method {
|
if req.Method != method {
|
||||||
|
@ -92,10 +124,6 @@ type PostRequest interface {
|
||||||
validate() error
|
validate() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO decoder.DisallowUnknownFields?
|
|
||||||
// TODO GET params too large (like StatusRequestEntityTooLarge)? Or is that
|
|
||||||
// somehow handled by the http library due to a size limit in the http spec?
|
|
||||||
|
|
||||||
// Confirm it's a Post request, various overhead, decode the json, validate the struct
|
// Confirm it's a Post request, various overhead, decode the json, validate the struct
|
||||||
func getPostData(w http.ResponseWriter, req *http.Request, reqStruct PostRequest) bool {
|
func getPostData(w http.ResponseWriter, req *http.Request, reqStruct PostRequest) bool {
|
||||||
if !requestOverhead(w, req, http.MethodPost) {
|
if !requestOverhead(w, req, http.MethodPost) {
|
||||||
|
@ -105,17 +133,26 @@ func getPostData(w http.ResponseWriter, req *http.Request, reqStruct PostRequest
|
||||||
// Make the limit 100k. Increase from there as needed. I'd rather block some
|
// Make the limit 100k. Increase from there as needed. I'd rather block some
|
||||||
// people's large wallets and increase the limit than OOM for everybody and
|
// people's large wallets and increase the limit than OOM for everybody and
|
||||||
// decrease the limit.
|
// decrease the limit.
|
||||||
req.Body = http.MaxBytesReader(w, req.Body, 100000)
|
req.Body = http.MaxBytesReader(w, req.Body, maxBodySize)
|
||||||
err := json.NewDecoder(req.Body).Decode(&reqStruct)
|
decoder := json.NewDecoder(req.Body)
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err := decoder.Decode(&reqStruct)
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
break
|
break
|
||||||
case err.Error() == "http: request body too large":
|
case err.Error() == "http: request body too large":
|
||||||
errorJson(w, http.StatusRequestEntityTooLarge, "")
|
errorJson(w, http.StatusRequestEntityTooLarge, "")
|
||||||
return false
|
return false
|
||||||
|
case strings.HasPrefix(err.Error(), "json: unknown field"):
|
||||||
|
// The error is coming straight out of the json decoder. I think the prefix
|
||||||
|
// we check for determines what it is pretty reliably. I'd think it's safe
|
||||||
|
// to give back to the requesting client (unlike an arbitrary error
|
||||||
|
// message).
|
||||||
|
errorJson(w, http.StatusBadRequest, err.Error())
|
||||||
|
return false
|
||||||
default:
|
default:
|
||||||
// Maybe we can suss out specific errors later. Need to study what errors
|
// Maybe we can suss out more specific errors later. Need to study what
|
||||||
// come from Decode.
|
// errors come from Decode.
|
||||||
errorJson(w, http.StatusBadRequest, "Error parsing JSON")
|
errorJson(w, http.StatusBadRequest, "Error parsing JSON")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -135,7 +172,7 @@ func getGetData(w http.ResponseWriter, req *http.Request) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - probably don't return all of authToken since we only need userId and
|
// TODO - probably don't return all of authToken since we only need userId and
|
||||||
// deviceId. Also this is apparently not idiomatic go error handling.
|
// deviceId.
|
||||||
func (s *Server) checkAuth(
|
func (s *Server) checkAuth(
|
||||||
w http.ResponseWriter,
|
w http.ResponseWriter,
|
||||||
token auth.AuthTokenString,
|
token auth.AuthTokenString,
|
||||||
|
@ -159,6 +196,22 @@ func (s *Server) checkAuth(
|
||||||
return authToken
|
return authToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Useful for any request where token is the only GET param to get
|
||||||
|
// TODO - There's probably a struct-based solution here like with POST/PUT.
|
||||||
|
func getTokenParam(req *http.Request) (token auth.AuthTokenString, err error) {
|
||||||
|
tokenSlice, hasTokenSlice := req.URL.Query()["token"]
|
||||||
|
|
||||||
|
if !hasTokenSlice || tokenSlice[0] == "" {
|
||||||
|
err = fmt.Errorf("Missing token parameter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
token = auth.AuthTokenString(tokenSlice[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// TODO - both wallet and token requests should be PUT, not POST.
|
// TODO - both wallet and token requests should be PUT, not POST.
|
||||||
// PUT = "...creates a new resource or replaces a representation of the target resource with the request payload."
|
// PUT = "...creates a new resource or replaces a representation of the target resource with the request payload."
|
||||||
|
|
||||||
|
@ -172,6 +225,14 @@ func (s *Server) wrongApiVersion(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serve(server *http.Server, done chan bool) {
|
||||||
|
log.Print("Server start")
|
||||||
|
server.ListenAndServe()
|
||||||
|
log.Print("Server finish")
|
||||||
|
|
||||||
|
done <- true
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) Serve() {
|
func (s *Server) Serve() {
|
||||||
http.HandleFunc(paths.PathAuthToken, s.getAuthToken)
|
http.HandleFunc(paths.PathAuthToken, s.getAuthToken)
|
||||||
http.HandleFunc(paths.PathWallet, s.handleWallet)
|
http.HandleFunc(paths.PathWallet, s.handleWallet)
|
||||||
|
@ -180,6 +241,7 @@ func (s *Server) Serve() {
|
||||||
http.HandleFunc(paths.PathVerify, s.verify)
|
http.HandleFunc(paths.PathVerify, s.verify)
|
||||||
http.HandleFunc(paths.PathResendVerify, s.resendVerifyEmail)
|
http.HandleFunc(paths.PathResendVerify, s.resendVerifyEmail)
|
||||||
http.HandleFunc(paths.PathClientSaltSeed, s.getClientSaltSeed)
|
http.HandleFunc(paths.PathClientSaltSeed, s.getClientSaltSeed)
|
||||||
|
http.HandleFunc(paths.PathWebsocket, s.websocket)
|
||||||
|
|
||||||
http.HandleFunc(paths.PathUnknownEndpoint, s.unknownEndpoint)
|
http.HandleFunc(paths.PathUnknownEndpoint, s.unknownEndpoint)
|
||||||
http.HandleFunc(paths.PathWrongApiVersion, s.wrongApiVersion)
|
http.HandleFunc(paths.PathWrongApiVersion, s.wrongApiVersion)
|
||||||
|
@ -187,5 +249,38 @@ func (s *Server) Serve() {
|
||||||
http.Handle(paths.PathPrometheus, promhttp.Handler())
|
http.Handle(paths.PathPrometheus, promhttp.Handler())
|
||||||
|
|
||||||
log.Printf("Serving at localhost:%d\n", s.port)
|
log.Printf("Serving at localhost:%d\n", s.port)
|
||||||
http.ListenAndServe(fmt.Sprintf("localhost:%d", s.port), nil)
|
|
||||||
|
// Signal *to* socket manager that it should finish (we use server.Shutdown
|
||||||
|
// to tell the server to finish)
|
||||||
|
socketsFinish := make(chan bool)
|
||||||
|
|
||||||
|
// Signal *from* server and socket manager that they are done:
|
||||||
|
serverDone := make(chan bool)
|
||||||
|
socketsDone := make(chan bool)
|
||||||
|
|
||||||
|
go s.manageSockets(socketsDone, socketsFinish)
|
||||||
|
|
||||||
|
server := http.Server{Addr: fmt.Sprintf("localhost:%d", s.port)}
|
||||||
|
go serve(&server, serverDone)
|
||||||
|
|
||||||
|
// Make sure that both the server and the websocket manager close properly on interrupt
|
||||||
|
interrupt := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(interrupt, os.Interrupt)
|
||||||
|
|
||||||
|
// Wait for the interrupt signal
|
||||||
|
<-interrupt
|
||||||
|
|
||||||
|
// Tell the server to finish and wait for it to do so. We want it to finish
|
||||||
|
// to guarantee no more incoming sockets before we turn off the socket
|
||||||
|
// manager.
|
||||||
|
server.Shutdown(context.Background())
|
||||||
|
<-serverDone
|
||||||
|
|
||||||
|
// The socket manager's cleanup procedure assumes that there will be no new
|
||||||
|
// socket connections. Now that the server is done, no new socket
|
||||||
|
// connections will be coming in, so we can close the socket manager.
|
||||||
|
socketsFinish <- true
|
||||||
|
<-socketsDone
|
||||||
|
|
||||||
|
log.Printf("All done")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,12 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/server/paths"
|
"lbryio/wallet-sync-server/server/paths"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
"lbryio/lbry-id/wallet"
|
"lbryio/wallet-sync-server/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TestPort = 8090
|
const TestPort = 8090
|
||||||
|
@ -131,6 +132,7 @@ type TestStore struct {
|
||||||
Errors TestStoreFunctionsErrors
|
Errors TestStoreFunctionsErrors
|
||||||
|
|
||||||
TestAuthToken auth.AuthToken
|
TestAuthToken auth.AuthToken
|
||||||
|
TestUserId auth.UserId
|
||||||
|
|
||||||
TestEncryptedWallet wallet.EncryptedWallet
|
TestEncryptedWallet wallet.EncryptedWallet
|
||||||
TestSequence wallet.Sequence
|
TestSequence wallet.Sequence
|
||||||
|
@ -203,7 +205,7 @@ func (s *TestStore) ChangePasswordWithWallet(
|
||||||
encryptedWallet wallet.EncryptedWallet,
|
encryptedWallet wallet.EncryptedWallet,
|
||||||
sequence wallet.Sequence,
|
sequence wallet.Sequence,
|
||||||
hmac wallet.WalletHmac,
|
hmac wallet.WalletHmac,
|
||||||
) (err error) {
|
) (auth.UserId, error) {
|
||||||
s.Called.ChangePasswordWithWallet = ChangePasswordWithWalletCall{
|
s.Called.ChangePasswordWithWallet = ChangePasswordWithWalletCall{
|
||||||
EncryptedWallet: encryptedWallet,
|
EncryptedWallet: encryptedWallet,
|
||||||
Sequence: sequence,
|
Sequence: sequence,
|
||||||
|
@ -213,7 +215,7 @@ func (s *TestStore) ChangePasswordWithWallet(
|
||||||
NewPassword: newPassword,
|
NewPassword: newPassword,
|
||||||
ClientSaltSeed: clientSaltSeed,
|
ClientSaltSeed: clientSaltSeed,
|
||||||
}
|
}
|
||||||
return s.Errors.ChangePasswordWithWallet
|
return s.TestUserId, s.Errors.ChangePasswordWithWallet
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TestStore) ChangePasswordNoWallet(
|
func (s *TestStore) ChangePasswordNoWallet(
|
||||||
|
@ -221,14 +223,14 @@ func (s *TestStore) ChangePasswordNoWallet(
|
||||||
oldPassword auth.Password,
|
oldPassword auth.Password,
|
||||||
newPassword auth.Password,
|
newPassword auth.Password,
|
||||||
clientSaltSeed auth.ClientSaltSeed,
|
clientSaltSeed auth.ClientSaltSeed,
|
||||||
) (err error) {
|
) (auth.UserId, error) {
|
||||||
s.Called.ChangePasswordNoWallet = ChangePasswordNoWalletCall{
|
s.Called.ChangePasswordNoWallet = ChangePasswordNoWalletCall{
|
||||||
Email: email,
|
Email: email,
|
||||||
OldPassword: oldPassword,
|
OldPassword: oldPassword,
|
||||||
NewPassword: newPassword,
|
NewPassword: newPassword,
|
||||||
ClientSaltSeed: clientSaltSeed,
|
ClientSaltSeed: clientSaltSeed,
|
||||||
}
|
}
|
||||||
return s.Errors.ChangePasswordNoWallet
|
return s.TestUserId, s.Errors.ChangePasswordNoWallet
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TestStore) GetClientSaltSeed(email auth.Email) (seed auth.ClientSaltSeed, err error) {
|
func (s *TestStore) GetClientSaltSeed(email auth.Email) (seed auth.ClientSaltSeed, err error) {
|
||||||
|
@ -269,6 +271,35 @@ func expectErrorString(t *testing.T, body []byte, expectedErrorString string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type wsMockManager struct {
|
||||||
|
s *Server
|
||||||
|
done chan bool
|
||||||
|
|
||||||
|
addedClientUserId auth.UserId
|
||||||
|
removedClientUserId auth.UserId
|
||||||
|
removedUserId auth.UserId
|
||||||
|
walletUpdateUserId auth.UserId
|
||||||
|
noMessage bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *wsMockManager) getOneMessage(timeout time.Duration) {
|
||||||
|
t := time.NewTicker(timeout)
|
||||||
|
select {
|
||||||
|
case msg := <-m.s.clientAdd:
|
||||||
|
m.addedClientUserId = msg.userId
|
||||||
|
case msg := <-m.s.clientRemove:
|
||||||
|
m.removedClientUserId = msg.userId
|
||||||
|
case msg := <-m.s.userRemove:
|
||||||
|
m.removedUserId = msg.userId
|
||||||
|
case msg := <-m.s.walletUpdates:
|
||||||
|
m.walletUpdateUserId = msg.userId
|
||||||
|
case <-t.C:
|
||||||
|
m.noMessage = true
|
||||||
|
}
|
||||||
|
t.Stop()
|
||||||
|
m.done <- true
|
||||||
|
}
|
||||||
|
|
||||||
func TestServerHelperCheckAuth(t *testing.T) {
|
func TestServerHelperCheckAuth(t *testing.T) {
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -324,7 +355,7 @@ func TestServerHelperCheckAuth(t *testing.T) {
|
||||||
Errors: tc.storeErrors,
|
Errors: tc.storeErrors,
|
||||||
TestAuthToken: auth.AuthToken{Token: auth.AuthTokenString("seekrit"), Scope: tc.userScope},
|
TestAuthToken: auth.AuthToken{Token: auth.AuthTokenString("seekrit"), Scope: tc.userScope},
|
||||||
}
|
}
|
||||||
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort}
|
s := Init(&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort)
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
authToken := s.checkAuth(w, testStore.TestAuthToken.Token, tc.requiredScope)
|
authToken := s.checkAuth(w, testStore.TestAuthToken.Token, tc.requiredScope)
|
||||||
|
@ -417,6 +448,13 @@ func TestServerHelperGetPostDataErrors(t *testing.T) {
|
||||||
expectedStatusCode: http.StatusBadRequest,
|
expectedStatusCode: http.StatusBadRequest,
|
||||||
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation: TestReq Error",
|
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation: TestReq Error",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "body JSON has unknown field",
|
||||||
|
method: http.MethodPost,
|
||||||
|
requestBody: `{"lol": "wut"}`,
|
||||||
|
expectedStatusCode: http.StatusBadRequest,
|
||||||
|
expectedErrorString: http.StatusText(http.StatusBadRequest) + `: json: unknown field "lol"`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|
|
@ -3,14 +3,16 @@ package server
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/metrics"
|
"lbryio/wallet-sync-server/metrics"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
"lbryio/lbry-id/wallet"
|
"lbryio/wallet-sync-server/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WalletRequest struct {
|
type WalletRequest struct {
|
||||||
|
@ -30,7 +32,7 @@ func (r *WalletRequest) validate() error {
|
||||||
if r.Hmac == "" {
|
if r.Hmac == "" {
|
||||||
return fmt.Errorf("Missing 'hmac'")
|
return fmt.Errorf("Missing 'hmac'")
|
||||||
}
|
}
|
||||||
if r.Sequence < 1 {
|
if r.Sequence < store.InitialWalletSequence {
|
||||||
return fmt.Errorf("Missing or zero-value 'sequence'")
|
return fmt.Errorf("Missing or zero-value 'sequence'")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -52,30 +54,14 @@ func (s *Server) handleWallet(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - There's probably a struct-based solution here like with POST/PUT.
|
|
||||||
// We could put that struct up top as well.
|
|
||||||
func getWalletParams(req *http.Request) (token auth.AuthTokenString, err error) {
|
|
||||||
tokenSlice, hasTokenSlice := req.URL.Query()["token"]
|
|
||||||
|
|
||||||
if !hasTokenSlice || tokenSlice[0] == "" {
|
|
||||||
err = fmt.Errorf("Missing token parameter")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
token = auth.AuthTokenString(tokenSlice[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) getWallet(w http.ResponseWriter, req *http.Request) {
|
func (s *Server) getWallet(w http.ResponseWriter, req *http.Request) {
|
||||||
metrics.RequestsCount.With(prometheus.Labels{"method": "GET wallet"}).Inc()
|
metrics.RequestsCount.With(prometheus.Labels{"method": "GET", "endpoint": "wallet"}).Inc()
|
||||||
|
|
||||||
if !getGetData(w, req) {
|
if !getGetData(w, req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, paramsErr := getWalletParams(req)
|
token, paramsErr := getTokenParam(req)
|
||||||
|
|
||||||
if paramsErr != nil {
|
if paramsErr != nil {
|
||||||
// In this specific case, the error is limited to values that are safe to
|
// In this specific case, the error is limited to values that are safe to
|
||||||
|
@ -123,7 +109,7 @@ func (s *Server) getWallet(w http.ResponseWriter, req *http.Request) {
|
||||||
// current wallet's sequence
|
// current wallet's sequence
|
||||||
// 500: Update unsuccessful for unanticipated reasons
|
// 500: Update unsuccessful for unanticipated reasons
|
||||||
func (s *Server) postWallet(w http.ResponseWriter, req *http.Request) {
|
func (s *Server) postWallet(w http.ResponseWriter, req *http.Request) {
|
||||||
metrics.RequestsCount.With(prometheus.Labels{"method": "POST wallet"}).Inc()
|
metrics.RequestsCount.With(prometheus.Labels{"method": "POST", "endpoint": "wallet"}).Inc()
|
||||||
|
|
||||||
var walletRequest WalletRequest
|
var walletRequest WalletRequest
|
||||||
if !getPostData(w, req, &walletRequest) {
|
if !getPostData(w, req, &walletRequest) {
|
||||||
|
@ -156,4 +142,20 @@ func (s *Server) postWallet(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(w, string(response))
|
fmt.Fprintf(w, string(response))
|
||||||
|
if walletRequest.Sequence == store.InitialWalletSequence {
|
||||||
|
log.Printf("Initial wallet created for user id %d", authToken.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inform the other clients over websockets. If we can't do it within 100
|
||||||
|
// milliseconds, don't bother. It's a nice-to-have, not mission critical.
|
||||||
|
// But, count the misses on the dashboard. If it happens a lot we should
|
||||||
|
// probably increase the buffer on the notify chans for the clients. Those
|
||||||
|
// will be a bottleneck within the socket manager.
|
||||||
|
timeout := time.NewTicker(100 * time.Millisecond)
|
||||||
|
select {
|
||||||
|
case s.walletUpdates <- walletUpdateMsg{authToken.UserId, walletRequest.Sequence}:
|
||||||
|
case <-timeout.C:
|
||||||
|
metrics.ErrorsCount.With(prometheus.Labels{"error_type": "ws-client-notify"}).Inc()
|
||||||
|
}
|
||||||
|
timeout.Stop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,12 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/server/paths"
|
"lbryio/wallet-sync-server/server/paths"
|
||||||
"lbryio/lbry-id/store"
|
"lbryio/wallet-sync-server/store"
|
||||||
"lbryio/lbry-id/wallet"
|
"lbryio/wallet-sync-server/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServerGetWallet(t *testing.T) {
|
func TestServerGetWallet(t *testing.T) {
|
||||||
|
@ -78,7 +79,7 @@ func TestServerGetWallet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
testEnv := TestEnv{}
|
testEnv := TestEnv{}
|
||||||
s := Server{&testAuth, &testStore, &testEnv, &TestMail{}, TestPort}
|
s := Init(&testAuth, &testStore, &testEnv, &TestMail{}, TestPort)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, paths.PathWallet, nil)
|
req := httptest.NewRequest(http.MethodGet, paths.PathWallet, nil)
|
||||||
q := req.URL.Query()
|
q := req.URL.Query()
|
||||||
|
@ -135,6 +136,7 @@ func TestServerPostWallet(t *testing.T) {
|
||||||
expectedStatusCode int
|
expectedStatusCode int
|
||||||
expectedErrorString string
|
expectedErrorString string
|
||||||
expectSetWalletCall bool
|
expectSetWalletCall bool
|
||||||
|
expectWsMsg bool
|
||||||
|
|
||||||
// This is getting messy, but in the case of validation failures, we don't
|
// This is getting messy, but in the case of validation failures, we don't
|
||||||
// even get around to trying to get an auth token, since the token string is
|
// even get around to trying to get an auth token, since the token string is
|
||||||
|
@ -155,6 +157,7 @@ func TestServerPostWallet(t *testing.T) {
|
||||||
name: "success",
|
name: "success",
|
||||||
expectedStatusCode: http.StatusOK,
|
expectedStatusCode: http.StatusOK,
|
||||||
expectSetWalletCall: true,
|
expectSetWalletCall: true,
|
||||||
|
expectWsMsg: true,
|
||||||
|
|
||||||
// Simulates a situation where the existing sequence is 1, the new
|
// Simulates a situation where the existing sequence is 1, the new
|
||||||
// sequence is 2.
|
// sequence is 2.
|
||||||
|
@ -225,18 +228,19 @@ func TestServerPostWallet(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|
||||||
testAuth := TestAuth{}
|
testAuth := TestAuth{}
|
||||||
testStore := TestStore{
|
testStore := TestStore{
|
||||||
TestAuthToken: auth.AuthToken{
|
TestAuthToken: auth.AuthToken{
|
||||||
Token: auth.AuthTokenString("seekrit"),
|
Token: auth.AuthTokenString("seekrit"),
|
||||||
Scope: auth.ScopeFull,
|
Scope: auth.ScopeFull,
|
||||||
|
UserId: auth.UserId(37),
|
||||||
},
|
},
|
||||||
|
|
||||||
Errors: tc.storeErrors,
|
Errors: tc.storeErrors,
|
||||||
}
|
}
|
||||||
|
|
||||||
s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort}
|
s := Init(&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort)
|
||||||
|
wsmm := wsMockManager{s: s, done: make(chan bool)}
|
||||||
|
|
||||||
requestBody := []byte(
|
requestBody := []byte(
|
||||||
fmt.Sprintf(`{
|
fmt.Sprintf(`{
|
||||||
|
@ -252,7 +256,15 @@ func TestServerPostWallet(t *testing.T) {
|
||||||
|
|
||||||
// test handleWallet while we're at it, which is a dispatch for get and post
|
// test handleWallet while we're at it, which is a dispatch for get and post
|
||||||
// wallet
|
// wallet
|
||||||
|
go wsmm.getOneMessage(100 * time.Millisecond)
|
||||||
s.handleWallet(w, req)
|
s.handleWallet(w, req)
|
||||||
|
<-wsmm.done
|
||||||
|
if tc.expectWsMsg && wsmm.walletUpdateUserId != testStore.TestAuthToken.UserId {
|
||||||
|
t.Error("Expected websocket message to update wallet")
|
||||||
|
}
|
||||||
|
if !tc.expectWsMsg && wsmm.walletUpdateUserId == testStore.TestAuthToken.UserId {
|
||||||
|
t.Error("Expected no websocket message to update wallet")
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure we tried to get an auth based on the `token` param (whether or
|
// Make sure we tried to get an auth based on the `token` param (whether or
|
||||||
// not it was a valid `token`)
|
// not it was a valid `token`)
|
||||||
|
|
285
server/websocket.go
Normal file
285
server/websocket.go
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"lbryio/wallet-sync-server/auth"
|
||||||
|
"lbryio/wallet-sync-server/wallet"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Using this as a guide:
|
||||||
|
// https://github.com/gorilla/websocket/blob/master/examples/chat/
|
||||||
|
//
|
||||||
|
// Skipping some things that seem like maybe overkill for a simple application,
|
||||||
|
// given that this isn't mission critical, and given that I'm not sure what a
|
||||||
|
// lot of it does. In particular the wsWriter ping stuff. But, we can add it if
|
||||||
|
// the performance is bad.
|
||||||
|
|
||||||
|
const pongWait = 60 * time.Second
|
||||||
|
const writeWait = 10 * time.Second
|
||||||
|
|
||||||
|
type wsClientNotifyType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// The channel is closed (i.e. this is the zero-value), so the socket should
|
||||||
|
// be closed too. We might not actually check for a closed channel using
|
||||||
|
// this value, but it's here for completeness.
|
||||||
|
wsClientNotifyFinish = wsClientNotifyType(iota)
|
||||||
|
|
||||||
|
// Inform the client about a wallet update
|
||||||
|
wsClientNotifyUpdate
|
||||||
|
)
|
||||||
|
|
||||||
|
// wsClientNotifyMsg is sent over wsClient.notify by the websocket manager
|
||||||
|
type wsClientNotifyMsg struct {
|
||||||
|
notifyType wsClientNotifyType
|
||||||
|
sequence wallet.Sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyChanBuffer = 5 // Each client shouldn't be getting a lot of concurrent messages
|
||||||
|
|
||||||
|
// Given a wsClientNotifyMsg of type wsClientNotifyUpdate, turn it into an
|
||||||
|
// appropriate message to the client to be sent over websocket
|
||||||
|
func walletUpdateWSMessage(msg wsClientNotifyMsg) []byte {
|
||||||
|
return []byte(fmt.Sprintf("wallet-update:%d", msg.sequence))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poor man's debug log
|
||||||
|
const debugWebsockets = false
|
||||||
|
|
||||||
|
func debugLog(format string, v ...any) {
|
||||||
|
if debugWebsockets {
|
||||||
|
log.Printf(format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represents a connection to a client.
|
||||||
|
type wsClient struct {
|
||||||
|
socket *websocket.Conn
|
||||||
|
notify chan wsClientNotifyMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each user with at least one actively connected client will have one of these
|
||||||
|
// associated.
|
||||||
|
type wsClientSet map[*wsClient]bool
|
||||||
|
|
||||||
|
// A message sent over a channel to indicate that the given client is
|
||||||
|
// connecting or disconnecting for the given user.
|
||||||
|
type wsClientForUser struct {
|
||||||
|
userId auth.UserId
|
||||||
|
client *wsClient
|
||||||
|
}
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{} // use default options
|
||||||
|
|
||||||
|
// Just handle ping/pong
|
||||||
|
func (s *Server) wsReader(userId auth.UserId, client *wsClient) {
|
||||||
|
defer func() {
|
||||||
|
// Since wsWriter is waiting on the notify channel, tell the manager to
|
||||||
|
// close it. This will make wsWriter stop (if it hasn't already).
|
||||||
|
s.clientRemove <- wsClientForUser{userId, client}
|
||||||
|
client.socket.Close()
|
||||||
|
|
||||||
|
debugLog("Done with wsReader %+v", client)
|
||||||
|
}()
|
||||||
|
|
||||||
|
client.socket.SetReadLimit(512)
|
||||||
|
client.socket.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
|
client.socket.SetPongHandler(func(string) error { client.socket.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||||
|
for {
|
||||||
|
_, _, err := client.socket.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
debugLog("wsReader: %s\n", err.Error())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) wsWriter(userId auth.UserId, client *wsClient) {
|
||||||
|
defer func() {
|
||||||
|
// Whatever the cause of closure here, closing the socket (if it's not
|
||||||
|
// closed already) will cause wsReader to stop (if it hasn't stopped
|
||||||
|
// already) since it's waiting on the socket.
|
||||||
|
client.socket.Close()
|
||||||
|
|
||||||
|
debugLog("Done with wsWriter %+v", client)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for notifyMsg := range client.notify {
|
||||||
|
if notifyMsg.notifyType != wsClientNotifyUpdate {
|
||||||
|
log.Printf("wsWriter: Got an unknown message type! %+v", notifyMsg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
debugLog("wsWriter: notify update")
|
||||||
|
client.socket.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
err := client.socket.WriteMessage(websocket.TextMessage, walletUpdateWSMessage(notifyMsg))
|
||||||
|
if err != nil {
|
||||||
|
debugLog("wsWriter: %s\n", err.Error())
|
||||||
|
return // skip close message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not sure what the point of this is, given that this probably
|
||||||
|
// wouldn't get triggered unless the socket already closed, but the
|
||||||
|
// example did this.
|
||||||
|
debugLog("wsWriter: sending CloseMessage")
|
||||||
|
client.socket.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
client.socket.WriteMessage(websocket.CloseMessage, []byte{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the server endpoint that initiates a new websocket
|
||||||
|
func (s *Server) websocket(w http.ResponseWriter, req *http.Request) {
|
||||||
|
token, paramsErr := getTokenParam(req)
|
||||||
|
|
||||||
|
if paramsErr != nil {
|
||||||
|
// In this specific case, the error is limited to values that are safe to
|
||||||
|
// give to the user.
|
||||||
|
errorJson(w, http.StatusBadRequest, paramsErr.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authToken := s.checkAuth(w, token, auth.ScopeFull)
|
||||||
|
|
||||||
|
if authToken == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
|
||||||
|
|
||||||
|
ws, err := upgrader.Upgrade(w, req, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := wsClient{ws, make(chan wsClientNotifyMsg, notifyChanBuffer)}
|
||||||
|
newClient := wsClientForUser{authToken.UserId, &client}
|
||||||
|
s.clientAdd <- newClient
|
||||||
|
|
||||||
|
go s.wsReader(authToken.UserId, &client)
|
||||||
|
go s.wsWriter(authToken.UserId, &client)
|
||||||
|
|
||||||
|
log.Println("Client Connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) manageSockets(done chan bool, finish chan bool) {
|
||||||
|
log.Println("Socket manager start")
|
||||||
|
clientsByUser := make(map[auth.UserId]wsClientSet)
|
||||||
|
|
||||||
|
removeClient := func(userId auth.UserId, client *wsClient) {
|
||||||
|
debugLog("removeClient %+v", client)
|
||||||
|
if _, ok := clientsByUser[userId]; !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := clientsByUser[userId][client]; !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
close(client.notify)
|
||||||
|
delete(clientsByUser[userId], client)
|
||||||
|
|
||||||
|
if len(clientsByUser[userId]) == 0 {
|
||||||
|
delete(clientsByUser, userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUser := func(userId auth.UserId) {
|
||||||
|
debugLog("removeUser (which calls removeClient) %d", userId)
|
||||||
|
|
||||||
|
for client := range clientsByUser[userId] {
|
||||||
|
removeClient(userId, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addClient := func(userId auth.UserId, client *wsClient) {
|
||||||
|
debugLog("addClient %+v", client)
|
||||||
|
if _, ok := clientsByUser[userId]; !ok {
|
||||||
|
clientsByUser[userId] = make(wsClientSet)
|
||||||
|
}
|
||||||
|
clientsByUser[userId][client] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
manage:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-s.walletUpdates:
|
||||||
|
for client := range clientsByUser[msg.userId] {
|
||||||
|
select {
|
||||||
|
case client.notify <- wsClientNotifyMsg{wsClientNotifyUpdate, msg.sequence}:
|
||||||
|
default:
|
||||||
|
log.Println("This is a bug: Channel was somehow closed but the manager has not (yet) received a clientRemove message.")
|
||||||
|
|
||||||
|
// The example program had this, but I don't see why.
|
||||||
|
removeClient(msg.userId, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case removedUser := <-s.userRemove:
|
||||||
|
removeUser(removedUser.userId)
|
||||||
|
case retiredClient := <-s.clientRemove:
|
||||||
|
removeClient(retiredClient.userId, retiredClient.client)
|
||||||
|
case newClient := <-s.clientAdd:
|
||||||
|
addClient(newClient.userId, newClient.client)
|
||||||
|
case <-finish:
|
||||||
|
break manage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Cleaning up sockets")
|
||||||
|
|
||||||
|
debugLog("Running any addClient messages that snuck in...")
|
||||||
|
|
||||||
|
// By the time the `finish` channel has triggered, the web server has shut
|
||||||
|
// down, so we won't have any clientAdd events _triggered_ by this point.
|
||||||
|
// However, one (or more, if we add a buffer later) may be in the queue, so
|
||||||
|
// let's keep track of them here so we can close them. But we close the
|
||||||
|
// clientAdd channel first so we can break out of this loop.
|
||||||
|
|
||||||
|
// We assume that the server is done writing at this point, thus it's safe
|
||||||
|
// to close this channel here.
|
||||||
|
close(s.clientAdd)
|
||||||
|
for newClient := range s.clientAdd {
|
||||||
|
addClient(newClient.userId, newClient.client)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we know about every running client, just close all of the sockets
|
||||||
|
// (double-closing seems to be safe, so we don't care about races here). But
|
||||||
|
// if it takes more than 10 seconds for whatever reason, just bail.
|
||||||
|
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
log.Println("Giving up on closing remaining sockets cleanly.")
|
||||||
|
|
||||||
|
// This will signal to main to exit, which will end the program
|
||||||
|
done <- true
|
||||||
|
|
||||||
|
log.Println("Socket manager impolite finish")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
debugLog("Closing sockets...")
|
||||||
|
for _, userClients := range clientsByUser {
|
||||||
|
for client := range userClients {
|
||||||
|
debugLog("Closing socket for %+v", client)
|
||||||
|
client.socket.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
client.socket.WriteMessage(websocket.CloseMessage, []byte{})
|
||||||
|
// TODO - wait for receiving the CloseMessage?
|
||||||
|
client.socket.Close()
|
||||||
|
debugLog("Closed socket for %+v", client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - Do we need to wait for the sockets to actually close after
|
||||||
|
// calling Close(), before exiting the program? Alternately: do they close
|
||||||
|
// automatically anyway and I don't need this cleanup stuff in the first
|
||||||
|
// place? (Probably doesn't automatically send the Close message at least.)
|
||||||
|
|
||||||
|
done <- true
|
||||||
|
log.Println("Socket manager finish")
|
||||||
|
}
|
35
server/websocket_test.go
Normal file
35
server/websocket_test.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebsocketManagerQuits(t *testing.T) {
|
||||||
|
s := Init(&TestAuth{}, &TestStore{}, &TestEnv{}, &TestMail{}, TestPort)
|
||||||
|
done := make(chan bool)
|
||||||
|
finish := make(chan bool)
|
||||||
|
|
||||||
|
go s.manageSockets(done, finish)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
t.Fatal("Websocket handler shouldn't be done yet")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
finish <- true
|
||||||
|
|
||||||
|
ticker := time.NewTicker(100 * time.Millisecond)
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-ticker.C:
|
||||||
|
t.Fatal("Websocket handler should be done by now")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Add some real tests. Making a meaningful test, given that we're dealing
|
||||||
|
// with websockets here, is a real pain in the ass, and it's probably not the
|
||||||
|
// highest priority right now. If websockets become higher profile we can work
|
||||||
|
// on it again.
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func expectAccountMatch(
|
func expectAccountMatch(
|
||||||
|
@ -20,17 +20,21 @@ func expectAccountMatch(
|
||||||
seed auth.ClientSaltSeed,
|
seed auth.ClientSaltSeed,
|
||||||
expectedVerifyTokenString *auth.VerifyTokenString,
|
expectedVerifyTokenString *auth.VerifyTokenString,
|
||||||
approxVerifyExpiration *time.Time,
|
approxVerifyExpiration *time.Time,
|
||||||
|
approxCreated time.Time,
|
||||||
|
approxUpdated time.Time,
|
||||||
) {
|
) {
|
||||||
var key auth.KDFKey
|
var key auth.KDFKey
|
||||||
var salt auth.ServerSalt
|
var salt auth.ServerSalt
|
||||||
var email auth.Email
|
var email auth.Email
|
||||||
var verifyExpiration *time.Time
|
var verifyExpiration *time.Time
|
||||||
var verifyTokenString *auth.VerifyTokenString
|
var verifyTokenString *auth.VerifyTokenString
|
||||||
|
var created time.Time
|
||||||
|
var updated time.Time
|
||||||
|
|
||||||
err := s.db.QueryRow(
|
err := s.db.QueryRow(
|
||||||
`SELECT key, server_salt, email, verify_token, verify_expiration from accounts WHERE normalized_email=? AND client_salt_seed=?`,
|
`SELECT key, server_salt, email, verify_token, verify_expiration, created, updated from accounts WHERE normalized_email=? AND client_salt_seed=?`,
|
||||||
normEmail, seed,
|
normEmail, seed,
|
||||||
).Scan(&key, &salt, &email, &verifyTokenString, &verifyExpiration)
|
).Scan(&key, &salt, &email, &verifyTokenString, &verifyExpiration, &created, &updated)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error finding account for: %s %s - %+v", normEmail, password, err)
|
t.Fatalf("Error finding account for: %s %s - %+v", normEmail, password, err)
|
||||||
}
|
}
|
||||||
|
@ -73,14 +77,32 @@ func expectAccountMatch(
|
||||||
if time.Second < expDiff || expDiff < -time.Second {
|
if time.Second < expDiff || expDiff < -time.Second {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"Verify expiration not as expected. Want approximately: %s Got: %s",
|
"Verify expiration not as expected. Want approximately: %s Got: %s",
|
||||||
verifyExpiration,
|
|
||||||
approxVerifyExpiration,
|
approxVerifyExpiration,
|
||||||
|
verifyExpiration,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if approxVerifyExpiration == nil && verifyExpiration != nil {
|
if approxVerifyExpiration == nil && verifyExpiration != nil {
|
||||||
t.Fatalf("Expected verify expiration to be nil. Got: %+v", verifyExpiration)
|
t.Fatalf("Expected verify expiration to be nil. Got: %+v", verifyExpiration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expDiff := approxCreated.Sub(created)
|
||||||
|
if time.Second*2 < expDiff || expDiff < -time.Second*2 {
|
||||||
|
t.Fatalf(
|
||||||
|
"Created timestamp not as expected. Want approximately: %s Got: %s",
|
||||||
|
approxCreated,
|
||||||
|
created,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
expDiff = approxUpdated.Sub(updated)
|
||||||
|
if time.Second*2 < expDiff || expDiff < -time.Second*2 {
|
||||||
|
t.Fatalf(
|
||||||
|
"Updated timestamp not as expected. Want approximately: %s Got: %s",
|
||||||
|
approxUpdated,
|
||||||
|
updated,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectAccountNotExists(t *testing.T, s *Store, normEmail auth.NormalizedEmail) {
|
func expectAccountNotExists(t *testing.T, s *Store, normEmail auth.NormalizedEmail) {
|
||||||
|
@ -119,7 +141,7 @@ func TestStoreCreateAccount(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get and confirm the account we just put in
|
// Get and confirm the account we just put in
|
||||||
expectAccountMatch(t, &s, normEmail, email, password, seed, nil, nil)
|
expectAccountMatch(t, &s, normEmail, email, password, seed, nil, nil, time.Now().UTC(), time.Now().UTC())
|
||||||
|
|
||||||
newPassword := auth.Password("xyz")
|
newPassword := auth.Password("xyz")
|
||||||
|
|
||||||
|
@ -138,7 +160,7 @@ func TestStoreCreateAccount(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the email and same *first* password we successfully put in
|
// Get the email and same *first* password we successfully put in
|
||||||
expectAccountMatch(t, &s, normEmail, email, password, seed, nil, nil)
|
expectAccountMatch(t, &s, normEmail, email, password, seed, nil, nil, time.Now().UTC(), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that I can use CreateAccount twice for different emails with no veriy token
|
// Test that I can use CreateAccount twice for different emails with no veriy token
|
||||||
|
@ -165,8 +187,8 @@ func TestStoreCreateAccountTwoVerifiedSucceed(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get and confirm the accounts we just put in
|
// Get and confirm the accounts we just put in
|
||||||
expectAccountMatch(t, &s, normEmail1, email1, password1, seed1, nil, nil)
|
expectAccountMatch(t, &s, normEmail1, email1, password1, seed1, nil, nil, time.Now().UTC(), time.Now().UTC())
|
||||||
expectAccountMatch(t, &s, normEmail2, email2, password2, seed2, nil, nil)
|
expectAccountMatch(t, &s, normEmail2, email2, password2, seed2, nil, nil, time.Now().UTC(), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that I cannot use CreateAccount twice with the same verify token, but
|
// Test that I cannot use CreateAccount twice with the same verify token, but
|
||||||
|
@ -204,8 +226,8 @@ func TestStoreCreateAccountTwoSameVerfiyTokenFail(t *testing.T) {
|
||||||
|
|
||||||
// Get and confirm the accounts we just put in
|
// Get and confirm the accounts we just put in
|
||||||
approxVerifyExpiration := time.Now().Add(time.Hour * 24 * 2).UTC()
|
approxVerifyExpiration := time.Now().Add(time.Hour * 24 * 2).UTC()
|
||||||
expectAccountMatch(t, &s, normEmail1, email1, password1, seed1, &verifyToken1, &approxVerifyExpiration)
|
expectAccountMatch(t, &s, normEmail1, email1, password1, seed1, &verifyToken1, &approxVerifyExpiration, time.Now().UTC(), time.Now().UTC())
|
||||||
expectAccountMatch(t, &s, normEmail2, email2, password2, seed2, &verifyToken2, &approxVerifyExpiration)
|
expectAccountMatch(t, &s, normEmail2, email2, password2, seed2, &verifyToken2, &approxVerifyExpiration, time.Now().UTC(), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try CreateAccount with a verification string, thus unverified
|
// Try CreateAccount with a verification string, thus unverified
|
||||||
|
@ -224,7 +246,7 @@ func TestStoreCreateAccountUnverified(t *testing.T) {
|
||||||
|
|
||||||
// Get and confirm the account we just put in
|
// Get and confirm the account we just put in
|
||||||
approxVerifyExpiration := time.Now().Add(time.Hour * 24 * 2).UTC()
|
approxVerifyExpiration := time.Now().Add(time.Hour * 24 * 2).UTC()
|
||||||
expectAccountMatch(t, &s, normEmail, email, password, seed, &verifyToken, &approxVerifyExpiration)
|
expectAccountMatch(t, &s, normEmail, email, password, seed, &verifyToken, &approxVerifyExpiration, time.Now().UTC(), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test GetUserId for nonexisting email
|
// Test GetUserId for nonexisting email
|
||||||
|
@ -380,12 +402,12 @@ func TestUpdateVerifyTokenStringSuccess(t *testing.T) {
|
||||||
if err := s.UpdateVerifyTokenString(lowerEmail, verifyTokenString2); err != nil {
|
if err := s.UpdateVerifyTokenString(lowerEmail, verifyTokenString2); err != nil {
|
||||||
t.Fatalf("Unexpected error in UpdateVerifyTokenString: err: %+v", err)
|
t.Fatalf("Unexpected error in UpdateVerifyTokenString: err: %+v", err)
|
||||||
}
|
}
|
||||||
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, &verifyTokenString2, &approxVerifyExpiration)
|
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, &verifyTokenString2, &approxVerifyExpiration, time.Now().UTC(), time.Now().UTC())
|
||||||
|
|
||||||
if err := s.UpdateVerifyTokenString(upperEmail, verifyTokenString3); err != nil {
|
if err := s.UpdateVerifyTokenString(upperEmail, verifyTokenString3); err != nil {
|
||||||
t.Fatalf("Unexpected error in UpdateVerifyTokenString: err: %+v", err)
|
t.Fatalf("Unexpected error in UpdateVerifyTokenString: err: %+v", err)
|
||||||
}
|
}
|
||||||
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, &verifyTokenString3, &approxVerifyExpiration)
|
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, &verifyTokenString3, &approxVerifyExpiration, time.Now().UTC(), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test UpdateVerifyTokenString for nonexisting email
|
// Test UpdateVerifyTokenString for nonexisting email
|
||||||
|
@ -418,9 +440,9 @@ func TestUpdateVerifyAccountSuccess(t *testing.T) {
|
||||||
defer StoreTestCleanup(sqliteTmpFile)
|
defer StoreTestCleanup(sqliteTmpFile)
|
||||||
|
|
||||||
verifyTokenString := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
|
verifyTokenString := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
|
||||||
time1 := time.Time{}
|
verifyExpiration := time.Now().Add(time.Second * 10).UTC() // expires in one second
|
||||||
|
|
||||||
_, email, password, createdSeed := makeTestUser(t, &s, &verifyTokenString, &time1)
|
_, email, password, createdSeed := makeTestUser(t, &s, &verifyTokenString, &verifyExpiration)
|
||||||
|
|
||||||
// we're not testing normalization features so we'll just use this here
|
// we're not testing normalization features so we'll just use this here
|
||||||
normEmail := email.Normalize()
|
normEmail := email.Normalize()
|
||||||
|
@ -428,7 +450,7 @@ func TestUpdateVerifyAccountSuccess(t *testing.T) {
|
||||||
if err := s.VerifyAccount(verifyTokenString); err != nil {
|
if err := s.VerifyAccount(verifyTokenString); err != nil {
|
||||||
t.Fatalf("Unexpected error in VerifyAccount: err: %+v", err)
|
t.Fatalf("Unexpected error in VerifyAccount: err: %+v", err)
|
||||||
}
|
}
|
||||||
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, nil, nil)
|
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, nil, nil, time.Now().UTC(), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test VerifyAccount for nonexisting token
|
// Test VerifyAccount for nonexisting token
|
||||||
|
@ -440,3 +462,23 @@ func TestStoreVerifyAccountTokenNotExists(t *testing.T) {
|
||||||
t.Fatalf(`VerifyAccount error for nonexistant token: wanted "%+v", got "%+v."`, ErrNoTokenForUser, err)
|
t.Fatalf(`VerifyAccount error for nonexistant token: wanted "%+v", got "%+v."`, ErrNoTokenForUser, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test VerifyAccount for expired token
|
||||||
|
func TestUpdateVerifyAccountTokenExpired(t *testing.T) {
|
||||||
|
s, sqliteTmpFile := StoreTestInit(t)
|
||||||
|
defer StoreTestCleanup(sqliteTmpFile)
|
||||||
|
|
||||||
|
verifyTokenString := auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
|
||||||
|
verifyExpiration := time.Now().Add(time.Second * (-1)).UTC() // expired one second ago
|
||||||
|
|
||||||
|
_, email, password, createdSeed := makeTestUser(t, &s, &verifyTokenString, &verifyExpiration)
|
||||||
|
|
||||||
|
// we're not testing normalization features so we'll just use this here
|
||||||
|
normEmail := email.Normalize()
|
||||||
|
|
||||||
|
if err := s.VerifyAccount(verifyTokenString); err != ErrNoTokenForUser {
|
||||||
|
t.Fatalf(`VerifyAccount error for expired token: wanted "%+v", got "%+v."`, ErrNoTokenForUser, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectAccountMatch(t, &s, normEmail, email, password, createdSeed, &verifyTokenString, &verifyExpiration, time.Now().UTC(), time.Now().UTC())
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func expectTokenExists(t *testing.T, s *Store, expectedToken auth.AuthToken) {
|
func expectTokenExists(t *testing.T, s *Store, expectedToken auth.AuthToken) {
|
||||||
|
@ -310,7 +310,7 @@ func TestStoreGetToken(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the token to be expired
|
// Update the token to be expired
|
||||||
expirationOld := time.Now().Add(time.Second * (-1))
|
expirationOld := time.Now().Add(time.Second * (-1)).UTC()
|
||||||
if err := s.updateToken(&authToken, expirationOld); err != nil {
|
if err := s.updateToken(&authToken, expirationOld); err != nil {
|
||||||
t.Fatalf("Unexpected error in updateToken: %+v", err)
|
t.Fatalf("Unexpected error in updateToken: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/wallet"
|
"lbryio/wallet-sync-server/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// It involves both wallet and account tables. Should it go in wallet_test.go
|
// It involves both wallet and account tables. Should it go in wallet_test.go
|
||||||
|
@ -28,11 +28,11 @@ func TestStoreChangePasswordSuccess(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.db.Exec(
|
_, err = s.db.Exec(
|
||||||
"INSERT INTO wallets (user_id, encrypted_wallet, sequence, hmac) VALUES(?,?,?,?)",
|
"INSERT INTO wallets (user_id, encrypted_wallet, sequence, hmac, updated) VALUES(?,?,?,?,datetime('now'))",
|
||||||
userId, "my-enc-wallet", 1, "my-hmac",
|
userId, "my-enc-wallet", 1, "my-hmac",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error creating test wallet")
|
t.Fatalf("Error creating test wallet: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
newPassword := oldPassword + auth.Password("_new")
|
newPassword := oldPassword + auth.Password("_new")
|
||||||
|
@ -43,12 +43,16 @@ func TestStoreChangePasswordSuccess(t *testing.T) {
|
||||||
|
|
||||||
lowerEmail := auth.Email(strings.ToLower(string(email)))
|
lowerEmail := auth.Email(strings.ToLower(string(email)))
|
||||||
|
|
||||||
if err := s.ChangePasswordWithWallet(lowerEmail, oldPassword, newPassword, newSeed, encryptedWallet, sequence, hmac); err != nil {
|
pwUserId, err := s.ChangePasswordWithWallet(lowerEmail, oldPassword, newPassword, newSeed, encryptedWallet, sequence, hmac)
|
||||||
|
if err != nil {
|
||||||
t.Errorf("ChangePasswordWithWallet (lower case email): unexpected error: %+v", err)
|
t.Errorf("ChangePasswordWithWallet (lower case email): unexpected error: %+v", err)
|
||||||
}
|
}
|
||||||
|
if userId != pwUserId {
|
||||||
|
t.Errorf("Expected ChangePasswordWithWallet to return correct user Id. Want %d got %d", userId, pwUserId)
|
||||||
|
}
|
||||||
|
|
||||||
expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed, nil, nil)
|
expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed, nil, nil, time.Now().UTC(), time.Now().UTC())
|
||||||
expectWalletExists(t, &s, userId, encryptedWallet, sequence, hmac)
|
expectWalletExists(t, &s, userId, encryptedWallet, sequence, hmac, time.Now().UTC())
|
||||||
expectTokenNotExists(t, &s, token)
|
expectTokenNotExists(t, &s, token)
|
||||||
|
|
||||||
newNewPassword := newPassword + auth.Password("_new")
|
newNewPassword := newPassword + auth.Password("_new")
|
||||||
|
@ -59,11 +63,15 @@ func TestStoreChangePasswordSuccess(t *testing.T) {
|
||||||
|
|
||||||
upperEmail := auth.Email(strings.ToUpper(string(email)))
|
upperEmail := auth.Email(strings.ToUpper(string(email)))
|
||||||
|
|
||||||
if err := s.ChangePasswordWithWallet(upperEmail, newPassword, newNewPassword, newNewSeed, newEncryptedWallet, newSequence, newHmac); err != nil {
|
pwUserId, err = s.ChangePasswordWithWallet(upperEmail, newPassword, newNewPassword, newNewSeed, newEncryptedWallet, newSequence, newHmac)
|
||||||
|
if err != nil {
|
||||||
t.Errorf("ChangePasswordWithWallet (upper case email): unexpected error: %+v", err)
|
t.Errorf("ChangePasswordWithWallet (upper case email): unexpected error: %+v", err)
|
||||||
}
|
}
|
||||||
|
if userId != pwUserId {
|
||||||
|
t.Errorf("Expected ChangePasswordWithWallet to return correct user Id. Want %d got %d", userId, pwUserId)
|
||||||
|
}
|
||||||
|
|
||||||
expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed, nil, nil)
|
expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed, nil, nil, time.Now().UTC(), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStoreChangePasswordErrors(t *testing.T) {
|
func TestStoreChangePasswordErrors(t *testing.T) {
|
||||||
|
@ -152,11 +160,11 @@ func TestStoreChangePasswordErrors(t *testing.T) {
|
||||||
|
|
||||||
if tc.hasWallet {
|
if tc.hasWallet {
|
||||||
_, err := s.db.Exec(
|
_, err := s.db.Exec(
|
||||||
"INSERT INTO wallets (user_id, encrypted_wallet, sequence, hmac) VALUES(?,?,?,?)",
|
"INSERT INTO wallets (user_id, encrypted_wallet, sequence, hmac, updated) VALUES(?,?,?,?,datetime('now'))",
|
||||||
userId, oldEncryptedWallet, oldSequence, oldHmac,
|
userId, oldEncryptedWallet, oldSequence, oldHmac,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error creating test wallet")
|
t.Fatalf("Error creating test wallet: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +173,7 @@ func TestStoreChangePasswordErrors(t *testing.T) {
|
||||||
newPassword := oldPassword + auth.Password("_new") // Make the new password different (as it should be)
|
newPassword := oldPassword + auth.Password("_new") // Make the new password different (as it should be)
|
||||||
newSeed := auth.ClientSaltSeed("edf98765edf98765edf98765edf98765edf98765edf98765edf98765edf98765")
|
newSeed := auth.ClientSaltSeed("edf98765edf98765edf98765edf98765edf98765edf98765edf98765edf98765")
|
||||||
|
|
||||||
if err := s.ChangePasswordWithWallet(submittedEmail, submittedOldPassword, newPassword, newSeed, newEncryptedWallet, tc.sequence, newHmac); err != tc.expectedError {
|
if _, err := s.ChangePasswordWithWallet(submittedEmail, submittedOldPassword, newPassword, newSeed, newEncryptedWallet, tc.sequence, newHmac); err != tc.expectedError {
|
||||||
t.Errorf("ChangePasswordWithWallet: unexpected value for err. want: %+v, got: %+v", tc.expectedError, err)
|
t.Errorf("ChangePasswordWithWallet: unexpected value for err. want: %+v, got: %+v", tc.expectedError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,9 +181,9 @@ func TestStoreChangePasswordErrors(t *testing.T) {
|
||||||
// This tests the transaction rollbacks in particular, given the errors
|
// This tests the transaction rollbacks in particular, given the errors
|
||||||
// that are at a couple different stages of the txn, triggered by these
|
// that are at a couple different stages of the txn, triggered by these
|
||||||
// tests.
|
// tests.
|
||||||
expectAccountMatch(t, &s, email.Normalize(), email, oldPassword, oldSeed, tc.verifyToken, tc.verifyExpiration)
|
expectAccountMatch(t, &s, email.Normalize(), email, oldPassword, oldSeed, tc.verifyToken, tc.verifyExpiration, time.Now().UTC(), time.Now().UTC())
|
||||||
if tc.hasWallet {
|
if tc.hasWallet {
|
||||||
expectWalletExists(t, &s, userId, oldEncryptedWallet, oldSequence, oldHmac)
|
expectWalletExists(t, &s, userId, oldEncryptedWallet, oldSequence, oldHmac, time.Now().UTC())
|
||||||
} else {
|
} else {
|
||||||
expectWalletNotExists(t, &s, userId)
|
expectWalletNotExists(t, &s, userId)
|
||||||
}
|
}
|
||||||
|
@ -204,11 +212,15 @@ func TestStoreChangePasswordNoWalletSuccess(t *testing.T) {
|
||||||
|
|
||||||
lowerEmail := auth.Email(strings.ToLower(string(email)))
|
lowerEmail := auth.Email(strings.ToLower(string(email)))
|
||||||
|
|
||||||
if err := s.ChangePasswordNoWallet(lowerEmail, oldPassword, newPassword, newSeed); err != nil {
|
pwUserId, err := s.ChangePasswordNoWallet(lowerEmail, oldPassword, newPassword, newSeed)
|
||||||
|
if err != nil {
|
||||||
t.Errorf("ChangePasswordNoWallet (lower case email): unexpected error: %+v", err)
|
t.Errorf("ChangePasswordNoWallet (lower case email): unexpected error: %+v", err)
|
||||||
}
|
}
|
||||||
|
if userId != pwUserId {
|
||||||
|
t.Errorf("Expected ChangePasswordNoWallet to return correct user Id. Want %d got %d", userId, pwUserId)
|
||||||
|
}
|
||||||
|
|
||||||
expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed, nil, nil)
|
expectAccountMatch(t, &s, email.Normalize(), email, newPassword, newSeed, nil, nil, time.Now().UTC(), time.Now().UTC())
|
||||||
expectWalletNotExists(t, &s, userId)
|
expectWalletNotExists(t, &s, userId)
|
||||||
expectTokenNotExists(t, &s, token)
|
expectTokenNotExists(t, &s, token)
|
||||||
|
|
||||||
|
@ -217,11 +229,16 @@ func TestStoreChangePasswordNoWalletSuccess(t *testing.T) {
|
||||||
|
|
||||||
upperEmail := auth.Email(strings.ToUpper(string(email)))
|
upperEmail := auth.Email(strings.ToUpper(string(email)))
|
||||||
|
|
||||||
if err := s.ChangePasswordNoWallet(upperEmail, newPassword, newNewPassword, newNewSeed); err != nil {
|
pwUserId, err = s.ChangePasswordNoWallet(upperEmail, newPassword, newNewPassword, newNewSeed)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
t.Errorf("ChangePasswordNoWallet (upper case email): unexpected error: %+v", err)
|
t.Errorf("ChangePasswordNoWallet (upper case email): unexpected error: %+v", err)
|
||||||
}
|
}
|
||||||
|
if userId != pwUserId {
|
||||||
|
t.Errorf("Expected ChangePasswordNoWallet to return correct user Id. Want %d got %d", userId, pwUserId)
|
||||||
|
}
|
||||||
|
|
||||||
expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed, nil, nil)
|
expectAccountMatch(t, &s, email.Normalize(), email, newNewPassword, newNewSeed, nil, nil, time.Now().UTC(), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStoreChangePasswordNoWalletErrors(t *testing.T) {
|
func TestStoreChangePasswordNoWalletErrors(t *testing.T) {
|
||||||
|
@ -295,11 +312,11 @@ func TestStoreChangePasswordNoWalletErrors(t *testing.T) {
|
||||||
|
|
||||||
if tc.hasWallet {
|
if tc.hasWallet {
|
||||||
_, err := s.db.Exec(
|
_, err := s.db.Exec(
|
||||||
"INSERT INTO wallets (user_id, encrypted_wallet, sequence, hmac) VALUES(?,?,?,?)",
|
"INSERT INTO wallets (user_id, encrypted_wallet, sequence, hmac, updated) VALUES(?,?,?,?,datetime('now'))",
|
||||||
userId, encryptedWallet, sequence, hmac,
|
userId, encryptedWallet, sequence, hmac,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error creating test wallet")
|
t.Fatalf("Error creating test wallet: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,7 +325,7 @@ func TestStoreChangePasswordNoWalletErrors(t *testing.T) {
|
||||||
newPassword := oldPassword + auth.Password("_new") // Possibly make the new password different (as it should be)
|
newPassword := oldPassword + auth.Password("_new") // Possibly make the new password different (as it should be)
|
||||||
newSeed := auth.ClientSaltSeed("edf98765edf98765edf98765edf98765edf98765edf98765edf98765edf98765")
|
newSeed := auth.ClientSaltSeed("edf98765edf98765edf98765edf98765edf98765edf98765edf98765edf98765")
|
||||||
|
|
||||||
if err := s.ChangePasswordNoWallet(submittedEmail, submittedOldPassword, newPassword, newSeed); err != tc.expectedError {
|
if _, err := s.ChangePasswordNoWallet(submittedEmail, submittedOldPassword, newPassword, newSeed); err != tc.expectedError {
|
||||||
t.Errorf("ChangePasswordNoWallet: unexpected value for err. want: %+v, got: %+v", tc.expectedError, err)
|
t.Errorf("ChangePasswordNoWallet: unexpected value for err. want: %+v, got: %+v", tc.expectedError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,9 +333,9 @@ func TestStoreChangePasswordNoWalletErrors(t *testing.T) {
|
||||||
// deleted. This tests the transaction rollbacks in particular, given the
|
// deleted. This tests the transaction rollbacks in particular, given the
|
||||||
// errors that are at a couple different stages of the txn, triggered by
|
// errors that are at a couple different stages of the txn, triggered by
|
||||||
// these tests.
|
// these tests.
|
||||||
expectAccountMatch(t, &s, email.Normalize(), email, oldPassword, oldSeed, tc.verifyToken, tc.verifyExpiration)
|
expectAccountMatch(t, &s, email.Normalize(), email, oldPassword, oldSeed, tc.verifyToken, tc.verifyExpiration, time.Now().UTC(), time.Now().UTC())
|
||||||
if tc.hasWallet {
|
if tc.hasWallet {
|
||||||
expectWalletExists(t, &s, userId, encryptedWallet, sequence, hmac)
|
expectWalletExists(t, &s, userId, encryptedWallet, sequence, hmac, time.Now().UTC())
|
||||||
} else {
|
} else {
|
||||||
expectWalletNotExists(t, &s, userId)
|
expectWalletNotExists(t, &s, userId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,8 @@ import (
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/wallet"
|
"lbryio/wallet-sync-server/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -37,6 +37,10 @@ var (
|
||||||
const (
|
const (
|
||||||
AuthTokenLifespan = time.Hour * 24 * 14
|
AuthTokenLifespan = time.Hour * 24 * 14
|
||||||
VerifyTokenLifespan = time.Hour * 24 * 2
|
VerifyTokenLifespan = time.Hour * 24 * 2
|
||||||
|
|
||||||
|
// Eventually it could become variable when we introduce server switching. A user
|
||||||
|
// might be on a later sequence when they switch from another server.
|
||||||
|
InitialWalletSequence = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// For test stubs
|
// For test stubs
|
||||||
|
@ -49,8 +53,8 @@ type StoreInterface interface {
|
||||||
CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed, *auth.VerifyTokenString) error
|
CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed, *auth.VerifyTokenString) error
|
||||||
UpdateVerifyTokenString(auth.Email, auth.VerifyTokenString) error
|
UpdateVerifyTokenString(auth.Email, auth.VerifyTokenString) error
|
||||||
VerifyAccount(auth.VerifyTokenString) error
|
VerifyAccount(auth.VerifyTokenString) error
|
||||||
ChangePasswordWithWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error
|
ChangePasswordWithWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) (auth.UserId, error)
|
||||||
ChangePasswordNoWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed) error
|
ChangePasswordNoWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed) (auth.UserId, error)
|
||||||
GetClientSaltSeed(auth.Email) (auth.ClientSaltSeed, error)
|
GetClientSaltSeed(auth.Email) (auth.ClientSaltSeed, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,6 +115,8 @@ func (s *Store) Migrate() error {
|
||||||
encrypted_wallet TEXT NOT NULL,
|
encrypted_wallet TEXT NOT NULL,
|
||||||
sequence INTEGER NOT NULL,
|
sequence INTEGER NOT NULL,
|
||||||
hmac TEXT NOT NULL,
|
hmac TEXT NOT NULL,
|
||||||
|
updated DATETIME NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (user_id)
|
PRIMARY KEY (user_id)
|
||||||
FOREIGN KEY (user_id) REFERENCES accounts(user_id)
|
FOREIGN KEY (user_id) REFERENCES accounts(user_id)
|
||||||
CHECK (
|
CHECK (
|
||||||
|
@ -134,6 +140,8 @@ func (s *Store) Migrate() error {
|
||||||
|
|
||||||
verify_expiration DATETIME,
|
verify_expiration DATETIME,
|
||||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
created DATETIME DEFAULT (DATETIME('now')),
|
||||||
|
updated DATETIME NOT NULL,
|
||||||
CHECK (
|
CHECK (
|
||||||
email <> '' AND
|
email <> '' AND
|
||||||
normalized_email <> '' AND
|
normalized_email <> '' AND
|
||||||
|
@ -277,12 +285,12 @@ func (s *Store) insertFirstWallet(
|
||||||
encryptedWallet wallet.EncryptedWallet,
|
encryptedWallet wallet.EncryptedWallet,
|
||||||
hmac wallet.WalletHmac,
|
hmac wallet.WalletHmac,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
// This will only be used to attempt to insert the first wallet (sequence=1).
|
// This will only be used to attempt to insert the first wallet (sequence=InitialWalletSequence).
|
||||||
// The database will enforce that this will not be set if this user already
|
// The database will enforce that this will not be set if this user already
|
||||||
// has a wallet.
|
// has a wallet.
|
||||||
_, err = s.db.Exec(
|
_, err = s.db.Exec(
|
||||||
"INSERT INTO wallets (user_id, encrypted_wallet, sequence, hmac) VALUES(?,?,?,?)",
|
"INSERT INTO wallets (user_id, encrypted_wallet, sequence, hmac, updated) VALUES(?,?,?,?, datetime('now'))",
|
||||||
userId, encryptedWallet, 1, hmac,
|
userId, encryptedWallet, InitialWalletSequence, hmac,
|
||||||
)
|
)
|
||||||
|
|
||||||
var sqliteErr sqlite3.Error
|
var sqliteErr sqlite3.Error
|
||||||
|
@ -305,12 +313,12 @@ func (s *Store) updateWalletToSequence(
|
||||||
sequence wallet.Sequence,
|
sequence wallet.Sequence,
|
||||||
hmac wallet.WalletHmac,
|
hmac wallet.WalletHmac,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
// This will be used for wallets with sequence > 1.
|
// This will be used for wallets with sequence > InitialWalletSequence.
|
||||||
// Use the database to enforce that we only update if we are incrementing the sequence.
|
// Use the database to enforce that we only update if we are incrementing the sequence.
|
||||||
// This way, if two clients attempt to update at the same time, it will return
|
// This way, if two clients attempt to update at the same time, it will return
|
||||||
// an error for the second one.
|
// an error for the second one.
|
||||||
res, err := s.db.Exec(
|
res, err := s.db.Exec(
|
||||||
"UPDATE wallets SET encrypted_wallet=?, sequence=?, hmac=? WHERE user_id=? AND sequence=?",
|
"UPDATE wallets SET encrypted_wallet=?, sequence=?, hmac=?, updated=datetime('now') WHERE user_id=? AND sequence=?",
|
||||||
encryptedWallet, sequence, hmac, userId, sequence-1,
|
encryptedWallet, sequence, hmac, userId, sequence-1,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -329,22 +337,22 @@ func (s *Store) updateWalletToSequence(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assumption: Sequence has been validated (>=1)
|
// Assumption: Sequence has been validated (>=InitialWalletSequence)
|
||||||
// Assumption: Auth token has been checked (thus account is verified)
|
// Assumption: Auth token has been checked (thus account is verified)
|
||||||
func (s *Store) SetWallet(userId auth.UserId, encryptedWallet wallet.EncryptedWallet, sequence wallet.Sequence, hmac wallet.WalletHmac) (err error) {
|
func (s *Store) SetWallet(userId auth.UserId, encryptedWallet wallet.EncryptedWallet, sequence wallet.Sequence, hmac wallet.WalletHmac) (err error) {
|
||||||
if sequence == 1 {
|
if sequence == InitialWalletSequence {
|
||||||
// If sequence == 1, the client assumed that this is our first
|
// If sequence == InitialWalletSequence, the client assumed that this is our first
|
||||||
// wallet. Try to insert. If we get a conflict, the client
|
// wallet. Try to insert. If we get a conflict, the client
|
||||||
// assumed incorrectly and we proceed below to return the latest
|
// assumed incorrectly and we proceed below to return the latest
|
||||||
// wallet from the db.
|
// wallet from the db.
|
||||||
err = s.insertFirstWallet(userId, encryptedWallet, hmac)
|
err = s.insertFirstWallet(userId, encryptedWallet, hmac)
|
||||||
if err == ErrDuplicateWallet {
|
if err == ErrDuplicateWallet {
|
||||||
// A wallet already exists. That means the input sequence should not be 1.
|
// A wallet already exists. That means the input sequence should not be InitialWalletSequence.
|
||||||
// To the caller, this means the sequence was wrong.
|
// To the caller, this means the sequence was wrong.
|
||||||
err = ErrWrongSequence
|
err = ErrWrongSequence
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If sequence > 1, the client assumed that it is replacing wallet
|
// If sequence > InitialWalletSequence, the client assumed that it is replacing wallet
|
||||||
// with sequence - 1. Explicitly try to update the wallet with
|
// with sequence - 1. Explicitly try to update the wallet with
|
||||||
// sequence - 1. If we updated no rows, the client assumed incorrectly
|
// sequence - 1. If we updated no rows, the client assumed incorrectly
|
||||||
// and we proceed below to return the latest wallet from the db.
|
// and we proceed below to return the latest wallet from the db.
|
||||||
|
@ -403,7 +411,7 @@ func (s *Store) CreateAccount(email auth.Email, password auth.Password, seed aut
|
||||||
|
|
||||||
// userId auto-increments
|
// userId auto-increments
|
||||||
_, err = s.db.Exec(
|
_, err = s.db.Exec(
|
||||||
"INSERT INTO accounts (normalized_email, email, key, server_salt, client_salt_seed, verify_token, verify_expiration) VALUES(?,?,?,?,?,?,?)",
|
"INSERT INTO accounts (normalized_email, email, key, server_salt, client_salt_seed, verify_token, verify_expiration, updated) VALUES(?,?,?,?,?,?,?, datetime('now'))",
|
||||||
email.Normalize(), email, key, salt, seed, verifyToken, verifyExpiration,
|
email.Normalize(), email, key, salt, seed, verifyToken, verifyExpiration,
|
||||||
)
|
)
|
||||||
var sqliteErr sqlite3.Error
|
var sqliteErr sqlite3.Error
|
||||||
|
@ -426,7 +434,7 @@ func (s *Store) UpdateVerifyTokenString(email auth.Email, verifyTokenString auth
|
||||||
expiration := time.Now().UTC().Add(VerifyTokenLifespan)
|
expiration := time.Now().UTC().Add(VerifyTokenLifespan)
|
||||||
|
|
||||||
res, err := s.db.Exec(
|
res, err := s.db.Exec(
|
||||||
`UPDATE accounts SET verify_token=?, verify_expiration=? WHERE normalized_email=? and verify_token is not null`,
|
`UPDATE accounts SET verify_token=?, verify_expiration=?, updated=datetime('now') WHERE normalized_email=? and verify_token is not null`,
|
||||||
verifyTokenString, expiration, email.Normalize(),
|
verifyTokenString, expiration, email.Normalize(),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -456,9 +464,11 @@ func (s *Store) UpdateVerifyTokenString(email auth.Email, verifyTokenString auth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) VerifyAccount(verifyTokenString auth.VerifyTokenString) (err error) {
|
func (s *Store) VerifyAccount(verifyTokenString auth.VerifyTokenString) (err error) {
|
||||||
|
expirationCutoff := time.Now().UTC()
|
||||||
|
|
||||||
res, err := s.db.Exec(
|
res, err := s.db.Exec(
|
||||||
"UPDATE accounts SET verify_token=null, verify_expiration=null WHERE verify_token=?",
|
"UPDATE accounts SET verify_token=null, verify_expiration=null, updated=datetime('now') WHERE verify_token=? AND verify_expiration>?",
|
||||||
verifyTokenString,
|
verifyTokenString, expirationCutoff,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -482,6 +492,22 @@ func (s *Store) VerifyAccount(verifyTokenString auth.VerifyTokenString) (err err
|
||||||
// Also delete all auth tokens to force clients to update their root password
|
// Also delete all auth tokens to force clients to update their root password
|
||||||
// to get a new token. This prevents other clients from posting a wallet
|
// to get a new token. This prevents other clients from posting a wallet
|
||||||
// encrypted with the old key.
|
// encrypted with the old key.
|
||||||
|
//
|
||||||
|
// Return userId as a pure convenience for the calling request handler.
|
||||||
|
//
|
||||||
|
// TODO - A wallet encrypted with the old key could still save successfully in
|
||||||
|
// a race condition:
|
||||||
|
// 1) get auth token request passes old password check
|
||||||
|
// 2) password change transaction begins and ends
|
||||||
|
// 3) get auth token request saves and returns a new token
|
||||||
|
// 4) post wallet using the auth token that snuck by
|
||||||
|
// One obvious solution would be to integrate everything into one database
|
||||||
|
// transaction. This problem could apply to other requests as well. Not just
|
||||||
|
// database ones: there's a similar potential race condition trying to boot
|
||||||
|
// users from all of their websockets on password change. We should think
|
||||||
|
// about it. Maybe we could have a counter for password changes, similar to
|
||||||
|
// Sequence? And the tokens have that number attached to it. We can check it
|
||||||
|
// as an extra validation of the token.
|
||||||
func (s *Store) ChangePasswordWithWallet(
|
func (s *Store) ChangePasswordWithWallet(
|
||||||
email auth.Email,
|
email auth.Email,
|
||||||
oldPassword auth.Password,
|
oldPassword auth.Password,
|
||||||
|
@ -490,7 +516,7 @@ func (s *Store) ChangePasswordWithWallet(
|
||||||
encryptedWallet wallet.EncryptedWallet,
|
encryptedWallet wallet.EncryptedWallet,
|
||||||
sequence wallet.Sequence,
|
sequence wallet.Sequence,
|
||||||
hmac wallet.WalletHmac,
|
hmac wallet.WalletHmac,
|
||||||
) (err error) {
|
) (userId auth.UserId, err error) {
|
||||||
return s.changePassword(
|
return s.changePassword(
|
||||||
email,
|
email,
|
||||||
oldPassword,
|
oldPassword,
|
||||||
|
@ -508,12 +534,14 @@ func (s *Store) ChangePasswordWithWallet(
|
||||||
// Also delete all auth tokens to force clients to update their root password
|
// Also delete all auth tokens to force clients to update their root password
|
||||||
// to get a new token. This prevents other clients from posting a wallet
|
// to get a new token. This prevents other clients from posting a wallet
|
||||||
// encrypted with the old key.
|
// encrypted with the old key.
|
||||||
|
//
|
||||||
|
// Return userId as a pure convenience for the calling request handler.
|
||||||
func (s *Store) ChangePasswordNoWallet(
|
func (s *Store) ChangePasswordNoWallet(
|
||||||
email auth.Email,
|
email auth.Email,
|
||||||
oldPassword auth.Password,
|
oldPassword auth.Password,
|
||||||
newPassword auth.Password,
|
newPassword auth.Password,
|
||||||
clientSaltSeed auth.ClientSaltSeed,
|
clientSaltSeed auth.ClientSaltSeed,
|
||||||
) (err error) {
|
) (userId auth.UserId, err error) {
|
||||||
return s.changePassword(
|
return s.changePassword(
|
||||||
email,
|
email,
|
||||||
oldPassword,
|
oldPassword,
|
||||||
|
@ -534,8 +562,7 @@ func (s *Store) changePassword(
|
||||||
encryptedWallet wallet.EncryptedWallet,
|
encryptedWallet wallet.EncryptedWallet,
|
||||||
sequence wallet.Sequence,
|
sequence wallet.Sequence,
|
||||||
hmac wallet.WalletHmac,
|
hmac wallet.WalletHmac,
|
||||||
) (err error) {
|
) (userId auth.UserId, err error) {
|
||||||
var userId auth.UserId
|
|
||||||
|
|
||||||
tx, err := s.db.Begin()
|
tx, err := s.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -585,7 +612,7 @@ func (s *Store) changePassword(
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := tx.Exec(
|
res, err := tx.Exec(
|
||||||
"UPDATE accounts SET key=?, server_salt=?, client_salt_seed=? WHERE user_id=?",
|
"UPDATE accounts SET key=?, server_salt=?, client_salt_seed=?, updated=datetime('now') WHERE user_id=?",
|
||||||
newKey, newSalt, clientSaltSeed, userId,
|
newKey, newSalt, clientSaltSeed, userId,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -605,7 +632,7 @@ func (s *Store) changePassword(
|
||||||
// With a wallet expected: update it.
|
// With a wallet expected: update it.
|
||||||
|
|
||||||
res, err = tx.Exec(
|
res, err = tx.Exec(
|
||||||
`UPDATE wallets SET encrypted_wallet=?, sequence=?, hmac=?
|
`UPDATE wallets SET encrypted_wallet=?, sequence=?, hmac=?, updated=datetime('now')
|
||||||
WHERE user_id=? AND sequence=?`,
|
WHERE user_id=? AND sequence=?`,
|
||||||
encryptedWallet, sequence, hmac, userId, sequence-1,
|
encryptedWallet, sequence, hmac, userId, sequence-1,
|
||||||
)
|
)
|
||||||
|
@ -636,8 +663,10 @@ func (s *Store) changePassword(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't care how many I delete here. Might even be zero. No login token while
|
// Don't care how many I delete here. Might even be zero (no login token
|
||||||
// changing password seems plausible.
|
// while changing password seems plausible). The main reason for this is
|
||||||
|
// that we want to prevent any client from saving a subsequent wallet
|
||||||
|
// without changing its password first.
|
||||||
_, err = tx.Exec("DELETE FROM auth_tokens WHERE user_id=?", userId)
|
_, err = tx.Exec("DELETE FROM auth_tokens WHERE user_id=?", userId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StoreTestInit(t *testing.T) (s Store, tmpFile *os.File) {
|
func StoreTestInit(t *testing.T) (s Store, tmpFile *os.File) {
|
||||||
|
@ -51,7 +51,7 @@ func makeTestUser(
|
||||||
seed = auth.ClientSaltSeed("abcd1234abcd1234")
|
seed = auth.ClientSaltSeed("abcd1234abcd1234")
|
||||||
|
|
||||||
rows, err := s.db.Query(
|
rows, err := s.db.Query(
|
||||||
"INSERT INTO accounts (normalized_email, email, key, server_salt, client_salt_seed, verify_token, verify_expiration) values(?,?,?,?,?,?,?) returning user_id",
|
"INSERT INTO accounts (normalized_email, email, key, server_salt, client_salt_seed, verify_token, verify_expiration, updated) values(?,?,?,?,?,?,?, datetime('now')) returning user_id",
|
||||||
normEmail, email, key, salt, seed, verifyToken, verifyExpiration,
|
normEmail, email, key, salt, seed, verifyToken, verifyExpiration,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,11 +3,12 @@ package store
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
"lbryio/lbry-id/auth"
|
"lbryio/wallet-sync-server/auth"
|
||||||
"lbryio/lbry-id/wallet"
|
"lbryio/wallet-sync-server/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
func expectWalletExists(
|
func expectWalletExists(
|
||||||
|
@ -17,9 +18,10 @@ func expectWalletExists(
|
||||||
expectedEncryptedWallet wallet.EncryptedWallet,
|
expectedEncryptedWallet wallet.EncryptedWallet,
|
||||||
expectedSequence wallet.Sequence,
|
expectedSequence wallet.Sequence,
|
||||||
expectedHmac wallet.WalletHmac,
|
expectedHmac wallet.WalletHmac,
|
||||||
|
approxUpdated time.Time,
|
||||||
) {
|
) {
|
||||||
rows, err := s.db.Query(
|
rows, err := s.db.Query(
|
||||||
"SELECT encrypted_wallet, sequence, hmac FROM wallets WHERE user_id=?", userId)
|
"SELECT encrypted_wallet, sequence, hmac, updated FROM wallets WHERE user_id=?", userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error finding wallet for user_id=%d: %+v", userId, err)
|
t.Fatalf("Error finding wallet for user_id=%d: %+v", userId, err)
|
||||||
}
|
}
|
||||||
|
@ -28,6 +30,7 @@ func expectWalletExists(
|
||||||
var encryptedWallet wallet.EncryptedWallet
|
var encryptedWallet wallet.EncryptedWallet
|
||||||
var sequence wallet.Sequence
|
var sequence wallet.Sequence
|
||||||
var hmac wallet.WalletHmac
|
var hmac wallet.WalletHmac
|
||||||
|
var updated time.Time
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
|
||||||
|
@ -35,6 +38,7 @@ func expectWalletExists(
|
||||||
&encryptedWallet,
|
&encryptedWallet,
|
||||||
&sequence,
|
&sequence,
|
||||||
&hmac,
|
&hmac,
|
||||||
|
&updated,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -45,6 +49,15 @@ func expectWalletExists(
|
||||||
t.Fatalf("Unexpected values for wallet: encrypted wallet: %+v sequence: %+v hmac: %+v err: %+v", encryptedWallet, sequence, hmac, err)
|
t.Fatalf("Unexpected values for wallet: encrypted wallet: %+v sequence: %+v hmac: %+v err: %+v", encryptedWallet, sequence, hmac, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expDiff := approxUpdated.Sub(updated)
|
||||||
|
if time.Second*2 < expDiff || expDiff < -time.Second*2 {
|
||||||
|
t.Fatalf(
|
||||||
|
"Updated timestamp not as expected. Want approximately: %s Got: %s",
|
||||||
|
approxUpdated,
|
||||||
|
updated,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return // found a match, we're good
|
return // found a match, we're good
|
||||||
}
|
}
|
||||||
t.Fatalf("Expected wallet for user_id=%d: %+v", userId, err)
|
t.Fatalf("Expected wallet for user_id=%d: %+v", userId, err)
|
||||||
|
@ -97,7 +110,7 @@ func TestStoreInsertWallet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a wallet, have the values we put in with a sequence of 1
|
// Get a wallet, have the values we put in with a sequence of 1
|
||||||
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet"), wallet.Sequence(1), wallet.WalletHmac("my-hmac"))
|
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet"), wallet.Sequence(1), wallet.WalletHmac("my-hmac"), time.Now().UTC())
|
||||||
|
|
||||||
// Put in a first wallet for a second time, have an error for trying
|
// Put in a first wallet for a second time, have an error for trying
|
||||||
if err := s.insertFirstWallet(userId, wallet.EncryptedWallet("my-enc-wallet-2"), wallet.WalletHmac("my-hmac-2")); err != ErrDuplicateWallet {
|
if err := s.insertFirstWallet(userId, wallet.EncryptedWallet("my-enc-wallet-2"), wallet.WalletHmac("my-hmac-2")); err != ErrDuplicateWallet {
|
||||||
|
@ -105,7 +118,7 @@ func TestStoreInsertWallet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the same *first* wallet we successfully put in
|
// Get the same *first* wallet we successfully put in
|
||||||
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet"), wallet.Sequence(1), wallet.WalletHmac("my-hmac"))
|
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet"), wallet.Sequence(1), wallet.WalletHmac("my-hmac"), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test updateWalletToSequence, using insertFirstWallet as a helper
|
// Test updateWalletToSequence, using insertFirstWallet as a helper
|
||||||
|
@ -139,7 +152,7 @@ func TestStoreUpdateWallet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the same wallet we initially *inserted*, since it didn't update
|
// Get the same wallet we initially *inserted*, since it didn't update
|
||||||
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a"))
|
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a"), time.Now().UTC())
|
||||||
|
|
||||||
// Update the wallet successfully, with the right sequence
|
// Update the wallet successfully, with the right sequence
|
||||||
if err := s.updateWalletToSequence(userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(2), wallet.WalletHmac("my-hmac-b")); err != nil {
|
if err := s.updateWalletToSequence(userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(2), wallet.WalletHmac("my-hmac-b")); err != nil {
|
||||||
|
@ -147,7 +160,7 @@ func TestStoreUpdateWallet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a wallet, have the values we put in
|
// Get a wallet, have the values we put in
|
||||||
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(2), wallet.WalletHmac("my-hmac-b"))
|
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(2), wallet.WalletHmac("my-hmac-b"), time.Now().UTC())
|
||||||
|
|
||||||
// Update the wallet again successfully
|
// Update the wallet again successfully
|
||||||
if err := s.updateWalletToSequence(userId, wallet.EncryptedWallet("my-enc-wallet-c"), wallet.Sequence(3), wallet.WalletHmac("my-hmac-c")); err != nil {
|
if err := s.updateWalletToSequence(userId, wallet.EncryptedWallet("my-enc-wallet-c"), wallet.Sequence(3), wallet.WalletHmac("my-hmac-c")); err != nil {
|
||||||
|
@ -155,13 +168,13 @@ func TestStoreUpdateWallet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a wallet, have the values we put in
|
// Get a wallet, have the values we put in
|
||||||
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-c"), wallet.Sequence(3), wallet.WalletHmac("my-hmac-c"))
|
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-c"), wallet.Sequence(3), wallet.WalletHmac("my-hmac-c"), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE - the "behind the scenes" comments give a view of what we're expecting
|
// NOTE - the "behind the scenes" comments give a view of what we're expecting
|
||||||
// to happen, and why we're testing what we are. Sometimes it should insert,
|
// to happen, and why we're testing what we are. Sometimes it should insert,
|
||||||
// sometimes it should update. It depends on whether it's the first wallet
|
// sometimes it should update. It depends on whether it's the first wallet
|
||||||
// submitted, and that's easily determined by sequence=1. However, if we switch
|
// submitted, and that's easily determined by sequence=store.InitialWalletSequence. However, if we switch
|
||||||
// to a database with "upserts" and take advantage of it, what happens behind
|
// to a database with "upserts" and take advantage of it, what happens behind
|
||||||
// the scenes will change a little, so the comments should be updated. Though,
|
// the scenes will change a little, so the comments should be updated. Though,
|
||||||
// we'd probably best test the same cases.
|
// we'd probably best test the same cases.
|
||||||
|
@ -186,33 +199,33 @@ func TestStoreSetWallet(t *testing.T) {
|
||||||
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a")); err != nil {
|
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a")); err != nil {
|
||||||
t.Fatalf("Unexpected error in SetWallet: %+v", err)
|
t.Fatalf("Unexpected error in SetWallet: %+v", err)
|
||||||
}
|
}
|
||||||
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a"))
|
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a"), time.Now().UTC())
|
||||||
|
|
||||||
// Sequence 1 - fails - out of sequence (behind the scenes, tries to insert but there's something there already)
|
// Sequence 1 - fails - out of sequence (behind the scenes, tries to insert but there's something there already)
|
||||||
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-b")); err != ErrWrongSequence {
|
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-b")); err != ErrWrongSequence {
|
||||||
t.Fatalf(`SetWallet err: wanted "%+v", got "%+v"`, ErrWrongSequence, err)
|
t.Fatalf(`SetWallet err: wanted "%+v", got "%+v"`, ErrWrongSequence, err)
|
||||||
}
|
}
|
||||||
// Expect the *first* wallet to still be there
|
// Expect the *first* wallet to still be there
|
||||||
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a"))
|
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a"), time.Now().UTC())
|
||||||
|
|
||||||
// Sequence 3 - fails - out of sequence (behind the scenes: tries via update, which is appropriate here)
|
// Sequence 3 - fails - out of sequence (behind the scenes: tries via update, which is appropriate here)
|
||||||
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(3), wallet.WalletHmac("my-hmac-b")); err != ErrWrongSequence {
|
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(3), wallet.WalletHmac("my-hmac-b")); err != ErrWrongSequence {
|
||||||
t.Fatalf(`SetWallet err: wanted "%+v", got "%+v"`, ErrWrongSequence, err)
|
t.Fatalf(`SetWallet err: wanted "%+v", got "%+v"`, ErrWrongSequence, err)
|
||||||
}
|
}
|
||||||
// Expect the *first* wallet to still be there
|
// Expect the *first* wallet to still be there
|
||||||
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a"))
|
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-a"), wallet.Sequence(1), wallet.WalletHmac("my-hmac-a"), time.Now().UTC())
|
||||||
|
|
||||||
// Sequence 2 - succeeds - (behind the scenes, does an update. Tests successful update-after-insert)
|
// Sequence 2 - succeeds - (behind the scenes, does an update. Tests successful update-after-insert)
|
||||||
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(2), wallet.WalletHmac("my-hmac-b")); err != nil {
|
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(2), wallet.WalletHmac("my-hmac-b")); err != nil {
|
||||||
t.Fatalf("Unexpected error in SetWallet: %+v", err)
|
t.Fatalf("Unexpected error in SetWallet: %+v", err)
|
||||||
}
|
}
|
||||||
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(2), wallet.WalletHmac("my-hmac-b"))
|
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-b"), wallet.Sequence(2), wallet.WalletHmac("my-hmac-b"), time.Now().UTC())
|
||||||
|
|
||||||
// Sequence 3 - succeeds - (behind the scenes, does an update. Tests successful update-after-update. Maybe gratuitous?)
|
// Sequence 3 - succeeds - (behind the scenes, does an update. Tests successful update-after-update. Maybe gratuitous?)
|
||||||
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-c"), wallet.Sequence(3), wallet.WalletHmac("my-hmac-c")); err != nil {
|
if err := s.SetWallet(userId, wallet.EncryptedWallet("my-enc-wallet-c"), wallet.Sequence(3), wallet.WalletHmac("my-hmac-c")); err != nil {
|
||||||
t.Fatalf("Unexpected error in SetWallet: %+v", err)
|
t.Fatalf("Unexpected error in SetWallet: %+v", err)
|
||||||
}
|
}
|
||||||
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-c"), wallet.Sequence(3), wallet.WalletHmac("my-hmac-c"))
|
expectWalletExists(t, &s, userId, wallet.EncryptedWallet("my-enc-wallet-c"), wallet.Sequence(3), wallet.WalletHmac("my-hmac-c"), time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pretty simple, only two cases: wallet is there or it's not.
|
// Pretty simple, only two cases: wallet is there or it's not.
|
||||||
|
|
|
@ -10,9 +10,11 @@ For this example we will be working with a locally running server so that we don
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> from test_client import Client
|
>>> from test_client import Client
|
||||||
>>> c1 = Client("joe2@example.com", "123abc2", 'test_wallet_1', local=True)
|
>>> import time
|
||||||
|
>>> email = "joe-%s@example.com" % int(time.time())
|
||||||
|
>>> c1 = Client("c1", email, "123abc2", 'test_wallet_1', local=True)
|
||||||
Connecting to Wallet API at http://localhost:8090
|
Connecting to Wallet API at http://localhost:8090
|
||||||
>>> c2 = Client("joe2@example.com", "123abc2", 'test_wallet_2', local=True)
|
>>> c2 = Client("c2", email, "123abc2", 'test_wallet_2', local=True)
|
||||||
Connecting to Wallet API at http://localhost:8090
|
Connecting to Wallet API at http://localhost:8090
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -24,7 +26,7 @@ Generating keys...
|
||||||
Done generating keys
|
Done generating keys
|
||||||
Registered
|
Registered
|
||||||
>>> c1.salt_seed
|
>>> c1.salt_seed
|
||||||
'1d52635c14b34f0fefcf86368d4e0b82e3555de9d3c93a6f22cd5500fd120c0d'
|
'8a77dcb8b2854c2fecabbde74a721fde5e326164f2cf1a7f6810d0e1f340d043'
|
||||||
```
|
```
|
||||||
|
|
||||||
Set up the other client. See that it got the same salt seed from the server in the process, which it needs to make sure we have the correct encryption key and login password.
|
Set up the other client. See that it got the same salt seed from the server in the process, which it needs to make sure we have the correct encryption key and login password.
|
||||||
|
@ -34,16 +36,16 @@ Set up the other client. See that it got the same salt seed from the server in t
|
||||||
Generating keys...
|
Generating keys...
|
||||||
Done generating keys
|
Done generating keys
|
||||||
>>> c2.salt_seed
|
>>> c2.salt_seed
|
||||||
'1d52635c14b34f0fefcf86368d4e0b82e3555de9d3c93a6f22cd5500fd120c0d'
|
'8a77dcb8b2854c2fecabbde74a721fde5e326164f2cf1a7f6810d0e1f340d043'
|
||||||
```
|
```
|
||||||
|
|
||||||
Now that the account exists, grab an auth token with both clients.
|
Now that the account exists, grab an auth token with both clients.
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> c1.get_auth_token()
|
>>> c1.get_auth_token()
|
||||||
Got auth token: e52f6e893fe3fa92d677d85f32e77357d68afd313c303a91d3af176ec684aa0d
|
Got auth token: 9cfbed8d587440b899beb0ea534caaff96981d1f212d83d606642a900deddd1c
|
||||||
>>> c2.get_auth_token()
|
>>> c2.get_auth_token()
|
||||||
Got auth token: b9fc2620990447d5f0305ecafc9f75e2a5f928a31bd86806aa8989567cad57d0
|
Got auth token: 99725c84039e323a880e936d14789d8a79b2fc7efdcae08ed4282e05160f5204
|
||||||
```
|
```
|
||||||
|
|
||||||
## Syncing
|
## Syncing
|
||||||
|
@ -55,7 +57,7 @@ Create a new wallet + metadata (we'll wrap it in a struct we'll call `WalletStat
|
||||||
>>> c1.update_remote_wallet()
|
>>> c1.update_remote_wallet()
|
||||||
Successfully updated wallet state on server
|
Successfully updated wallet state on server
|
||||||
Synced walletState:
|
Synced walletState:
|
||||||
WalletState(sequence=1, encrypted_wallet='czo4MTkyOjE2OjE6XBEQgEACPvxgUFW3MGnY9tG5VYh/Hx7iNG6DAX+q4zTbVZM17OQ/5D1+IOjxS7jxOB+dZmtxmo6qwGtizjc4+YBhNk/eKb+uIU8T6HQ4T3m+PiWpedLnBwF4RStPPBp1M2WNFTIZQPKirETPO3GqRQSzveB17A3iESqYTqHnGeE=')
|
WalletState(sequence=1, encrypted_wallet='czo4MTkyOjE2OjE6MArKm6fT20MSDlsRPAxl5gk49wHPwBxjNnouMGsTEi2uQaMwVOyDRETIRLBTPHFHn6Uz5j+9a5o6RfAbChvToaFRpe4FWZGtlSBiRqdnatxnYzwTRK9OxAttPJdO6BJ0tO1pmn5ipSHfkQdbTT/POTSnElsrnfDU+4AtdoO9kNA=')
|
||||||
'Success'
|
'Success'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -67,7 +69,7 @@ Now, call `init_wallet_state` with the other client. Then, we call `get_remote_w
|
||||||
>>> c2.init_wallet_state()
|
>>> c2.init_wallet_state()
|
||||||
>>> c2.get_remote_wallet()
|
>>> c2.get_remote_wallet()
|
||||||
Got latest walletState:
|
Got latest walletState:
|
||||||
WalletState(sequence=1, encrypted_wallet='czo4MTkyOjE2OjE6XBEQgEACPvxgUFW3MGnY9tG5VYh/Hx7iNG6DAX+q4zTbVZM17OQ/5D1+IOjxS7jxOB+dZmtxmo6qwGtizjc4+YBhNk/eKb+uIU8T6HQ4T3m+PiWpedLnBwF4RStPPBp1M2WNFTIZQPKirETPO3GqRQSzveB17A3iESqYTqHnGeE=')
|
WalletState(sequence=1, encrypted_wallet='czo4MTkyOjE2OjE6MArKm6fT20MSDlsRPAxl5gk49wHPwBxjNnouMGsTEi2uQaMwVOyDRETIRLBTPHFHn6Uz5j+9a5o6RfAbChvToaFRpe4FWZGtlSBiRqdnatxnYzwTRK9OxAttPJdO6BJ0tO1pmn5ipSHfkQdbTT/POTSnElsrnfDU+4AtdoO9kNA=')
|
||||||
'Success'
|
'Success'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -79,12 +81,12 @@ Push a new version, GET it with the other client. Even though we haven't edited
|
||||||
>>> c2.update_remote_wallet()
|
>>> c2.update_remote_wallet()
|
||||||
Successfully updated wallet state on server
|
Successfully updated wallet state on server
|
||||||
Synced walletState:
|
Synced walletState:
|
||||||
WalletState(sequence=2, encrypted_wallet='czo4MTkyOjE2OjE6gL9aGNjy4U+6mBQZRzx+GS+/1dhl54+5sBzVtBQz51az7HQ3HFI2PjUL7XkeTcjdsaPEKh3eFTQwly9fNFKJIya5YvmtY8zhxe8FCqCkTITrn2EPwZFYXF6E3Wi1gLaPMpZlb2EXIZ1E7Gbg1Uxcpj+s1CB4ttjIZdnFwUrfAw4=')
|
WalletState(sequence=2, encrypted_wallet='czo4MTkyOjE2OjE6RbDcGPPGipR3f++iY2IV4TseRvEuZ18HX/SWzzGrw0qbAlChXgSRUTvAlCV1sGyKJEHhBIlGfC+KOCKEGPaK9fx7BmhhcHvCDmwIlcpJ3VwMtwTjxTJZE9+Q8YLOXjZM1RZhPPiCDqxUzNVPaJm2F1MLSn3tDtX5Duz15ll998Y=')
|
||||||
'Success'
|
'Success'
|
||||||
>>> c1.get_remote_wallet()
|
>>> c1.get_remote_wallet()
|
||||||
Nothing to merge. Taking remote walletState as latest walletState.
|
Nothing to merge. Taking remote walletState as latest walletState.
|
||||||
Got latest walletState:
|
Got latest walletState:
|
||||||
WalletState(sequence=2, encrypted_wallet='czo4MTkyOjE2OjE6gL9aGNjy4U+6mBQZRzx+GS+/1dhl54+5sBzVtBQz51az7HQ3HFI2PjUL7XkeTcjdsaPEKh3eFTQwly9fNFKJIya5YvmtY8zhxe8FCqCkTITrn2EPwZFYXF6E3Wi1gLaPMpZlb2EXIZ1E7Gbg1Uxcpj+s1CB4ttjIZdnFwUrfAw4=')
|
WalletState(sequence=2, encrypted_wallet='czo4MTkyOjE2OjE6RbDcGPPGipR3f++iY2IV4TseRvEuZ18HX/SWzzGrw0qbAlChXgSRUTvAlCV1sGyKJEHhBIlGfC+KOCKEGPaK9fx7BmhhcHvCDmwIlcpJ3VwMtwTjxTJZE9+Q8YLOXjZM1RZhPPiCDqxUzNVPaJm2F1MLSn3tDtX5Duz15ll998Y=')
|
||||||
'Success'
|
'Success'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -110,12 +112,12 @@ The wallet is synced between the clients. The client with the changed preference
|
||||||
>>> c1.update_remote_wallet()
|
>>> c1.update_remote_wallet()
|
||||||
Successfully updated wallet state on server
|
Successfully updated wallet state on server
|
||||||
Synced walletState:
|
Synced walletState:
|
||||||
WalletState(sequence=3, encrypted_wallet='czo4MTkyOjE2OjE6JwVggGDjqoLy9YUqFXzIltph5bvO46SwJoAbLydlLg1mjfoXksGm9NsWbmYYmBoiXmiIbJPIsj8xfOjO5JlCH+EHSdyjCXizzwClYwgM4UD1+/ltuv1TH7H59cXd6Kztefn4y9IL/97rs+2DxDHM6cb/AdYGohIc3VaCmYBSbYRQFjTbQHaaScW6ntYuXAyE')
|
WalletState(sequence=3, encrypted_wallet='czo4MTkyOjE2OjE6H0sr9zU/SYL2/0abnfmb4y1WqSnRFuylbket0kahuBi42l3RzZunVY5qp7DHFheQ5RNI/KvaEMV6efC9a7EZc/J5nqZOolgdv0dCSPpgwDS0TxUtsCSH6DGZ3htLxqU2r3ZqKX5XCP4f93lTc8loPGvB+e8k6+CYAeXnkS57ske5U6ZYvJtlMMQpYPSVU3xN')
|
||||||
'Success'
|
'Success'
|
||||||
>>> c2.get_remote_wallet()
|
>>> c2.get_remote_wallet()
|
||||||
Nothing to merge. Taking remote walletState as latest walletState.
|
Nothing to merge. Taking remote walletState as latest walletState.
|
||||||
Got latest walletState:
|
Got latest walletState:
|
||||||
WalletState(sequence=3, encrypted_wallet='czo4MTkyOjE2OjE6JwVggGDjqoLy9YUqFXzIltph5bvO46SwJoAbLydlLg1mjfoXksGm9NsWbmYYmBoiXmiIbJPIsj8xfOjO5JlCH+EHSdyjCXizzwClYwgM4UD1+/ltuv1TH7H59cXd6Kztefn4y9IL/97rs+2DxDHM6cb/AdYGohIc3VaCmYBSbYRQFjTbQHaaScW6ntYuXAyE')
|
WalletState(sequence=3, encrypted_wallet='czo4MTkyOjE2OjE6H0sr9zU/SYL2/0abnfmb4y1WqSnRFuylbket0kahuBi42l3RzZunVY5qp7DHFheQ5RNI/KvaEMV6efC9a7EZc/J5nqZOolgdv0dCSPpgwDS0TxUtsCSH6DGZ3htLxqU2r3ZqKX5XCP4f93lTc8loPGvB+e8k6+CYAeXnkS57ske5U6ZYvJtlMMQpYPSVU3xN')
|
||||||
'Success'
|
'Success'
|
||||||
>>> c2.get_preferences()
|
>>> c2.get_preferences()
|
||||||
{'animal': 'cow', 'car': ''}
|
{'animal': 'cow', 'car': ''}
|
||||||
|
@ -142,7 +144,7 @@ One client POSTs its change first.
|
||||||
>>> c1.update_remote_wallet()
|
>>> c1.update_remote_wallet()
|
||||||
Successfully updated wallet state on server
|
Successfully updated wallet state on server
|
||||||
Synced walletState:
|
Synced walletState:
|
||||||
WalletState(sequence=4, encrypted_wallet='czo4MTkyOjE2OjE6xMKOvjQ9RBAWCac5Cj5d30YSI4PaMh3T+99fLdHKJC2RCcwrbhCurNIDBln6QJWCfa3gRp2/sY9k47XwZNsknCTrdIe4c3YJejvL/WCZTzoJ81m9QGbP/05DHQUV5c7z30taIESp4qOFwpSwYMB972gn6ZXOhn1iNDKSCLN3nSLHFnA0arjCAPQof//lJriz')
|
WalletState(sequence=4, encrypted_wallet='czo4MTkyOjE2OjE6W9I5lyVJgKYlT10doJs9NOGHygtBMXyyXVtDI3doUx/eoPwX+qPxHl/Cz3mjDCEWojZgFZqS70ZPBjbRITWQ9iizPWyUG+FtddUVgadW+nCGiSeKHqVmu5n0MihrvNrtKgEka10dmdtj3U4JJsF/0CwlsyzRKLhPgjlJvzn3miW5DOKrNNtJaFmWFJXIJke5')
|
||||||
'Success'
|
'Success'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -154,7 +156,7 @@ Eventually, the client will be responsible (or at least more responsible) for me
|
||||||
>>> c2.get_remote_wallet()
|
>>> c2.get_remote_wallet()
|
||||||
Merging local changes with remote changes to create latest walletState.
|
Merging local changes with remote changes to create latest walletState.
|
||||||
Got latest walletState:
|
Got latest walletState:
|
||||||
WalletState(sequence=4, encrypted_wallet='czo4MTkyOjE2OjE6xMKOvjQ9RBAWCac5Cj5d30YSI4PaMh3T+99fLdHKJC2RCcwrbhCurNIDBln6QJWCfa3gRp2/sY9k47XwZNsknCTrdIe4c3YJejvL/WCZTzoJ81m9QGbP/05DHQUV5c7z30taIESp4qOFwpSwYMB972gn6ZXOhn1iNDKSCLN3nSLHFnA0arjCAPQof//lJriz')
|
WalletState(sequence=4, encrypted_wallet='czo4MTkyOjE2OjE6W9I5lyVJgKYlT10doJs9NOGHygtBMXyyXVtDI3doUx/eoPwX+qPxHl/Cz3mjDCEWojZgFZqS70ZPBjbRITWQ9iizPWyUG+FtddUVgadW+nCGiSeKHqVmu5n0MihrvNrtKgEka10dmdtj3U4JJsF/0CwlsyzRKLhPgjlJvzn3miW5DOKrNNtJaFmWFJXIJke5')
|
||||||
'Success'
|
'Success'
|
||||||
>>> c2.get_preferences()
|
>>> c2.get_preferences()
|
||||||
{'animal': 'horse', 'car': 'Audi'}
|
{'animal': 'horse', 'car': 'Audi'}
|
||||||
|
@ -166,12 +168,12 @@ Finally, the client with the merged wallet pushes it to the server, and the othe
|
||||||
>>> c2.update_remote_wallet()
|
>>> c2.update_remote_wallet()
|
||||||
Successfully updated wallet state on server
|
Successfully updated wallet state on server
|
||||||
Synced walletState:
|
Synced walletState:
|
||||||
WalletState(sequence=5, encrypted_wallet='czo4MTkyOjE2OjE6L+PCpF1qh1ayai/fqnc7kwa2eBJc1n6L6FuaLps8gdZhY9UdaBMc/BckvgUF9OXR7yOvndrFy73+5EzWxpmffBfZGqq42XjtbmHGScEERjuzra8UB2vLn+N2oe5s+e2O+7lJxPKYBD2pX4xKm3HjKqAso+D0MsWHMz9hqRLFekJfv5pVglUVkweW+h8yNxn1')
|
WalletState(sequence=5, encrypted_wallet='czo4MTkyOjE2OjE60UtNFnnuzYfjdEV2VH/QWO6WTqfSlu0KpUxOm2CuEij9erCmNkiuAQCzihsAZPVSFvt3N9UJRGQdeDRwjN9P2yKr89ED/qBhNHSZzEI8dwR7qrPyqPE5vchiw0UclZPeUdrQiAyyCvZSkThiQNEnwvQyeoucxCZ8P3Gi48Vht48MQ8W07zloMXmndiF81h7G')
|
||||||
'Success'
|
'Success'
|
||||||
>>> c1.get_remote_wallet()
|
>>> c1.get_remote_wallet()
|
||||||
Nothing to merge. Taking remote walletState as latest walletState.
|
Nothing to merge. Taking remote walletState as latest walletState.
|
||||||
Got latest walletState:
|
Got latest walletState:
|
||||||
WalletState(sequence=5, encrypted_wallet='czo4MTkyOjE2OjE6L+PCpF1qh1ayai/fqnc7kwa2eBJc1n6L6FuaLps8gdZhY9UdaBMc/BckvgUF9OXR7yOvndrFy73+5EzWxpmffBfZGqq42XjtbmHGScEERjuzra8UB2vLn+N2oe5s+e2O+7lJxPKYBD2pX4xKm3HjKqAso+D0MsWHMz9hqRLFekJfv5pVglUVkweW+h8yNxn1')
|
WalletState(sequence=5, encrypted_wallet='czo4MTkyOjE2OjE60UtNFnnuzYfjdEV2VH/QWO6WTqfSlu0KpUxOm2CuEij9erCmNkiuAQCzihsAZPVSFvt3N9UJRGQdeDRwjN9P2yKr89ED/qBhNHSZzEI8dwR7qrPyqPE5vchiw0UclZPeUdrQiAyyCvZSkThiQNEnwvQyeoucxCZ8P3Gi48Vht48MQ8W07zloMXmndiF81h7G')
|
||||||
'Success'
|
'Success'
|
||||||
>>> c1.get_preferences()
|
>>> c1.get_preferences()
|
||||||
{'animal': 'horse', 'car': 'Audi'}
|
{'animal': 'horse', 'car': 'Audi'}
|
||||||
|
@ -202,7 +204,7 @@ We try to POST both of them to the server. The second one fails because of the c
|
||||||
>>> c2.update_remote_wallet()
|
>>> c2.update_remote_wallet()
|
||||||
Successfully updated wallet state on server
|
Successfully updated wallet state on server
|
||||||
Synced walletState:
|
Synced walletState:
|
||||||
WalletState(sequence=6, encrypted_wallet='czo4MTkyOjE2OjE6HieQoVznUMTBF6x643Mg/AQUZadaikkiuRZsw3IaQsapK56WL3IBGrlemOjSH6uTfBWsWaLDMXEz+X7j5wqchSAt/wle2+I9dKgyDdFhWMOaEd61pT6r+lS8O8AbSKUJ6r5FSDgJRE/vz5l4xP/W9AVrK4l0u9ZqpvsKAet3UlfVV48cOnhwgPqlPoGBQ1xF')
|
WalletState(sequence=6, encrypted_wallet='czo4MTkyOjE2OjE6e2cIHgNvUiveemLTBYx1BDyueh5JNh4ojvIMnSaass14+Li5eKjVCZaU1LQ1gT4zB6ibqoSu3P60MuOc6/A8GUAwh4KVzQfBBJHjmHWN5ZYoBlJY7AdflrFo0mkUwD1pzTYA0+9iexnI+s0v7ya/rFvw77GtErotgLCnlvOoZiJs3EUyRkltjukz2UCy5LKc')
|
||||||
'Success'
|
'Success'
|
||||||
>>> c1.update_remote_wallet()
|
>>> c1.update_remote_wallet()
|
||||||
Submitted wallet is out of date.
|
Submitted wallet is out of date.
|
||||||
|
@ -218,14 +220,14 @@ The client that is out of date will then call `get_remote_wallet`, which GETs an
|
||||||
>>> c1.get_remote_wallet()
|
>>> c1.get_remote_wallet()
|
||||||
Merging local changes with remote changes to create latest walletState.
|
Merging local changes with remote changes to create latest walletState.
|
||||||
Got latest walletState:
|
Got latest walletState:
|
||||||
WalletState(sequence=6, encrypted_wallet='czo4MTkyOjE2OjE6HieQoVznUMTBF6x643Mg/AQUZadaikkiuRZsw3IaQsapK56WL3IBGrlemOjSH6uTfBWsWaLDMXEz+X7j5wqchSAt/wle2+I9dKgyDdFhWMOaEd61pT6r+lS8O8AbSKUJ6r5FSDgJRE/vz5l4xP/W9AVrK4l0u9ZqpvsKAet3UlfVV48cOnhwgPqlPoGBQ1xF')
|
WalletState(sequence=6, encrypted_wallet='czo4MTkyOjE2OjE6e2cIHgNvUiveemLTBYx1BDyueh5JNh4ojvIMnSaass14+Li5eKjVCZaU1LQ1gT4zB6ibqoSu3P60MuOc6/A8GUAwh4KVzQfBBJHjmHWN5ZYoBlJY7AdflrFo0mkUwD1pzTYA0+9iexnI+s0v7ya/rFvw77GtErotgLCnlvOoZiJs3EUyRkltjukz2UCy5LKc')
|
||||||
'Success'
|
'Success'
|
||||||
>>> c1.get_preferences()
|
>>> c1.get_preferences()
|
||||||
{'animal': 'beaver', 'car': 'Toyota'}
|
{'animal': 'beaver', 'car': 'Toyota'}
|
||||||
>>> c1.update_remote_wallet()
|
>>> c1.update_remote_wallet()
|
||||||
Successfully updated wallet state on server
|
Successfully updated wallet state on server
|
||||||
Synced walletState:
|
Synced walletState:
|
||||||
WalletState(sequence=7, encrypted_wallet='czo4MTkyOjE2OjE6ypuX2e/wjiVZZVrLQEwoEuHZH7xhs6B3/awzxH/5WZITlKOo7TvV2Mjke/MSdTk2/YyWhfN8U0e4IwGxKW9VIpnF3ElEtEZxvJBklzDXeDNh5pWMgeZkBH5EempDQ6VzT0206z89EeiCK+3QSofUv7Ob90xNVUOdJq5/OBrG4LAGFh2ZVrh5KnqDm1+d8/ls')
|
WalletState(sequence=7, encrypted_wallet='czo4MTkyOjE2OjE6mAo3n7J3aNpyiOLHPaHvOypgop4C2Re+fGzMXrdbSLnrMPxVZxlGS3KRzN58jrQ8gkMUqg3aPviXXyII/W8I8MB3lvRoZPRvD1Mkw48sMhPZ5/QsLvNlJT/29ZzqCo4PQAM3PSVo/ho6i7Bilh7Z6pcm7b8u/fuegbsR5NNdTkQ2nbnZl2sDKH6G3idHGc11')
|
||||||
'Success'
|
'Success'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -243,7 +245,7 @@ Generating keys...
|
||||||
Done generating keys
|
Done generating keys
|
||||||
Successfully updated password and wallet state on server
|
Successfully updated password and wallet state on server
|
||||||
Synced walletState:
|
Synced walletState:
|
||||||
WalletState(sequence=8, encrypted_wallet='czo4MTkyOjE2OjE6Kd/DnozNDXYia8yYqrVI6OJ56tDAo5X4/Il+Ein/E6GRQ6K8/niK8Sjx1Cmpf7ecru14QS51pTwlFpS9mbwNE7CZ1wjAZHoLlL5B+dAECkSCFBHgBvq/29cXt6gG7KP+TLRLxZzGtgQRQiq6fsMBIIirw1ZCmpUNQP/PCHIJRfjJS0MNAGN8+srlPv+eUXIn')
|
WalletState(sequence=8, encrypted_wallet='czo4MTkyOjE2OjE6BuNkOY60NKVP4vIjXKfiy4qV75pDzf3YKzBQwU02yhlUR/Jh6ZZTdpEKrKnScwYTVFrSCO+0V7TyPTEVZrh4eLIHJoLgPgDPGl7BNP1aXH4VH+eroqwPQPMLeMJztInWFJt4U2gM+TqExfjG4pNTm5CbD5qiTkv9RMPLBvapcLbD3xeVQkpAYhTi0I458Hsn')
|
||||||
'Success'
|
'Success'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -251,7 +253,7 @@ We generate a new salt seed when we change the password
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> c1.salt_seed
|
>>> c1.salt_seed
|
||||||
'155b6e8a9a8c9406844b6b0c4a40c3204ab1f06668470faa89e28aa89fefe3cf'
|
'e128e66be3c433b30ba40c72d2a42dac8ee84d37a182d4fa2022d4b857d156b0'
|
||||||
```
|
```
|
||||||
|
|
||||||
This operation invalidates all of the user's auth tokens. This prevents other clients from accidentally pushing a wallet encrypted with the old password.
|
This operation invalidates all of the user's auth tokens. This prevents other clients from accidentally pushing a wallet encrypted with the old password.
|
||||||
|
@ -271,19 +273,19 @@ The client that changed its password can easily get a new token because it has t
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> c1.get_auth_token()
|
>>> c1.get_auth_token()
|
||||||
Got auth token: 68a3db244e21709429e69e67352d02a3b26542c5ef2ac3377e19b17de71942d6
|
Got auth token: 10c06893d0f6b5c6506d75c55f6bdea361df1514ad8e9d04b19e7cf6852f6352
|
||||||
>>> c2.get_auth_token()
|
>>> c2.get_auth_token()
|
||||||
Error 401
|
Error 401
|
||||||
b'{"error":"Unauthorized: No match for email and/or password"}\n'
|
b'{"error":"Unauthorized: No match for email and/or password"}\n'
|
||||||
Failed to get the auth token. Do you need to update this client's password (set_local_password())?
|
Failed to get the auth token. Do you need to verify your email address? Or update this client's password (set_local_password())?
|
||||||
Or, in the off-chance the user changed their password back and forth, try updating secrets (update_derived_secrets()) to get the latest salt seed.
|
Or, in the off-chance the user changed their password back and forth, try updating secrets (update_derived_secrets()) to get the latest salt seed.
|
||||||
>>> c2.set_local_password("eggsandwich")
|
>>> c2.set_local_password("eggsandwich")
|
||||||
Generating keys...
|
Generating keys...
|
||||||
Done generating keys
|
Done generating keys
|
||||||
>>> c2.salt_seed
|
>>> c2.salt_seed
|
||||||
'155b6e8a9a8c9406844b6b0c4a40c3204ab1f06668470faa89e28aa89fefe3cf'
|
'e128e66be3c433b30ba40c72d2a42dac8ee84d37a182d4fa2022d4b857d156b0'
|
||||||
>>> c2.get_auth_token()
|
>>> c2.get_auth_token()
|
||||||
Got auth token: 3917215675c5cc7fb5c5e24d583fddcd0a14c4370140e2274cf4c5da7eaae7bb
|
Got auth token: 90689d10cda131747d9dcaddbd474806c16e3d218b54cff4a4ea8fc9d2888e23
|
||||||
```
|
```
|
||||||
|
|
||||||
We don't allow password changes if we have pending wallet changes to push. This is to prevent a situation where the user has to merge local and remote changes in the middle of a password change.
|
We don't allow password changes if we have pending wallet changes to push. This is to prevent a situation where the user has to merge local and remote changes in the middle of a password change.
|
||||||
|
@ -296,16 +298,79 @@ Generating keys...
|
||||||
Done generating keys
|
Done generating keys
|
||||||
Local changes found. Update remote wallet before changing password.
|
Local changes found. Update remote wallet before changing password.
|
||||||
'Failure'
|
'Failure'
|
||||||
|
```
|
||||||
|
|
||||||
|
If we update the wallet first, we can do it.
|
||||||
|
|
||||||
|
```
|
||||||
>>> c1.update_remote_wallet()
|
>>> c1.update_remote_wallet()
|
||||||
Successfully updated wallet state on server
|
Successfully updated wallet state on server
|
||||||
Synced walletState:
|
Synced walletState:
|
||||||
WalletState(sequence=9, encrypted_wallet='czo4MTkyOjE2OjE6Hspn+wbfHEzSv+1zsM/sFUaJJZuLLP7jLtCl3Ou3OQhXGEpkC0pP7WcbdGdQ+4foakTaB/y/b9All85rJ1ZiGWFnaK8SS9Rd7JT1UCEHs0BhN5+SfIK58yukIefzP39ZlSGUomE3eifOqso8C/gY2FltO96TS8WXx6czxqm6M/dvLk6q10LpODCQEH5auTA6')
|
WalletState(sequence=9, encrypted_wallet='czo4MTkyOjE2OjE6JB6y6xDMds4pp4eDh8TDfXvpENnfaQztvwua+ipXE7XSlkOJDjQ7GEv7NtnSPP+Whsny1p6GIWgf22WJ76KGMg16oMTxXWVwK0yTzSiQVhQhc5lNHGMSQDu6wOlHhQAfwta3xwMKbFnklq0GlTRYayeUNspFmFRxKulC5dsdU72V6bPnk3lE5K15JKpNGSQ5')
|
||||||
'Success'
|
'Success'
|
||||||
>>> c1.change_password("starboard")
|
>>> c1.change_password("starboard")
|
||||||
Generating keys...
|
Generating keys...
|
||||||
Done generating keys
|
Done generating keys
|
||||||
Successfully updated password and wallet state on server
|
Successfully updated password and wallet state on server
|
||||||
Synced walletState:
|
Synced walletState:
|
||||||
WalletState(sequence=10, encrypted_wallet='czo4MTkyOjE2OjE6Cnditb9t+rU56hfcMq6gW+lx1ek3TzyBZ4633FoiWCzTxIenbMyapolU0gnpWHasP8olOoL56LfSGVzP8eKG4JoRsU9VmOYXjkpY9QZCcKomVC4fJ17jPq/e2gJWDSv03pA1xbDhRpXRnZr3wd+37znTUyLpYzRDRAHpb2IGDi9FforobQRNcZUhx0DY8WIR')
|
WalletState(sequence=10, encrypted_wallet='czo4MTkyOjE2OjE64YLYwQx95KgCS+kqWhVm3pHBV18bFANe+Kb3Fi3+m9k73p78l/4RF82pxxsYgJeZPrAcpV3pz21dQPVgBDuWszDqk8LkvSQXK/OAmrL3gPFmvuXEV4E504UPcxWeBqiFHNxV1cbWs7UJf31ofVJTEDwqhd0jVitJr/U8+0YU1enEnQd6UavKBxhiaIcy4+xd')
|
||||||
|
'Success'
|
||||||
|
>>> c1.get_auth_token()
|
||||||
|
Got auth token: ab5820fa0f92ca11698b88ca11c7a38e778e4760b85e2fdaedd761561d87951e
|
||||||
|
>>> c2.set_local_password("starboard")
|
||||||
|
Generating keys...
|
||||||
|
Done generating keys
|
||||||
|
>>> c2.get_auth_token()
|
||||||
|
Got auth token: 4c32f3348d51662519b7d8675b69571a1ce4d76de88a7b495a4848c5dbb4da7f
|
||||||
|
```
|
||||||
|
|
||||||
|
# Websockets
|
||||||
|
|
||||||
|
A client can make a websocket connection to the server and receive notifications whenever another client updates the wallet on the server. The message will contain the sequence number so that the client can know whether they happen to be up to date. (The client that made the update will of course be up to date).
|
||||||
|
|
||||||
|
This test client will have a thread listening to the websocket which just prints info about new messages as they come in. A real client would likely choose to get the latest wallet from the server as soon as a messag ecomes through, assuming the sequence is newer than what the client has.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> c1.start_websocket()
|
||||||
|
c1 connected for now
|
||||||
|
>>> c2.start_websocket()
|
||||||
|
c2 connected for now
|
||||||
|
```
|
||||||
|
|
||||||
|
Now make an update and see:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> c1.update_remote_wallet()
|
||||||
|
c2 got notified of a wallet update, sequence=11. If your client is behind this sequence, you should get the latest from the server.
|
||||||
|
Successfully updated wallet state on server
|
||||||
|
Synced walletState:
|
||||||
|
WalletState(sequence=11, encrypted_wallet='czo4MTkyOjE2OjE6h5sx/erPDmRUd7lUwhW9xDkWHmWOvooebx99Je6WMvG+XXd98MjOuBYTIFHbtr0XzR2ARzvNCmnvxSUBGfRoCiF9Um6OqimJTuc636E2pCgLzvEq8W39qYb3enlu6zd+NiGUXEo85j9WY1FvxrSPBxvV21cM4HLWijDGBob/adYTx33sT8o6tZ/18axxNdNK')
|
||||||
|
'Success'
|
||||||
|
c1 got notified of a wallet update, sequence=11. If your client is behind this sequence, you should get the latest from the server.
|
||||||
|
```
|
||||||
|
|
||||||
|
Update again and we'll see the new sequence number:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> c1.update_remote_wallet()
|
||||||
|
c2 got notified of a wallet update, sequence=12. If your client is behind this sequence, you should get the latest from the server.
|
||||||
|
Successfully updated wallet state on server
|
||||||
|
Synced walletState:
|
||||||
|
WalletState(sequence=12, encrypted_wallet='czo4MTkyOjE2OjE6WvomdZhbeg1ypEkHVR07Z4yI2uj1thLkzolgIcvPfE2i+vexeTfG6WE+bsVlhMwn7wXzQ3tIeW7P8IGpAS+HtcySxXyAyZXJxX/2N6fzqd9Dai8pNO1Ed2RyxGGjMf4spMDT3wJOQ93Plsc96y36iPIzNXQ8gtL15IjAfbiImR+KPmIG8E0IDyYVuOoaTF+A')
|
||||||
|
'Success'
|
||||||
|
c1 got notified of a wallet update, sequence=12. If your client is behind this sequence, you should get the latest from the server.
|
||||||
|
```
|
||||||
|
|
||||||
|
When we change a password, just as all auth tokens are invalidated, all sockets are also disconnected.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> c1.change_password("ihatesockets")
|
||||||
|
Generating keys...
|
||||||
|
Done generating keys
|
||||||
|
c2 disconnected for now: code = 1005 (no status code [internal]), no reason
|
||||||
|
c1 disconnected for now: code = 1005 (no status code [internal]), no reason
|
||||||
|
Successfully updated password and wallet state on server
|
||||||
|
Synced walletState:
|
||||||
|
WalletState(sequence=13, encrypted_wallet='czo4MTkyOjE2OjE6GGAs7vW0bt+fzo0xNwQQLd0nY9CM9r1flMtwXNNktXuEA0TkaWVSpP1r/pyN3qLkY8it+OAD2dm44usxzEtNnq1hH40vDPgAYpMtSXRVV1aPTZK6Zv2W1MQN88N4qI592tqpGEm8q2FtqrvA61Rf/CIUGbVKmf5jiPx9aiON6t3etENkZD9t+DEMJo6TOlO9')
|
||||||
'Success'
|
'Success'
|
||||||
```
|
```
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
# Generate the README since I want real behavior interspersed with comments
|
# Generate the README since I want real behavior interspersed with comments
|
||||||
# Come to think of it, this is accidentally a pretty okay integration test for client and server
|
# Come to think of it, this is accidentally a pretty okay integration test for client and server
|
||||||
# NOTE - delete the database before running this, or else you'll get an error for registering. also we want the wallet to start empty
|
|
||||||
# NOTE - in the SDK, create wallets called "test_wallet_1" and "test_wallet_2"
|
# NOTE - in the SDK, create wallets called "test_wallet_1" and "test_wallet_2"
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
@ -15,7 +14,7 @@ for wallet in ['test_wallet_1', 'test_wallet_2']:
|
||||||
# Make sure the next preference changes have a later timestamp!
|
# Make sure the next preference changes have a later timestamp!
|
||||||
time.sleep(1.1)
|
time.sleep(1.1)
|
||||||
|
|
||||||
def code_block(code):
|
def code_block(code, stall=0):
|
||||||
print ("```")
|
print ("```")
|
||||||
for line in code.strip().split('\n'):
|
for line in code.strip().split('\n'):
|
||||||
print(">>> " + line)
|
print(">>> " + line)
|
||||||
|
@ -25,9 +24,7 @@ def code_block(code):
|
||||||
result = eval(line)
|
result = eval(line)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
print(repr(result))
|
print(repr(result))
|
||||||
if 'set_preference' in line:
|
time.sleep(stall) # Some commands we want to give some async aspect to finish before continuing
|
||||||
# Make sure the next preference changes have a later timestamp!
|
|
||||||
time.sleep(1.1)
|
|
||||||
print ("```")
|
print ("```")
|
||||||
|
|
||||||
print("""# Test Client
|
print("""# Test Client
|
||||||
|
@ -44,8 +41,10 @@ For this example we will be working with a locally running server so that we don
|
||||||
|
|
||||||
code_block("""
|
code_block("""
|
||||||
from test_client import Client
|
from test_client import Client
|
||||||
c1 = Client("joe2@example.com", "123abc2", 'test_wallet_1', local=True)
|
import time
|
||||||
c2 = Client("joe2@example.com", "123abc2", 'test_wallet_2', local=True)
|
email = "joe-%s@example.com" % int(time.time())
|
||||||
|
c1 = Client("c1", email, "123abc2", 'test_wallet_1', local=True)
|
||||||
|
c2 = Client("c2", email, "123abc2", 'test_wallet_2', local=True)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
print("""
|
print("""
|
||||||
|
@ -120,7 +119,9 @@ c1.get_preferences()
|
||||||
c2.get_preferences()
|
c2.get_preferences()
|
||||||
c1.set_preference('animal', 'cow')
|
c1.set_preference('animal', 'cow')
|
||||||
c1.get_preferences()
|
c1.get_preferences()
|
||||||
""")
|
""",
|
||||||
|
stall=1.1, # Make sure the next preference changes have a later timestamp!
|
||||||
|
)
|
||||||
|
|
||||||
print("""
|
print("""
|
||||||
The wallet is synced between the clients. The client with the changed preference sends its wallet to the server, and the other one GETs it locally.
|
The wallet is synced between the clients. The client with the changed preference sends its wallet to the server, and the other one GETs it locally.
|
||||||
|
@ -143,7 +144,9 @@ c1.set_preference('car', 'Audi')
|
||||||
c2.set_preference('animal', 'horse')
|
c2.set_preference('animal', 'horse')
|
||||||
c1.get_preferences()
|
c1.get_preferences()
|
||||||
c2.get_preferences()
|
c2.get_preferences()
|
||||||
""")
|
""",
|
||||||
|
stall=1.1, # Make sure the next preference changes have a later timestamp!
|
||||||
|
)
|
||||||
|
|
||||||
print("""
|
print("""
|
||||||
One client POSTs its change first.
|
One client POSTs its change first.
|
||||||
|
@ -191,7 +194,9 @@ _ = c2.set_preference('animal', 'beaver')
|
||||||
_ = c1.set_preference('car', 'Toyota')
|
_ = c1.set_preference('car', 'Toyota')
|
||||||
c2.get_preferences()
|
c2.get_preferences()
|
||||||
c1.get_preferences()
|
c1.get_preferences()
|
||||||
""")
|
""",
|
||||||
|
stall=1.1, # Make sure the next preference changes have a later timestamp!
|
||||||
|
)
|
||||||
|
|
||||||
print("""
|
print("""
|
||||||
We try to POST both of them to the server. The second one fails because of the conflict, and we see that its preferences don't change yet.
|
We try to POST both of them to the server. The second one fails because of the conflict, and we see that its preferences don't change yet.
|
||||||
|
@ -263,6 +268,63 @@ We don't allow password changes if we have pending wallet changes to push. This
|
||||||
code_block("""
|
code_block("""
|
||||||
c1.set_preference('animal', 'leemur')
|
c1.set_preference('animal', 'leemur')
|
||||||
c1.change_password("starboard")
|
c1.change_password("starboard")
|
||||||
|
""",
|
||||||
|
stall=1.1, # Make sure the next preference changes have a later timestamp!
|
||||||
|
)
|
||||||
|
|
||||||
|
print("""
|
||||||
|
If we update the wallet first, we can do it.
|
||||||
|
""")
|
||||||
|
|
||||||
|
code_block("""
|
||||||
c1.update_remote_wallet()
|
c1.update_remote_wallet()
|
||||||
c1.change_password("starboard")
|
c1.change_password("starboard")
|
||||||
|
c1.get_auth_token()
|
||||||
|
c2.set_local_password("starboard")
|
||||||
|
c2.get_auth_token()
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
print("""
|
||||||
|
# Websockets
|
||||||
|
|
||||||
|
A client can make a websocket connection to the server and receive notifications whenever another client updates the wallet on the server. The message will contain the sequence number so that the client can know whether they happen to be up to date. (The client that made the update will of course be up to date).
|
||||||
|
|
||||||
|
This test client will have a thread listening to the websocket which just prints info about new messages as they come in. A real client would likely choose to get the latest wallet from the server as soon as a messag ecomes through, assuming the sequence is newer than what the client has.
|
||||||
|
""")
|
||||||
|
|
||||||
|
code_block("""
|
||||||
|
c1.start_websocket()
|
||||||
|
c2.start_websocket()
|
||||||
|
""",
|
||||||
|
stall=0.1, # Make sure the messages in the other thread have time to print
|
||||||
|
)
|
||||||
|
|
||||||
|
print("""
|
||||||
|
Now make an update and see:
|
||||||
|
""")
|
||||||
|
|
||||||
|
code_block("""
|
||||||
|
c1.update_remote_wallet()
|
||||||
|
""",
|
||||||
|
stall=0.1, # Make sure the messages in the other thread have time to print
|
||||||
|
)
|
||||||
|
|
||||||
|
print("""
|
||||||
|
Update again and we'll see the new sequence number:
|
||||||
|
""")
|
||||||
|
|
||||||
|
code_block("""
|
||||||
|
c1.update_remote_wallet()
|
||||||
|
""",
|
||||||
|
stall=0.1, # Make sure the messages in the other thread have time to print
|
||||||
|
)
|
||||||
|
|
||||||
|
print("""
|
||||||
|
When we change a password, just as all auth tokens are invalidated, all sockets are also disconnected.
|
||||||
|
""")
|
||||||
|
|
||||||
|
code_block("""
|
||||||
|
c1.change_password("ihatesockets")
|
||||||
|
""",
|
||||||
|
stall=0.1, # Make sure the messages in the other thread have time to print
|
||||||
|
)
|
||||||
|
|
|
@ -4,11 +4,14 @@ import base64, json, uuid, requests, hashlib, hmac
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from hashlib import scrypt, sha256 # TODO - audit! Should I use hazmat `Scrypt` instead for some reason?
|
from hashlib import scrypt, sha256 # TODO - audit! Should I use hazmat `Scrypt` instead for some reason?
|
||||||
import secrets
|
import secrets
|
||||||
|
import threading
|
||||||
|
|
||||||
WalletState = namedtuple('WalletState', ['sequence', 'encrypted_wallet'])
|
WalletState = namedtuple('WalletState', ['sequence', 'encrypted_wallet'])
|
||||||
|
|
||||||
|
import asyncio, time
|
||||||
|
from websockets import connect as websockets_connect
|
||||||
|
|
||||||
class LBRYSDK():
|
class LBRYSDK():
|
||||||
# TODO - error checking
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_wallet(wallet_id, password):
|
def get_wallet(wallet_id, password):
|
||||||
response = requests.post('http://localhost:5279', json.dumps({
|
response = requests.post('http://localhost:5279', json.dumps({
|
||||||
|
@ -30,7 +33,6 @@ class LBRYSDK():
|
||||||
}))
|
}))
|
||||||
return response.json()['result']
|
return response.json()['result']
|
||||||
|
|
||||||
# TODO - error checking
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_wallet(wallet_id, password, data):
|
def update_wallet(wallet_id, password, data):
|
||||||
response = requests.post('http://localhost:5279', json.dumps({
|
response = requests.post('http://localhost:5279', json.dumps({
|
||||||
|
@ -43,7 +45,6 @@ class LBRYSDK():
|
||||||
}))
|
}))
|
||||||
return response.json()['result']['data']
|
return response.json()['result']['data']
|
||||||
|
|
||||||
# TODO - error checking
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_preference(wallet_id, key, value):
|
def set_preference(wallet_id, key, value):
|
||||||
response = requests.post('http://localhost:5279', json.dumps({
|
response = requests.post('http://localhost:5279', json.dumps({
|
||||||
|
@ -56,7 +57,6 @@ class LBRYSDK():
|
||||||
}))
|
}))
|
||||||
return response.json()['result']
|
return response.json()['result']
|
||||||
|
|
||||||
# TODO - error checking
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_preferences(wallet_id):
|
def get_preferences(wallet_id):
|
||||||
response = requests.post('http://localhost:5279', json.dumps({
|
response = requests.post('http://localhost:5279', json.dumps({
|
||||||
|
@ -72,20 +72,25 @@ class WalletSync():
|
||||||
self.API_VERSION = 3
|
self.API_VERSION = 3
|
||||||
|
|
||||||
if local:
|
if local:
|
||||||
BASE_URL = 'http://localhost:8090'
|
BASE_HTTP_URL = 'http://localhost:8090'
|
||||||
|
BASE_WS_URL = 'ws://localhost:8090'
|
||||||
else:
|
else:
|
||||||
BASE_URL = 'https://dev.lbry.id'
|
BASE_HTTP_URL = 'https://dev.lbry.id'
|
||||||
|
BASE_WS_URL = 'wss://dev.lbry.id'
|
||||||
|
|
||||||
# Avoid confusion. I sometimes forget, at any rate.
|
# Avoid confusion. I sometimes forget, at any rate.
|
||||||
print ("Connecting to Wallet API at " + BASE_URL)
|
print ("Connecting to Wallet API at " + BASE_HTTP_URL)
|
||||||
|
|
||||||
API_URL = BASE_URL + '/api/%d' % self.API_VERSION
|
API_HTTP_URL = BASE_HTTP_URL + '/api/%d' % self.API_VERSION
|
||||||
|
API_WS_URL = BASE_WS_URL + '/api/%d' % self.API_VERSION
|
||||||
|
|
||||||
self.AUTH_URL = API_URL + '/auth/full'
|
self.AUTH_URL = API_HTTP_URL + '/auth/full'
|
||||||
self.REGISTER_URL = API_URL + '/signup'
|
self.REGISTER_URL = API_HTTP_URL + '/signup'
|
||||||
self.PASSWORD_URL = API_URL + '/password'
|
self.PASSWORD_URL = API_HTTP_URL + '/password'
|
||||||
self.WALLET_URL = API_URL + '/wallet'
|
self.WALLET_URL = API_HTTP_URL + '/wallet'
|
||||||
self.CLIENT_SALT_SEED_URL = API_URL + '/client-salt-seed'
|
self.CLIENT_SALT_SEED_URL = API_HTTP_URL + '/client-salt-seed'
|
||||||
|
|
||||||
|
self.WEBSOCKET_URL = API_WS_URL + '/websocket'
|
||||||
|
|
||||||
# def resend_registration_email():
|
# def resend_registration_email():
|
||||||
# also rename this to __init__.py later
|
# also rename this to __init__.py later
|
||||||
|
@ -119,7 +124,7 @@ class WalletSync():
|
||||||
|
|
||||||
def get_salt_seed(self, email):
|
def get_salt_seed(self, email):
|
||||||
params = {
|
params = {
|
||||||
'email': base64.encodestring(bytes(email.encode('utf-8'))),
|
'email': base64.encodebytes(bytes(email.encode('utf-8'))),
|
||||||
}
|
}
|
||||||
response = requests.get(self.CLIENT_SALT_SEED_URL, params=params)
|
response = requests.get(self.CLIENT_SALT_SEED_URL, params=params)
|
||||||
|
|
||||||
|
@ -223,6 +228,46 @@ class WalletSync():
|
||||||
print (response.content)
|
print (response.content)
|
||||||
raise Exception("Unexpected status code")
|
raise Exception("Unexpected status code")
|
||||||
|
|
||||||
|
# NOTE this doesn't have a way to explicitly disconnect! Hopefully the real
|
||||||
|
# thing is designed better.
|
||||||
|
# NOTE - if you change your password, the server will kick off all existing
|
||||||
|
# websocket connections for that user. each client will need to change their
|
||||||
|
# password to connect again.
|
||||||
|
def start_websocket(self, client_name, token):
|
||||||
|
DEBUG = False
|
||||||
|
# Poor man's debug log
|
||||||
|
debugLog = lambda *x: print(*x) if DEBUG else None
|
||||||
|
self.try_connect_websocket = True
|
||||||
|
async def connection():
|
||||||
|
while self.try_connect_websocket:
|
||||||
|
debugLog (client_name, "trying to connect")
|
||||||
|
try:
|
||||||
|
async with websockets_connect(self.WEBSOCKET_URL + "?token=" + token) as websocket:
|
||||||
|
print (client_name, "connected for now")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
msg = await websocket.recv()
|
||||||
|
# ex: 'wallet-update:5'
|
||||||
|
if msg.startswith('wallet-update'):
|
||||||
|
sequence = int(msg.split(':')[-1])
|
||||||
|
print (client_name, "got notified of a wallet update, sequence=" + str(sequence) + ". If your client is behind this sequence, you should get the latest from the server.")
|
||||||
|
else:
|
||||||
|
debugLog (client_name, "got an unknown message:", msg)
|
||||||
|
except Exception as e:
|
||||||
|
print (client_name, "disconnected for now:", e)
|
||||||
|
time.sleep(1)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
debugLog (client_name, "failed to connect:", e)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
asyncio.run(connection())
|
||||||
|
|
||||||
|
# NOTE - this only stops retrying connections and sending messages. If a
|
||||||
|
# socket is happily connected this won't stop it.
|
||||||
|
def stop_try_reconnect_websocket(self):
|
||||||
|
self.try_connect_websocket = False
|
||||||
|
|
||||||
# Thanks to Standard Notes. See:
|
# Thanks to Standard Notes. See:
|
||||||
# https://docs.standardnotes.com/specification/encryption/
|
# https://docs.standardnotes.com/specification/encryption/
|
||||||
|
|
||||||
|
@ -303,12 +348,13 @@ class Client():
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __init__(self, email, root_password, wallet_id='default_wallet', local=False):
|
def __init__(self, client_name, email, root_password, wallet_id='default_wallet', local=False):
|
||||||
self.wallet_sync_api = WalletSync(local=local)
|
self.wallet_sync_api = WalletSync(local=local)
|
||||||
|
self.client_name = client_name # Just for async output so we know who's talking
|
||||||
|
|
||||||
# Represents normal client behavior (though a real client will of course save device id)
|
# Represents normal client behavior (though a real client will of course save device id)
|
||||||
self.device_id = str(uuid.uuid4())
|
self.device_id = str(uuid.uuid4())
|
||||||
self.auth_token = 'bad token'
|
self.auth_token = 'bad-token'
|
||||||
self.synced_wallet_state = None
|
self.synced_wallet_state = None
|
||||||
|
|
||||||
self.email = email
|
self.email = email
|
||||||
|
@ -316,6 +362,8 @@ class Client():
|
||||||
|
|
||||||
self.wallet_id = wallet_id
|
self.wallet_id = wallet_id
|
||||||
|
|
||||||
|
self.ws_thread = None
|
||||||
|
|
||||||
def register(self):
|
def register(self):
|
||||||
# Note that for each registration, i.e. for each domain, we generate a
|
# Note that for each registration, i.e. for each domain, we generate a
|
||||||
# different salt seed.
|
# different salt seed.
|
||||||
|
@ -398,6 +446,29 @@ class Client():
|
||||||
# TODO - actually set the right hash
|
# TODO - actually set the right hash
|
||||||
self.mark_local_changes_synced_to_empty()
|
self.mark_local_changes_synced_to_empty()
|
||||||
|
|
||||||
|
def start_websocket(self):
|
||||||
|
# NOTE - Not putting any effort into here responsible thread programming
|
||||||
|
# here. Not accounting for errors, logging out and logging into other
|
||||||
|
# servers, etc. Only going so far as to make sure we don't start two at
|
||||||
|
# once.
|
||||||
|
|
||||||
|
if self.ws_thread is None:
|
||||||
|
self.ws_thread = threading.Thread(
|
||||||
|
target=self.wallet_sync_api.start_websocket,
|
||||||
|
args=(self.client_name, self.auth_token),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self.ws_thread.start()
|
||||||
|
else:
|
||||||
|
print("Websocket already connected (or trying to).")
|
||||||
|
|
||||||
|
def stop_try_reconnect_websocket(self):
|
||||||
|
self.wallet_sync_api.stop_try_reconnect_websocket()
|
||||||
|
|
||||||
|
# Not trying to be a responsible thread programmer here, this is just a
|
||||||
|
# demo, and not a threading demo
|
||||||
|
self.ws_thread = None
|
||||||
|
|
||||||
def get_auth_token(self):
|
def get_auth_token(self):
|
||||||
token = self.wallet_sync_api.get_auth_token(
|
token = self.wallet_sync_api.get_auth_token(
|
||||||
self.email,
|
self.email,
|
||||||
|
@ -450,8 +521,6 @@ class Client():
|
||||||
|
|
||||||
# Returns: status
|
# Returns: status
|
||||||
def get_remote_wallet(self):
|
def get_remote_wallet(self):
|
||||||
# TODO - Do try/catch for other calls I guess. I needed it here in
|
|
||||||
# particular for the README
|
|
||||||
try:
|
try:
|
||||||
new_wallet_state, hmac = self.wallet_sync_api.get_wallet(self.auth_token)
|
new_wallet_state, hmac = self.wallet_sync_api.get_wallet(self.auth_token)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -589,11 +658,9 @@ class Client():
|
||||||
return "Failure"
|
return "Failure"
|
||||||
|
|
||||||
def set_preference(self, key, value):
|
def set_preference(self, key, value):
|
||||||
# TODO - error checking
|
|
||||||
return LBRYSDK.set_preference(self.wallet_id, key, value)
|
return LBRYSDK.set_preference(self.wallet_id, key, value)
|
||||||
|
|
||||||
def get_preferences(self):
|
def get_preferences(self):
|
||||||
# TODO - error checking
|
|
||||||
return LBRYSDK.get_preferences(self.wallet_id)
|
return LBRYSDK.get_preferences(self.wallet_id)
|
||||||
|
|
||||||
def has_unsynced_local_changes(self):
|
def has_unsynced_local_changes(self):
|
||||||
|
@ -608,7 +675,6 @@ class Client():
|
||||||
self.lbry_sdk_last_synced_hash = ""
|
self.lbry_sdk_last_synced_hash = ""
|
||||||
|
|
||||||
def update_local_encrypted_wallet(self, encrypted_wallet):
|
def update_local_encrypted_wallet(self, encrypted_wallet):
|
||||||
# TODO - error checking
|
|
||||||
return LBRYSDK.update_wallet(self.wallet_id, self.root_password, encrypted_wallet)
|
return LBRYSDK.update_wallet(self.wallet_id, self.root_password, encrypted_wallet)
|
||||||
|
|
||||||
def get_local_encrypted_wallet(self, sync_password):
|
def get_local_encrypted_wallet(self, sync_password):
|
||||||
|
@ -622,5 +688,4 @@ class Client():
|
||||||
# between the KDFs. The question is, is it safe to use the same root
|
# between the KDFs. The question is, is it safe to use the same root
|
||||||
# password on two two different KDFs like this?
|
# password on two two different KDFs like this?
|
||||||
|
|
||||||
# TODO - error checking
|
|
||||||
return LBRYSDK.get_wallet(self.wallet_id, sync_password)
|
return LBRYSDK.get_wallet(self.wallet_id, sync_password)
|
||||||
|
|
Loading…
Reference in a new issue