From 9d149694915528216fcbee097e2336be5305c0d3 Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Wed, 27 Jul 2022 01:09:57 +0300 Subject: [PATCH 1/4] ignore state backups as well --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3ccc136..c0406ce 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ bin/ *.snap windscribe-proxy -wndstate.json +wndstate.json* From 01d9444e81140de0dce4c92e83a1ca6012bba722 Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Wed, 27 Jul 2022 01:10:06 +0300 Subject: [PATCH 2/4] more error reporting --- main.go | 6 +++--- wndclient/wndclient.go | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index ae9a85b..33ace0c 100644 --- a/main.go +++ b/main.go @@ -372,21 +372,21 @@ func coldInit(wndc *wndclient.WndClient, timeout time.Duration) error { err := wndc.RegisterToken(ctx) cl() if err != nil { - return err + return fmt.Errorf("RegisterToken call failed: %w", err) } ctx, cl = context.WithTimeout(context.Background(), timeout) err = wndc.Users(ctx) cl() if err != nil { - return err + return fmt.Errorf("Users call failed: %w", err) } ctx, cl = context.WithTimeout(context.Background(), timeout) err = wndc.ServerCredentials(ctx) cl() if err != nil { - return err + return fmt.Errorf("ServerCredentials call failed: %w", err) } return nil diff --git a/wndclient/wndclient.go b/wndclient/wndclient.go index 66e680d..fcd7b8d 100644 --- a/wndclient/wndclient.go +++ b/wndclient/wndclient.go @@ -13,6 +13,7 @@ import ( "path" "strconv" "sync" + "time" ) const ( @@ -279,7 +280,14 @@ func (c *WndClient) postJSON(ctx context.Context, endpoint string, input, output } if resp.StatusCode < 200 || resp.StatusCode > 299 { - return fmt.Errorf("bad http status: %s, headers: %#v", resp.Status, resp.Header) + errBodyBytes, _ := ioutil.ReadAll( + &io.LimitedReader{ + R: resp.Body, + N: 1024, + }) + defer resp.Body.Close() + return fmt.Errorf("bad http status: %s, headers: %#v, body: %q", + resp.Status, resp.Header, string(errBodyBytes)) } decoder := json.NewDecoder(resp.Body) @@ -319,7 +327,14 @@ func (c *WndClient) getJSON(ctx context.Context, requestUrl string, output inter } if resp.StatusCode < 200 || resp.StatusCode > 299 { - return fmt.Errorf("bad http status: %s, headers: %#v", resp.Status, resp.Header) + errBodyBytes, _ := ioutil.ReadAll( + &io.LimitedReader{ + R: resp.Body, + N: 1024, + }) + defer resp.Body.Close() + return fmt.Errorf("bad http status: %s, headers: %#v, body: %q", + resp.Status, resp.Header, string(errBodyBytes)) } decoder := json.NewDecoder(resp.Body) From 3febbd6c59a9313e2993e7dfc9b1c552ac1e5712 Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Wed, 27 Jul 2022 02:06:44 +0300 Subject: [PATCH 3/4] add login Signed-off-by: Vladislav Yarmak --- main.go | 19 ++++----- wndclient/messages.go | 18 ++++++++ wndclient/wndclient.go | 97 ++++++++++++++++++++++++------------------ 3 files changed, 81 insertions(+), 53 deletions(-) diff --git a/main.go b/main.go index 33ace0c..9469d82 100644 --- a/main.go +++ b/main.go @@ -58,6 +58,8 @@ type CLIArgs struct { caFile string clientAuthSecret string stateFile string + username string + password string } func parse_args() CLIArgs { @@ -82,6 +84,8 @@ func parse_args() CLIArgs { flag.StringVar(&args.clientAuthSecret, "auth-secret", DEFAULT_CLIENT_AUTH_SECRET, "client auth secret") flag.StringVar(&args.stateFile, "state-file", "wndstate.json", "file name used to persist "+ "Windscribe API client state") + flag.StringVar(&args.username, "username", "", "username for login") + flag.StringVar(&args.password, "password", "", "password for login") flag.Parse() if args.listLocations && args.listProxies { arg_fail("list-locations and list-proxies flags are mutually exclusive") @@ -187,7 +191,7 @@ func run() int { state, err := loadState(args.stateFile) if err != nil { mainLogger.Warning("Failed to load client state: %v. Performing cold init...", err) - err = coldInit(wndc, args.timeout) + err = coldInit(wndc, args.username, args.password, args.timeout) if err != nil { mainLogger.Critical("Cold init failed: %v", err) return 9 @@ -367,19 +371,12 @@ func saveState(filename string, state *wndclient.WndClientState) error { return err } -func coldInit(wndc *wndclient.WndClient, timeout time.Duration) error { +func coldInit(wndc *wndclient.WndClient, username, password string, timeout time.Duration) error { ctx, cl := context.WithTimeout(context.Background(), timeout) - err := wndc.RegisterToken(ctx) + err := wndc.Session(ctx, username, password) cl() if err != nil { - return fmt.Errorf("RegisterToken call failed: %w", err) - } - - ctx, cl = context.WithTimeout(context.Background(), timeout) - err = wndc.Users(ctx) - cl() - if err != nil { - return fmt.Errorf("Users call failed: %w", err) + return fmt.Errorf("Session call failed: %w", err) } ctx, cl = context.WithTimeout(context.Background(), timeout) diff --git a/wndclient/messages.go b/wndclient/messages.go index b81aada..64fbbba 100644 --- a/wndclient/messages.go +++ b/wndclient/messages.go @@ -14,6 +14,24 @@ type RegisterTokenResponse struct { } `json:"data"` } +type SessionResponse struct { + Data *struct { + SessionAuthHash string `json:"session_auth_hash"` + Username string `json:"username"` + UserID string `json:"user_id"` + TrafficUsed float64 `json:"traffic_used"` + TrafficMax float64 `json:"traffic_max"` + Status int `json:"status"` + Email *string `json:"email"` + EmailStatus int `json:"email_status"` + BillingPlanID int64 `json:"billing_plan_id"` + IsPremium int `json:"is_premium"` + RegDate float64 `json:"reg_date"` + LocationRevision int `json:"loc_rev"` + LocationHash string `json:"loc_hash"` + } `json:"data"` +} + type UsersRequest struct { ClientAuthHash string `json:"client_auth_hash"` SessionType int `json:"session_type_id"` diff --git a/wndclient/wndclient.go b/wndclient/wndclient.go index fcd7b8d..ae3374c 100644 --- a/wndclient/wndclient.go +++ b/wndclient/wndclient.go @@ -12,8 +12,8 @@ import ( "net/url" "path" "strconv" + "strings" "sync" - "time" ) const ( @@ -30,16 +30,14 @@ const ( var ErrNoDataInResponse = errors.New("no \"data\" key in response") type WndEndpoints struct { - RegisterToken string `json:"RegisterToken"` - Users string `json:"Users"` + Session string `json:"Session"` ServerList string `json:"serverlist"` ServerCredentials string `json:"ServerCredentials"` BestLocation string `json:"BestLocation"` } var DefaultWndEndpoints = WndEndpoints{ - RegisterToken: "https://api.windscribe.com/RegToken", - Users: "https://api.windscribe.com/Users", + Session: "https://api.windscribe.com/Session", ServerList: "https://assets.windscribe.com/serverlist", ServerCredentials: "https://api.windscribe.com/ServerCredentials", BestLocation: "https://api.windscribe.com/BestLocation", @@ -59,7 +57,7 @@ var DefaultWndSettings = WndSettings{ ClientAuthSecret: "952b4412f002315aa50751032fcaab03", Platform: "chrome", Type: "chrome", - UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36", + UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36", Origin: "chrome-extension://hnmpcagpplmpfojmgmnngilcnanddlhb", SessionType: SESSION_TYPE_EXT, Endpoints: DefaultWndEndpoints, @@ -104,49 +102,22 @@ func NewWndClient(transport http.RoundTripper) (*WndClient, error) { }, nil } -func (c *WndClient) RegisterToken(ctx context.Context) error { +func (c *WndClient) Session(ctx context.Context, username, password string) error { c.Mux.Lock() defer c.Mux.Unlock() clientAuthHash, authTime := MakeAuthHash(c.State.Settings.ClientAuthSecret) - input := RegisterTokenRequest{ - ClientAuthHash: clientAuthHash, - Time: authTime, + input := url.Values{ + "client_auth_hash": []string{clientAuthHash}, + "time": []string{strconv.FormatInt(authTime, 10)}, + "session_type_id": []string{strconv.FormatInt(SESSION_TYPE_EXT, 10)}, + "username": []string{username}, + "password": []string{password}, } - var output RegisterTokenResponse + var output SessionResponse - err := c.postJSON(ctx, c.State.Settings.Endpoints.RegisterToken, input, &output) - if err != nil { - return err - } - if output.Data == nil { - return ErrNoDataInResponse - } - - c.State.TokenID = output.Data.TokenID - c.State.Token = output.Data.Token - c.State.TokenSignature = output.Data.TokenSignature - c.State.TokenSignatureTime = output.Data.TokenTime - - return nil -} - -func (c *WndClient) Users(ctx context.Context) error { - c.Mux.Lock() - defer c.Mux.Unlock() - - clientAuthHash, authTime := MakeAuthHash(c.State.Settings.ClientAuthSecret) - input := UsersRequest{ - ClientAuthHash: clientAuthHash, - Time: authTime, - SessionType: SESSION_TYPE_EXT, - Token: c.State.Token, - } - - var output UsersResponse - - err := c.postJSON(ctx, c.State.Settings.Endpoints.Users, input, &output) + err := c.postForm(ctx, c.State.Settings.Endpoints.Session, input, &output) if err != nil { return err } @@ -301,6 +272,48 @@ func (c *WndClient) postJSON(ctx context.Context, endpoint string, input, output return nil } +func (c *WndClient) postForm(ctx context.Context, endpoint string, input url.Values, output interface{}) error { + req, err := http.NewRequestWithContext( + ctx, + "POST", + endpoint, + strings.NewReader(input.Encode()), + ) + if err != nil { + return err + } + + c.populateRequest(req) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + errBodyBytes, _ := ioutil.ReadAll( + &io.LimitedReader{ + R: resp.Body, + N: 1024, + }) + defer resp.Body.Close() + return fmt.Errorf("bad http status: %s, headers: %#v, body: %q", + resp.Status, resp.Header, string(errBodyBytes)) + } + + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(output) + cleanupBody(resp.Body) + + if err != nil { + return err + } + + return nil +} + func (c *WndClient) GetProxyCredentials() (string, string) { c.Mux.Lock() defer c.Mux.Unlock() From e4995b2a9256820cb7a2d678ea8fcf9af3e2698c Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Wed, 27 Jul 2022 02:21:44 +0300 Subject: [PATCH 4/4] update docs --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6da595a..7b55672 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,12 @@ windscribe-proxy -list-proxies | list-locations | - | list available locations and exit | | list-proxies | - | output proxy list and exit | | location | String | desired proxy location. Default: best location | +| password | String | password for login | | proxy | String | sets base proxy to use for all dial-outs. Format: `://[login:password@]host[:port]` Examples: `http://user:password@192.168.1.1:3128`, `socks5://10.0.0.1:1080` | | resolver | String | Use DNS/DoH/DoT/DoQ resolver for all dial-outs. See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format. Examples: `https://1.1.1.1/dns-query`, `quic://dns.adguard.com` | | state-file | String | file name used to persist Windscribe API client state. Default: `wndstate.json` | | timeout | Duration | timeout for network operations. Default: `10s` | +| username | String | username for login | | verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical). Default: `20` | | version | - | show program version and exit |