8 Commits

Author SHA1 Message Date
Snawoot
b7ac8a196d Merge pull request #10 from Snawoot/2fa
2FA support
2022-07-27 10:24:17 +03:00
Vladislav Yarmak
b187a9ad28 2fa support 2022-07-27 10:18:40 +03:00
Snawoot
ee3ab40244 Merge pull request #9 from Snawoot/fix
Use login
2022-07-27 02:33:18 +03:00
Vladislav Yarmak
e4995b2a92 update docs 2022-07-27 02:21:44 +03:00
Vladislav Yarmak
3febbd6c59 add login
Signed-off-by: Vladislav Yarmak <vladislav-ex-src@vm-0.com>
2022-07-27 02:06:44 +03:00
Vladislav Yarmak
01d9444e81 more error reporting 2022-07-27 01:18:26 +03:00
Vladislav Yarmak
9d14969491 ignore state backups as well 2022-07-27 01:09:57 +03:00
Vladislav Yarmak
130ecb8dcb enable stale bot 2022-07-01 20:10:20 +03:00
6 changed files with 124 additions and 56 deletions

17
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

2
.gitignore vendored
View File

@@ -16,4 +16,4 @@
bin/
*.snap
windscribe-proxy
wndstate.json
wndstate.json*

View File

@@ -64,16 +64,19 @@ windscribe-proxy -list-proxies
| Argument | Type | Description |
| -------- | ---- | ----------- |
| 2fa | String | 2FA code for login |
| auth-secret | String | client auth secret (default `952b4412f002315aa50751032fcaab03`) |
| bind-address | String | HTTP proxy listen address (default `127.0.0.1:28080`) |
| cafile | String | use custom CA certificate bundle file |
| 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: `<http\|https\|socks5\|socks5h>://[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 |

23
main.go
View File

@@ -58,6 +58,9 @@ type CLIArgs struct {
caFile string
clientAuthSecret string
stateFile string
username string
password string
tfacode string
}
func parse_args() CLIArgs {
@@ -82,6 +85,9 @@ 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.StringVar(&args.tfacode, "2fa", "", "2FA code for login")
flag.Parse()
if args.listLocations && args.listProxies {
arg_fail("list-locations and list-proxies flags are mutually exclusive")
@@ -187,7 +193,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.tfacode, args.timeout)
if err != nil {
mainLogger.Critical("Cold init failed: %v", err)
return 9
@@ -367,26 +373,19 @@ 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, tfacode string, timeout time.Duration) error {
ctx, cl := context.WithTimeout(context.Background(), timeout)
err := wndc.RegisterToken(ctx)
err := wndc.Session(ctx, username, password, tfacode)
cl()
if err != nil {
return err
}
ctx, cl = context.WithTimeout(context.Background(), timeout)
err = wndc.Users(ctx)
cl()
if err != nil {
return err
return fmt.Errorf("Session 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

View File

@@ -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"`

View File

@@ -12,6 +12,7 @@ import (
"net/url"
"path"
"strconv"
"strings"
"sync"
)
@@ -29,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",
@@ -58,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,
@@ -103,49 +102,25 @@ 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, tfacode 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},
}
if tfacode != "" {
input["2fa_code"] = []string{tfacode}
}
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
}
@@ -279,7 +254,56 @@ 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)
err = decoder.Decode(output)
cleanupBody(resp.Body)
if err != nil {
return err
}
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)
@@ -319,7 +343,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)