Files
windscribe-proxy/wndclient/wndclient.go
Vladislav Yarmak b187a9ad28 2fa support
2022-07-27 10:18:40 +03:00

384 lines
9.4 KiB
Go

package wndclient
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"sync"
)
const (
READ_LIMIT int64 = 128 * 1024
SESSION_TYPE_WEB = 1
SESSION_TYPE_EXT = 2
SESSION_TYPE_DESKTOP = 3
SESSION_TYPE_MOBILE = 4
ACCOUNT_STATE_ACTIVE = 1
ACCOUNT_STATE_OUT_OF_DATA = 2
ACCOUNT_STATE_BANNED = 3
)
var ErrNoDataInResponse = errors.New("no \"data\" key in response")
type WndEndpoints struct {
Session string `json:"Session"`
ServerList string `json:"serverlist"`
ServerCredentials string `json:"ServerCredentials"`
BestLocation string `json:"BestLocation"`
}
var DefaultWndEndpoints = WndEndpoints{
Session: "https://api.windscribe.com/Session",
ServerList: "https://assets.windscribe.com/serverlist",
ServerCredentials: "https://api.windscribe.com/ServerCredentials",
BestLocation: "https://api.windscribe.com/BestLocation",
}
type WndSettings struct {
ClientAuthSecret string `json:"client_auth_secret"`
Platform string `json:"platform"`
Type string `json:"type"`
UserAgent string `json:"user_agent"`
Origin string `json:"origin"`
SessionType int `json:"session_type"`
Endpoints WndEndpoints `json:"endpoints"`
}
var DefaultWndSettings = WndSettings{
ClientAuthSecret: "952b4412f002315aa50751032fcaab03",
Platform: "chrome",
Type: "chrome",
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,
}
type WndClientState struct {
TokenID string `json:"token_id"`
Token string `json:"token"`
TokenSignature string `json:"token_signature"`
TokenSignatureTime int64 `json:"token_signature_time,string"`
LocationHash string `json:"loc_hash"`
LocationRevision int `json:"loc_rev"`
IsPremium bool `json:"is_premium"`
Status int `json:"status"`
UserID string `json:"user_id"`
SessionAuthHash string `json:"session_auth_hash"`
ProxyUsername string `json:"proxy_username"`
ProxyPassword string `json:"proxy_password"`
Settings WndSettings `json:"settings"`
}
type WndClient struct {
httpClient *http.Client
Mux sync.Mutex
State WndClientState
}
type StrKV map[string]string
func NewWndClient(transport http.RoundTripper) (*WndClient, error) {
if transport == nil {
transport = http.DefaultTransport
}
return &WndClient{
httpClient: &http.Client{
Transport: transport,
},
State: WndClientState{
Settings: DefaultWndSettings,
},
}, nil
}
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 := 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 SessionResponse
err := c.postForm(ctx, c.State.Settings.Endpoints.Session, input, &output)
if err != nil {
return err
}
if output.Data == nil {
return ErrNoDataInResponse
}
c.State.UserID = output.Data.UserID
c.State.SessionAuthHash = output.Data.SessionAuthHash
c.State.Status = output.Data.Status
c.State.IsPremium = output.Data.IsPremium != 0
c.State.LocationRevision = output.Data.LocationRevision
c.State.LocationHash = output.Data.LocationHash
return nil
}
func (c *WndClient) ServerCredentials(ctx context.Context) error {
c.Mux.Lock()
defer c.Mux.Unlock()
clientAuthHash, authTime := MakeAuthHash(c.State.Settings.ClientAuthSecret)
requestUrl, err := url.Parse(c.State.Settings.Endpoints.ServerCredentials)
if err != nil {
return err
}
queryValues := requestUrl.Query()
queryValues.Set("client_auth_hash", clientAuthHash)
queryValues.Set("session_auth_hash", c.State.SessionAuthHash)
queryValues.Set("time", strconv.FormatInt(authTime, 10))
requestUrl.RawQuery = queryValues.Encode()
var output ServerCredentialsResponse
err = c.getJSON(ctx, requestUrl.String(), &output)
if err != nil {
return err
}
if output.Data == nil {
return ErrNoDataInResponse
}
c.State.ProxyUsername = string(output.Data.Username)
c.State.ProxyPassword = string(output.Data.Password)
return nil
}
func (c *WndClient) BestLocation(ctx context.Context) (*BestLocation, error) {
c.Mux.Lock()
defer c.Mux.Unlock()
clientAuthHash, authTime := MakeAuthHash(c.State.Settings.ClientAuthSecret)
requestUrl, err := url.Parse(c.State.Settings.Endpoints.BestLocation)
if err != nil {
return nil, err
}
queryValues := requestUrl.Query()
queryValues.Set("client_auth_hash", clientAuthHash)
queryValues.Set("session_auth_hash", c.State.SessionAuthHash)
queryValues.Set("time", strconv.FormatInt(authTime, 10))
requestUrl.RawQuery = queryValues.Encode()
var output BestLocationResponse
err = c.getJSON(ctx, requestUrl.String(), &output)
if err != nil {
return nil, err
}
if output.Data == nil {
return nil, ErrNoDataInResponse
}
return output.Data, nil
}
func (c *WndClient) ServerList(ctx context.Context) (ServerList, error) {
c.Mux.Lock()
defer c.Mux.Unlock()
requestUrl, err := url.Parse(c.State.Settings.Endpoints.ServerList)
if err != nil {
return nil, err
}
isPremium := "0"
if c.State.IsPremium {
isPremium = "1"
}
requestUrl.Path = path.Join(requestUrl.Path, c.State.Settings.Type, isPremium, c.State.LocationHash)
var output ServerListResponse
err = c.getJSON(ctx, requestUrl.String(), &output)
if err != nil {
return nil, err
}
if output.Data == nil {
return nil, ErrNoDataInResponse
}
return output.Data, nil
}
func (c *WndClient) postJSON(ctx context.Context, endpoint string, input, output interface{}) error {
var reqBuf bytes.Buffer
reqEncoder := json.NewEncoder(&reqBuf)
err := reqEncoder.Encode(input)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(
ctx,
"POST",
endpoint,
&reqBuf,
)
if err != nil {
return err
}
c.populateRequest(req)
req.Header.Set("Content-Type", "application/json")
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) 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()
return c.State.ProxyUsername, c.State.ProxyPassword
}
func (c *WndClient) getJSON(ctx context.Context, requestUrl string, output interface{}) error {
req, err := http.NewRequestWithContext(
ctx,
"GET",
requestUrl,
nil,
)
if err != nil {
return err
}
c.populateRequest(req)
req.Header.Set("Accept", "*/*")
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) populateRequest(req *http.Request) {
req.Header.Set("User-Agent", c.State.Settings.UserAgent)
req.Header.Set("Origin", c.State.Settings.Origin)
queryValues := req.URL.Query()
queryValues.Set("platform", c.State.Settings.Platform)
req.URL.RawQuery = queryValues.Encode()
}
// Does cleanup of HTTP response in order to make it reusable by keep-alive
// logic of HTTP client
func cleanupBody(body io.ReadCloser) {
io.Copy(ioutil.Discard, &io.LimitedReader{
R: body,
N: READ_LIMIT,
})
body.Close()
}