Merge pull request #4 from Snawoot/login

Auto-update credentials
This commit is contained in:
Snawoot
2021-04-02 13:38:01 +03:00
committed by GitHub
10 changed files with 305 additions and 147 deletions
+1
View File
@@ -15,3 +15,4 @@
# vendor/
bin/
*.snap
opera-proxy
+1
View File
@@ -96,6 +96,7 @@ eu3.sec-tunnel.com,77.111.244.22,443
| list-countries | - | list available countries and exit |
| list-proxies | - | output proxy list and exit |
| 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` |
| refresh | Duration | login refresh interval (default 4h0m0s) |
| timeout | Duration | timeout for network operations (default 10s) |
| verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
| version | - | show program version and exit |
+1 -1
View File
@@ -4,7 +4,7 @@ go 1.16
require (
github.com/AdguardTeam/dnsproxy v0.36.0
github.com/Snawoot/go-http-digest-auth-client v1.0.0
github.com/Snawoot/go-http-digest-auth-client v1.1.3
github.com/miekg/dns v1.1.35
golang.org/x/net v0.0.0-20210324205630-d1beb07c2056
)
+2 -2
View File
@@ -13,8 +13,8 @@ github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKU
github.com/AdguardTeam/golibs v0.4.4 h1:cM9UySQiYFW79zo5XRwnaIWVzfW4eNXmZktMrWbthpw=
github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Snawoot/go-http-digest-auth-client v1.0.0 h1:1xNR4muFtT+PjP1Z2QAZG0fooszF8nWuYv6QlTKgi1E=
github.com/Snawoot/go-http-digest-auth-client v1.0.0/go.mod h1:WiwNiPXTRGyjTGpBtSQJlM2wDPRRPpFGhMkMWpV4uqg=
github.com/Snawoot/go-http-digest-auth-client v1.1.3 h1:Xd/SNBuIUJqotzmxRpbXovBJxmlVZOT19IZZdMdrJ0Q=
github.com/Snawoot/go-http-digest-auth-client v1.1.3/go.mod h1:WiwNiPXTRGyjTGpBtSQJlM2wDPRRPpFGhMkMWpV4uqg=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
+25 -2
View File
@@ -54,6 +54,7 @@ type CLIArgs struct {
apiPassword string
apiAddress string
bootstrapDNS string
refresh time.Duration
}
func parse_args() CLIArgs {
@@ -76,6 +77,7 @@ func parse_args() CLIArgs {
"DNS/DoH/DoT/DoQ resolver for initial discovering of SurfEasy API address. "+
"See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format. "+
"Examples: https://1.1.1.1/dns-query, quic://dns.adguard.com")
flag.DurationVar(&args.refresh, "refresh", 4*time.Hour, "login refresh interval")
flag.Parse()
if args.country == "" {
arg_fail("Country can't be empty string.")
@@ -227,10 +229,31 @@ func run() int {
return 13
}
runTicker(context.Background(), args.refresh, func(ctx context.Context) {
mainLogger.Info("Refreshing login...")
reqCtx, cl := context.WithTimeout(ctx, args.timeout)
defer cl()
err := seclient.Login(reqCtx)
if err != nil {
mainLogger.Error("Login refresh failed: %v", err)
return
}
mainLogger.Info("Login refreshed.")
mainLogger.Info("Refreshing device password...")
reqCtx, cl = context.WithTimeout(ctx, args.timeout)
defer cl()
err = seclient.DeviceGeneratePassword(reqCtx)
if err != nil {
mainLogger.Error("Device password refresh failed: %v", err)
return
}
mainLogger.Info("Device password refreshed.")
})
endpoint := ips[0]
authHdr := basic_auth_header(seclient.GetProxyCredentials())
auth := func() string {
return authHdr
return basic_auth_header(seclient.GetProxyCredentials())
}
handlerDialer := NewProxyDialer(endpoint.NetAddr(), fmt.Sprintf("%s0.%s", args.country, PROXY_SUFFIX), auth, dialer)
+53
View File
@@ -0,0 +1,53 @@
package seclient
import (
"net/http"
"net/http/cookiejar"
"net/url"
"sync"
"golang.org/x/net/publicsuffix"
)
type StdJar struct {
jar *cookiejar.Jar
mux sync.RWMutex
}
func NewStdJar() (*StdJar, error) {
var jar StdJar
err := jar.Reset()
if err != nil {
return nil, err
}
return &jar, nil
}
func (j *StdJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
j.mux.RLock()
j.jar.SetCookies(u, cookies)
j.mux.RUnlock()
}
func (j *StdJar) Cookies(u *url.URL) []*http.Cookie {
j.mux.RLock()
c := j.jar.Cookies(u)
j.mux.RUnlock()
return c
}
func (j *StdJar) Reset() error {
jar, err := cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})
if err != nil {
return err
}
j.mux.Lock()
j.jar = jar
j.mux.Unlock()
return nil
}
+11
View File
@@ -62,6 +62,15 @@ type SERegisterDeviceResponse struct {
Status SEStatusPair `json:"return_code"`
}
type SEDeviceGeneratePasswordData struct {
DevicePassword string `json:"device_password"`
}
type SEDeviceGeneratePasswordResponse struct {
Data SEDeviceGeneratePasswordData `json:"data"`
Status SEStatusPair `json:"return_code"`
}
type SEGeoEntry struct {
Country string `json:"country,omitempty"`
CountryCode string `json:"country_code"`
@@ -94,3 +103,5 @@ type SEDiscoverResponse struct {
} `json:"data"`
Status SEStatusPair `json:"return_code"`
}
type SESubscriberLoginResponse SERegisterSubscriberResponse
+154 -125
View File
@@ -8,12 +8,11 @@ import (
"io/ioutil"
"math/rand"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"sync"
dac "github.com/Snawoot/go-http-digest-auth-client"
"golang.org/x/net/publicsuffix"
)
const (
@@ -27,6 +26,7 @@ type SEEndpoints struct {
RegisterSubscriber string
SubscriberLogin string
RegisterDevice string
DeviceGeneratePassword string
GeoList string
Discover string
}
@@ -35,6 +35,7 @@ var DefaultSEEndpoints = SEEndpoints{
RegisterSubscriber: "https://api.sec-tunnel.com/v4/register_subscriber",
SubscriberLogin: "https://api.sec-tunnel.com/v4/subscriber_login",
RegisterDevice: "https://api.sec-tunnel.com/v4/register_device",
DeviceGeneratePassword: "https://api.sec-tunnel.com/v4/device_generate_password",
GeoList: "https://api.sec-tunnel.com/v4/geo_list",
Discover: "https://api.sec-tunnel.com/v4/discover",
}
@@ -58,7 +59,7 @@ var DefaultSESettings = SESettings{
}
type SEClient struct {
HttpClient *http.Client
httpClient *http.Client
Settings SESettings
SubscriberEmail string
SubscriberPassword string
@@ -66,9 +67,12 @@ type SEClient struct {
AssignedDeviceID string
AssignedDeviceIDHash string
DevicePassword string
Mux sync.Mutex
rng *rand.Rand
}
type StrKV map[string]string
// Instantiates SurfEasy client with default settings and given API keys.
// Optional `transport` parameter allows to override HTTP transport used
// for HTTP calls
@@ -77,13 +81,6 @@ func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*S
transport = http.DefaultTransport
}
jar, err := cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})
if err != nil {
return nil, err
}
rng := rand.New(RandomSource)
device_id, err := randomCapitalHexString(rng, DEVICE_ID_BYTES)
@@ -91,18 +88,38 @@ func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*S
return nil, err
}
return &SEClient{
HttpClient: &http.Client{
Transport: dac.NewDigestTransport(apiUsername, apiSecret, transport),
jar, err := NewStdJar()
if err != nil {
return nil, err
}
res := &SEClient{
httpClient: &http.Client{
Jar: jar,
Transport: dac.NewDigestTransport(apiUsername, apiSecret, transport),
},
Settings: DefaultSESettings,
rng: rng,
DeviceID: device_id,
}, nil
}
return res, nil
}
func (c *SEClient) ResetCookies() error {
c.Mux.Lock()
defer c.Mux.Unlock()
return c.resetCookies()
}
func (c *SEClient) resetCookies() error {
return (c.httpClient.Jar.(*StdJar)).Reset()
}
func (c *SEClient) AnonRegister(ctx context.Context) error {
c.Mux.Lock()
defer c.Mux.Unlock()
localPart, err := randomEmailLocalPart(c.rng)
if err != nil {
return err
@@ -111,40 +128,26 @@ func (c *SEClient) AnonRegister(ctx context.Context) error {
c.SubscriberEmail = fmt.Sprintf("%s@%s.best.vpn", localPart, c.Settings.ClientType)
c.SubscriberPassword = capitalHexSHA1(c.SubscriberEmail)
return c.Register(ctx)
return c.register(ctx)
}
func (c *SEClient) Register(ctx context.Context) error {
registerInput := url.Values{
"email": {c.SubscriberEmail},
"password": {c.SubscriberPassword},
c.Mux.Lock()
defer c.Mux.Unlock()
return c.register(ctx)
}
req, err := http.NewRequestWithContext(
ctx,
"POST",
c.Settings.Endpoints.RegisterSubscriber,
strings.NewReader(registerInput.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)
func (c *SEClient) register(ctx context.Context) error {
err := c.resetCookies()
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad http status: %s", resp.Status)
}
decoder := json.NewDecoder(resp.Body)
var regRes SERegisterSubscriberResponse
err = decoder.Decode(&regRes)
cleanupBody(resp.Body)
err = c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{
"email": c.SubscriberEmail,
"password": c.SubscriberPassword,
}, &regRes)
if err != nil {
return err
}
@@ -157,38 +160,15 @@ func (c *SEClient) Register(ctx context.Context) error {
}
func (c *SEClient) RegisterDevice(ctx context.Context) error {
registerDeviceInput := url.Values{
"client_type": {c.Settings.ClientType},
"device_hash": {c.DeviceID},
"device_name": {c.Settings.DeviceName},
}
req, err := http.NewRequestWithContext(
ctx,
"POST",
c.Settings.Endpoints.RegisterDevice,
strings.NewReader(registerDeviceInput.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")
c.Mux.Lock()
defer c.Mux.Unlock()
resp, err := c.HttpClient.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad http status: %s", resp.Status)
}
decoder := json.NewDecoder(resp.Body)
var regRes SERegisterDeviceResponse
err = decoder.Decode(&regRes)
cleanupBody(resp.Body)
err := c.rpcCall(ctx, c.Settings.Endpoints.RegisterDevice, StrKV{
"client_type": c.Settings.ClientType,
"device_hash": c.DeviceID,
"device_name": c.Settings.DeviceName,
}, &regRes)
if err != nil {
return err
}
@@ -205,36 +185,13 @@ func (c *SEClient) RegisterDevice(ctx context.Context) error {
}
func (c *SEClient) GeoList(ctx context.Context) ([]SEGeoEntry, error) {
geoListInput := url.Values{
"device_id": {c.AssignedDeviceIDHash},
}
req, err := http.NewRequestWithContext(
ctx,
"POST",
c.Settings.Endpoints.GeoList,
strings.NewReader(geoListInput.Encode()),
)
if err != nil {
return nil, err
}
c.populateRequest(req)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
c.Mux.Lock()
defer c.Mux.Unlock()
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad http status: %s", resp.Status)
}
decoder := json.NewDecoder(resp.Body)
var geoListRes SEGeoListResponse
err = decoder.Decode(&geoListRes)
cleanupBody(resp.Body)
err := c.rpcCall(ctx, c.Settings.Endpoints.GeoList, StrKV{
"device_id": c.AssignedDeviceIDHash,
}, &geoListRes)
if err != nil {
return nil, err
}
@@ -248,37 +205,14 @@ func (c *SEClient) GeoList(ctx context.Context) ([]SEGeoEntry, error) {
}
func (c *SEClient) Discover(ctx context.Context, requestedGeo string) ([]SEIPEntry, error) {
geoListInput := url.Values{
"serial_no": {c.AssignedDeviceIDHash},
"requested_geo": {requestedGeo},
}
req, err := http.NewRequestWithContext(
ctx,
"POST",
c.Settings.Endpoints.Discover,
strings.NewReader(geoListInput.Encode()),
)
if err != nil {
return nil, err
}
c.populateRequest(req)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
c.Mux.Lock()
defer c.Mux.Unlock()
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad http status: %s", resp.Status)
}
decoder := json.NewDecoder(resp.Body)
var discoverRes SEDiscoverResponse
err = decoder.Decode(&discoverRes)
cleanupBody(resp.Body)
err := c.rpcCall(ctx, c.Settings.Endpoints.Discover, StrKV{
"serial_no": c.AssignedDeviceIDHash,
"requested_geo": requestedGeo,
}, &discoverRes)
if err != nil {
return nil, err
}
@@ -291,7 +225,57 @@ func (c *SEClient) Discover(ctx context.Context, requestedGeo string) ([]SEIPEnt
return discoverRes.Data.IPs, nil
}
func (c *SEClient) Login(ctx context.Context) error {
c.Mux.Lock()
defer c.Mux.Unlock()
err := c.resetCookies()
if err != nil {
return err
}
var loginRes SESubscriberLoginResponse
err = c.rpcCall(ctx, c.Settings.Endpoints.SubscriberLogin, StrKV{
"login": c.SubscriberEmail,
"password": c.SubscriberPassword,
"client_type": c.Settings.ClientType,
}, &loginRes)
if err != nil {
return err
}
if loginRes.Status.Code != SE_STATUS_OK {
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
loginRes.Status.Code, loginRes.Status.Message)
}
return nil
}
func (c *SEClient) DeviceGeneratePassword(ctx context.Context) error {
c.Mux.Lock()
defer c.Mux.Unlock()
var genRes SEDeviceGeneratePasswordResponse
err := c.rpcCall(ctx, c.Settings.Endpoints.DeviceGeneratePassword, StrKV{
"device_id": c.AssignedDeviceID,
}, &genRes)
if err != nil {
return err
}
if genRes.Status.Code != SE_STATUS_OK {
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
genRes.Status.Code, genRes.Status.Message)
}
c.DevicePassword = genRes.Data.DevicePassword
return nil
}
func (c *SEClient) GetProxyCredentials() (string, string) {
c.Mux.Lock()
defer c.Mux.Unlock()
return c.AssignedDeviceIDHash, c.DevicePassword
}
@@ -301,6 +285,51 @@ func (c *SEClient) populateRequest(req *http.Request) {
req.Header["User-Agent"] = []string{c.Settings.UserAgent}
}
func (c *SEClient) RpcCall(ctx context.Context, endpoint string, params map[string]string, res interface{}) error {
c.Mux.Lock()
defer c.Mux.Unlock()
return c.rpcCall(ctx, endpoint, params, res)
}
func (c *SEClient) rpcCall(ctx context.Context, endpoint string, params map[string]string, res interface{}) error {
input := make(url.Values)
for k, v := range params {
input[k] = []string{v}
}
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 != http.StatusOK {
return fmt.Errorf("bad http status: %s, headers: %#v", resp.Status, resp.Header)
}
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(res)
cleanupBody(resp.Body)
if err != nil {
return err
}
return nil
}
// Does cleanup of HTTP response in order to make it reusable by keep-alive
// logic of HTTP client
func cleanupBody(body io.ReadCloser) {
+41 -1
View File
@@ -12,7 +12,10 @@ import (
"time"
)
const COPY_BUF = 128 * 1024
const (
COPY_BUF = 128 * 1024
WALLCLOCK_PRECISION = 1 * time.Second
)
func basic_auth_header(login, password string) string {
return "Basic " + base64.StdEncoding.EncodeToString(
@@ -143,3 +146,40 @@ func copyBody(wr io.Writer, body io.Reader) {
}
}
}
func AfterWallClock(d time.Duration) <-chan time.Time {
ch := make(chan time.Time, 1)
deadline := time.Now().Add(d).Truncate(0)
after_ch := time.After(d)
ticker := time.NewTicker(WALLCLOCK_PRECISION)
go func() {
var t time.Time
defer ticker.Stop()
for {
select {
case t = <-after_ch:
ch <- t
return
case t = <-ticker.C:
if t.After(deadline) {
ch <- t
return
}
}
}
}()
return ch
}
func runTicker(ctx context.Context, interval time.Duration, cb func(context.Context)) {
go func() {
for {
select {
case <-ctx.Done():
return
case <-AfterWallClock(interval):
cb(ctx)
}
}
}()
}