mirror of
https://github.com/Snawoot/opera-proxy.git
synced 2025-09-02 02:30:21 +00:00
341 lines
8.3 KiB
Go
341 lines
8.3 KiB
Go
package seclient
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
|
|
dac "github.com/Snawoot/go-http-digest-auth-client"
|
|
)
|
|
|
|
const (
|
|
ANON_EMAIL_LOCALPART_BYTES = 32
|
|
ANON_PASSWORD_BYTES = 20
|
|
DEVICE_ID_BYTES = 20
|
|
READ_LIMIT int64 = 128 * 1024
|
|
)
|
|
|
|
type SEEndpoints struct {
|
|
RegisterSubscriber string
|
|
SubscriberLogin string
|
|
RegisterDevice string
|
|
DeviceGeneratePassword string
|
|
GeoList string
|
|
Discover string
|
|
}
|
|
|
|
var DefaultSEEndpoints = SEEndpoints{
|
|
RegisterSubscriber: "https://api2.sec-tunnel.com/v4/register_subscriber",
|
|
SubscriberLogin: "https://api2.sec-tunnel.com/v4/subscriber_login",
|
|
RegisterDevice: "https://api2.sec-tunnel.com/v4/register_device",
|
|
DeviceGeneratePassword: "https://api2.sec-tunnel.com/v4/device_generate_password",
|
|
GeoList: "https://api2.sec-tunnel.com/v4/geo_list",
|
|
Discover: "https://api2.sec-tunnel.com/v4/discover",
|
|
}
|
|
|
|
type SESettings struct {
|
|
ClientVersion string
|
|
ClientType string
|
|
DeviceName string
|
|
OperatingSystem string
|
|
UserAgent string
|
|
Endpoints SEEndpoints
|
|
}
|
|
|
|
var DefaultSESettings = SESettings{
|
|
ClientVersion: "Stable 114.0.5282.21",
|
|
ClientType: "se0316",
|
|
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0",
|
|
DeviceName: "Opera-Browser-Client",
|
|
OperatingSystem: "Windows",
|
|
Endpoints: DefaultSEEndpoints,
|
|
}
|
|
|
|
type SEClient struct {
|
|
httpClient *http.Client
|
|
Settings SESettings
|
|
SubscriberEmail string
|
|
SubscriberPassword string
|
|
DeviceID string
|
|
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
|
|
func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*SEClient, error) {
|
|
if transport == nil {
|
|
transport = http.DefaultTransport
|
|
}
|
|
|
|
rng := rand.New(RandomSource)
|
|
|
|
device_id, err := randomCapitalHexString(rng, DEVICE_ID_BYTES)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
c.SubscriberEmail = fmt.Sprintf("%s@%s.best.vpn", localPart, c.Settings.ClientType)
|
|
c.SubscriberPassword = capitalHexSHA1(c.SubscriberEmail)
|
|
|
|
return c.register(ctx)
|
|
}
|
|
|
|
func (c *SEClient) Register(ctx context.Context) error {
|
|
c.Mux.Lock()
|
|
defer c.Mux.Unlock()
|
|
return c.register(ctx)
|
|
}
|
|
|
|
func (c *SEClient) register(ctx context.Context) error {
|
|
err := c.resetCookies()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var regRes SERegisterSubscriberResponse
|
|
err = c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{
|
|
"email": c.SubscriberEmail,
|
|
"password": c.SubscriberPassword,
|
|
}, ®Res)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if regRes.Status.Code != SE_STATUS_OK {
|
|
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
|
|
regRes.Status.Code, regRes.Status.Message)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SEClient) RegisterDevice(ctx context.Context) error {
|
|
c.Mux.Lock()
|
|
defer c.Mux.Unlock()
|
|
|
|
var regRes SERegisterDeviceResponse
|
|
err := c.rpcCall(ctx, c.Settings.Endpoints.RegisterDevice, StrKV{
|
|
"client_type": c.Settings.ClientType,
|
|
"device_hash": c.DeviceID,
|
|
"device_name": c.Settings.DeviceName,
|
|
}, ®Res)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if regRes.Status.Code != SE_STATUS_OK {
|
|
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
|
|
regRes.Status.Code, regRes.Status.Message)
|
|
}
|
|
|
|
c.AssignedDeviceID = regRes.Data.DeviceID
|
|
c.DevicePassword = regRes.Data.DevicePassword
|
|
c.AssignedDeviceIDHash = capitalHexSHA1(regRes.Data.DeviceID)
|
|
return nil
|
|
}
|
|
|
|
func (c *SEClient) GeoList(ctx context.Context) ([]SEGeoEntry, error) {
|
|
c.Mux.Lock()
|
|
defer c.Mux.Unlock()
|
|
|
|
var geoListRes SEGeoListResponse
|
|
err := c.rpcCall(ctx, c.Settings.Endpoints.GeoList, StrKV{
|
|
"device_id": c.AssignedDeviceIDHash,
|
|
}, &geoListRes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if geoListRes.Status.Code != SE_STATUS_OK {
|
|
return nil, fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
|
|
geoListRes.Status.Code, geoListRes.Status.Message)
|
|
}
|
|
|
|
return geoListRes.Data.Geos, nil
|
|
}
|
|
|
|
func (c *SEClient) Discover(ctx context.Context, requestedGeo string) ([]SEIPEntry, error) {
|
|
c.Mux.Lock()
|
|
defer c.Mux.Unlock()
|
|
|
|
var discoverRes SEDiscoverResponse
|
|
err := c.rpcCall(ctx, c.Settings.Endpoints.Discover, StrKV{
|
|
"serial_no": c.AssignedDeviceIDHash,
|
|
"requested_geo": requestedGeo,
|
|
}, &discoverRes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if discoverRes.Status.Code != SE_STATUS_OK {
|
|
return nil, fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
|
|
discoverRes.Status.Code, discoverRes.Status.Message)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (c *SEClient) populateRequest(req *http.Request) {
|
|
req.Header["SE-Client-Version"] = []string{c.Settings.ClientVersion}
|
|
req.Header["SE-Operating-System"] = []string{c.Settings.OperatingSystem}
|
|
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) {
|
|
io.Copy(ioutil.Discard, &io.LimitedReader{
|
|
R: body,
|
|
N: READ_LIMIT,
|
|
})
|
|
body.Close()
|
|
}
|