mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-23 00:16:54 +00:00
239 lines
6.9 KiB
Go
239 lines
6.9 KiB
Go
package bitrix
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/sagernet/sing-box/transport/call/common"
|
|
|
|
headless "github.com/kulikov0/headless-client"
|
|
)
|
|
|
|
const authNetBase = "https://auth2.bitrix24.net"
|
|
|
|
var (
|
|
reFlowCfg = regexp.MustCompile(`b24network\.security\.flowtoken"[^>]*>(\{[^<]*\})</script>`)
|
|
reJunk = regexp.MustCompile(`[!#$%@~]`)
|
|
reJSRedirect = regexp.MustCompile(`window\.location\.href\s*=\s*['"]([^'"]+)['"]`)
|
|
)
|
|
|
|
func (c *Client) SetCredentials(email, password string) {
|
|
c.email = strings.TrimSpace(email)
|
|
c.password = password
|
|
}
|
|
|
|
func (c *Client) loginWithPassword(email, password string) error {
|
|
if email == "" || password == "" {
|
|
return fmt.Errorf("login: no email/password")
|
|
}
|
|
if err := c.resetJar(); err != nil {
|
|
return err
|
|
}
|
|
flowToken, sessid, currentURI, err := c.bootstrapLogin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Logger.Debug(fmt.Sprintf("[auth] bootstrap: flowToken=%dB sessid=%v authURL=%v", len(flowToken), sessid != "", strings.Contains(currentURI, "auth2.bitrix24.net")))
|
|
if err := c.authCheckLogin(flowToken, email, sessid, currentURI); err != nil {
|
|
return err
|
|
}
|
|
c.Logger.Debug("[auth] checkLogin OK")
|
|
if err := c.authCheck(flowToken, email, password, sessid); err != nil {
|
|
return err
|
|
}
|
|
c.Logger.Debug("[auth] check OK")
|
|
c.email = email
|
|
c.password = password
|
|
return c.completeOAuth()
|
|
}
|
|
|
|
func (c *Client) bootstrapLogin() (flowToken, sessid, currentURI string, err error) {
|
|
req, err := http.NewRequest("GET", c.portal+"/", nil)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
req.Header = headless.ChromeWindows.Headers(headless.DestDocument)
|
|
if c.userAgent != "" {
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
}
|
|
resp, err := c.HTTP.Do(req)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
page := string(body)
|
|
currentURI = resp.Request.URL.String()
|
|
if !strings.Contains(currentURI, "auth2.bitrix24.net") {
|
|
return "", "", "", fmt.Errorf("portal did not redirect to auth2 (landed on %s)", currentURI)
|
|
}
|
|
flowToken = computeFlowToken(page)
|
|
if m := reSessid.FindStringSubmatch(page); m != nil {
|
|
sessid = m[1]
|
|
}
|
|
return flowToken, sessid, currentURI, nil
|
|
}
|
|
|
|
func computeFlowToken(page string) string {
|
|
m := reFlowCfg.FindStringSubmatch(page)
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
var cfg struct {
|
|
FTV string `json:"ftv"`
|
|
}
|
|
if err := json.Unmarshal([]byte(m[1]), &cfg); err != nil {
|
|
return ""
|
|
}
|
|
return reJunk.ReplaceAllString(cfg.FTV, "")
|
|
}
|
|
|
|
func (c *Client) authCheckLogin(flowToken, login, sessid, currentURI string) error {
|
|
form := url.Values{}
|
|
form.Set("flow-token-unique-id", flowToken)
|
|
form.Set("login", login)
|
|
form.Set("currentUri", currentURI)
|
|
form.Set("checkSocserv", "Y")
|
|
body, status, err := c.authDo("b24network.authorize.checkLogin", form, sessid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var out struct {
|
|
Status string `json:"status"`
|
|
Data []struct {
|
|
HasAccess bool `json:"HAS_ACCESS"`
|
|
Captcha any `json:"CAPTCHA"`
|
|
} `json:"data"`
|
|
Errors []struct {
|
|
Message string `json:"message"`
|
|
} `json:"errors"`
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return fmt.Errorf("checkLogin: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
if out.Status != "success" || len(out.Data) == 0 {
|
|
return fmt.Errorf("login rejected: checkLogin (status %d, errors %+v, body %s)", status, out.Errors, common.BodySnippet(body))
|
|
}
|
|
if out.Data[0].Captcha != nil {
|
|
return fmt.Errorf("login rejected: captcha required at login step")
|
|
}
|
|
if !out.Data[0].HasAccess {
|
|
return fmt.Errorf("login rejected: account has no access")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) authCheck(flowToken, login, password, sessid string) error {
|
|
form := url.Values{}
|
|
form.Set("flow-token-unique-id", flowToken)
|
|
form.Set("login", login)
|
|
form.Set("password", password)
|
|
form.Set("remember", "Y")
|
|
body, status, err := c.authDo("b24network.authorize.check", form, sessid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var out struct {
|
|
Status string `json:"status"`
|
|
Errors []struct {
|
|
Message string `json:"message"`
|
|
Code string `json:"code"`
|
|
} `json:"errors"`
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return fmt.Errorf("check: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
if out.Status != "success" || len(out.Errors) > 0 {
|
|
return fmt.Errorf("login rejected: password check (status %d, errors %+v, body %s)", status, out.Errors, common.BodySnippet(body))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) authDo(action string, form url.Values, sessid string) ([]byte, int, error) {
|
|
endpoint := authNetBase + "/bitrix/services/main/ajax.php?action=" + action
|
|
req, err := http.NewRequest("POST", endpoint, strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
req.Header = headless.ChromeWindows.Headers(headless.DestEmpty)
|
|
if c.userAgent != "" {
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
}
|
|
req.Header.Set("Accept", "application/json, text/plain, */*")
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
|
req.Header.Set("Origin", authNetBase)
|
|
req.Header.Set("Referer", authNetBase+"/authorization/")
|
|
req.Header.Set("Sec-Fetch-Site", secFetchSiteFor(endpoint, authNetBase))
|
|
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
|
if sessid != "" {
|
|
req.Header.Set("X-Bitrix-Csrf-Token", sessid)
|
|
}
|
|
resp, err := c.HTTP.Do(req)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
return body, resp.StatusCode, err
|
|
}
|
|
|
|
func (c *Client) getPage(target string) (body, final string, err error) {
|
|
req, err := http.NewRequest("GET", target, nil)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
req.Header = headless.ChromeWindows.Headers(headless.DestDocument)
|
|
if c.userAgent != "" {
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
}
|
|
resp, err := c.HTTP.Do(req)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
return string(raw), resp.Request.URL.String(), nil
|
|
}
|
|
|
|
func (c *Client) completeOAuth() error {
|
|
target := c.portal + "/"
|
|
for hop := range 8 {
|
|
body, final, err := c.getPage(target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Logger.Debug(fmt.Sprintf("[auth] oauth hop %d: final=%s len=%d", hop, final, len(body)))
|
|
if next := reJSRedirect.FindStringSubmatch(body); next != nil {
|
|
target = next[1]
|
|
continue
|
|
}
|
|
if strings.HasPrefix(final, c.portal) {
|
|
if m := reSessid.FindStringSubmatch(body); m != nil {
|
|
c.sessid = m[1]
|
|
return nil
|
|
}
|
|
}
|
|
target = c.portal + "/"
|
|
body, final, err = c.getPage(target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.HasPrefix(final, c.portal) {
|
|
return fmt.Errorf("oauth handoff landed outside the portal, final=%s", final)
|
|
}
|
|
if m := reSessid.FindStringSubmatch(body); m != nil {
|
|
c.sessid = m[1]
|
|
return nil
|
|
}
|
|
c.sessid = ""
|
|
c.Logger.Debug("[auth] no csrf token on the portal page, it will be taken from the first response")
|
|
return nil
|
|
}
|
|
return fmt.Errorf("oauth handoff did not converge")
|
|
}
|