Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ dist

# ignore cosign private key
cosign.key

# ignore test.sh and test files
test.sh
cookies.txt
headers.txt
178 changes: 154 additions & 24 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"time"

"github.com/PuerkitoBio/goquery"
"github.com/thedevsaddam/gojsonq/v2"
Expand All @@ -30,9 +34,19 @@ func NewHttpClient() (*HttpClient, error) {
return &HttpClient{
http.Client{
Jar: jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// Don't follow redirects automatically - we want to handle them manually
return http.ErrUseLastResponse
},
},
http.Header{
"User-Agent": []string{"BL3 Auto SHiFT"},
"User-Agent": []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0"},
"Accept": []string{"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"},
"Accept-Language": []string{"en-US,en;q=0.5"},
"Accept-Encoding": []string{"gzip, deflate, br"},
"DNT": []string{"1"},
"Connection": []string{"keep-alive"},
"Upgrade-Insecure-Requests": []string{"1"},
},
}, nil
}
Expand Down Expand Up @@ -64,9 +78,15 @@ func (response *HttpResponse) BodyAsJson() (*gojsonq.JSONQ, error) {
}

func getResponse(res *http.Response, err error) (*HttpResponse, error) {
if err != nil {
return nil, err
}
if res == nil {
return nil, errors.New("received nil response")
}
return &HttpResponse{
*res,
}, err
}, nil
}

func (client *HttpClient) SetDefaultHeader(k, v string) {
Expand Down Expand Up @@ -126,17 +146,17 @@ func NewBl3Client() (*Bl3Client, error) {
return nil, errors.New("failed to start client")
}

res, err := client.Get("https://raw.githubusercontent.com/jauderho/bl3auto/main/config.json")
// Load config from local file
configBytes, err := os.ReadFile("config.json")
if err != nil {
return nil, errors.New("failed to get config")
return nil, errors.New("failed to read local config.json: " + err.Error())
}

configJson, err := res.BodyAsJson()
config := Bl3Config{}
err = json.Unmarshal(configBytes, &config)
if err != nil {
return nil, errors.New("failed to get config")
return nil, errors.New("failed to parse config.json: " + err.Error())
}
config := Bl3Config{}
configJson.Out(&config)

for header, value := range config.RequestHeaders {
client.SetDefaultHeader(header, value)
Expand All @@ -149,31 +169,141 @@ func NewBl3Client() (*Bl3Client, error) {
}

func (client *Bl3Client) Login(username string, password string) error {
data := map[string]string{
"username": username,
"password": password,
// First, get the login page to extract CSRF token
homeRes, err := client.Get("https://shift.gearboxsoftware.com/home")
if err != nil {
return errors.New("failed to get login page")
}
defer homeRes.Body.Close()

loginRes, err := client.PostJson(client.Config.LoginUrl, data)
// Parse the HTML to extract CSRF token from the hidden form field
doc, err := homeRes.BodyAsHtmlDoc()
if err != nil {
return errors.New("failed to submit login credentials")
return errors.New("failed to parse login page")
}
defer loginRes.Body.Close()

if loginRes.StatusCode != 200 {
return errors.New("failed to login")
// Get the authenticity token from the hidden form input (not the meta tag)
csrfToken, exists := doc.Find("input[name='authenticity_token']").Attr("value")
if !exists {
return errors.New("failed to find authenticity token in form")
}

/* if loginRes.Header.Get(client.Config.LoginRedirectHeader) == "" {
return errors.New("Failed to start session")
// Add a small delay to mimic human behavior
time.Sleep(1 * time.Second)

// Prepare form data using proper URL encoding
formValues := url.Values{}
formValues.Set("utf8", "✓")
formValues.Set("authenticity_token", csrfToken)
formValues.Set("user[email]", username)
formValues.Set("user[password]", password)
formValues.Set("commit", "SIGN IN")

formData := formValues.Encode()

// Create the POST request with proper headers
req, err := http.NewRequest("POST", client.Config.LoginUrl, bytes.NewBufferString(formData))
if err != nil {
return errors.New("failed to create login request: " + err.Error())
}

sessionRes, err := client.Get(loginRes.Header.Get(client.Config.LoginRedirectHeader))

// Set required headers for form submission
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Referer", "https://shift.gearboxsoftware.com/home")

loginRes, err := client.Do(req)
if err != nil {
return errors.New("Failed to get session")
return errors.New("failed to submit login credentials: " + err.Error())
}
defer loginRes.Body.Close()

// Check for successful login (should be a redirect)
if loginRes.StatusCode == 503 {
return errors.New("SHiFT login service is temporarily unavailable (503). This may be due to rate limiting, maintenance, or the service being overloaded. Please try again later.")
}

// Check for successful login - should be 302 redirect
if loginRes.StatusCode == 302 {
location := loginRes.Header.Get("Location")

// Check if this is a failed login redirect (back to home with redirect_to=false)
if bytes.Contains([]byte(location), []byte("home?redirect_to=false")) {
return errors.New("login failed - invalid credentials (redirected back to login page)")
}

// If it's a redirect to somewhere else, it's likely successful
if location != "" {
// Extract session cookie from response
cookies := loginRes.Header.Values("Set-Cookie")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor duplicate session cookie extraction logic into a helper function.

for _, cookie := range cookies {
if len(cookie) >= 12 && cookie[:12] == "_session_id=" {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strings.HasPrefix for cookie prefix check instead of manual slicing.

Suggested change
if len(cookie) >= 12 && cookie[:12] == "_session_id=" {
if strings.HasPrefix(cookie, "_session_id=") {

// Extract just the session cookie part
sessionCookie := cookie
if idx := bytes.IndexByte([]byte(cookie), ';'); idx != -1 {
sessionCookie = cookie[:idx]
}
client.SetDefaultHeader("Cookie", sessionCookie)
return nil
}
}
// Even if no session cookie found, the redirect might indicate success
return nil
}
}

if loginRes.StatusCode != 302 && loginRes.StatusCode != 200 {
return errors.New("unexpected login response status: " + fmt.Sprintf("%d", loginRes.StatusCode))
}

if loginRes.StatusCode != 302 {
// Read the response body to get more details about the error
bodyBytes, _ := io.ReadAll(loginRes.Body)
bodyStr := string(bodyBytes)

// Look for specific error messages in the response
if bytes.Contains(bodyBytes, []byte("Invalid email or password")) ||
bytes.Contains(bodyBytes, []byte("invalid email or password")) ||
bytes.Contains(bodyBytes, []byte("Invalid credentials")) {
return errors.New("invalid email or password - please check your credentials")
}

// Look for other common error patterns
if bytes.Contains(bodyBytes, []byte("alert-danger")) ||
bytes.Contains(bodyBytes, []byte("error-message")) ||
bytes.Contains(bodyBytes, []byte("field_with_errors")) {
return errors.New("login failed - form validation error or invalid credentials")
}

// Check if we're still on the login page (sign in form present)
if bytes.Contains(bodyBytes, []byte("Sign in")) && bytes.Contains(bodyBytes, []byte("user[email]")) {
return errors.New("login failed - still on login page, likely invalid credentials")
}

// If it's a 200 but not a redirect, it might be the login page with errors
if loginRes.StatusCode == 200 {
return errors.New("login failed - credentials may be invalid or additional verification required (status: 200, expected 302 redirect)")
}

maxLen := 200
if len(bodyStr) < maxLen {
maxLen = len(bodyStr)
}
return errors.New("failed to login - server error (status: " + fmt.Sprintf("%d", loginRes.StatusCode) + "). Expected 302 redirect for successful login. Response: " + bodyStr[:maxLen])
}

// Extract session cookie from response
cookies := loginRes.Header.Values("Set-Cookie")
for _, cookie := range cookies {
if len(cookie) >= 12 && cookie[:12] == "_session_id=" {
// Extract just the session cookie part
sessionCookie := cookie
if idx := bytes.IndexByte([]byte(cookie), ';'); idx != -1 {
sessionCookie = cookie[:idx]
}
client.SetDefaultHeader("Cookie", sessionCookie)
return nil
}
}
defer sessionRes.Body.Close()*/

client.SetDefaultHeader(client.Config.SessionHeader, loginRes.Header.Get(client.Config.SessionIdHeader))
return nil
return errors.New("failed to extract session cookie")
}
16 changes: 8 additions & 8 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"version": "2.2.28",
"loginUrl": "https://api.2k.com/borderlands/users/authenticate",
"loginRedirectHeader": "X-CT-REDIRECT",
"sessionIdHeader": "X-SESSION-SET",
"sessionHeader": "X-SESSION",
"loginUrl": "https://shift.gearboxsoftware.com/sessions",
"loginRedirectHeader": "Location",
"sessionIdHeader": "Set-Cookie",
"sessionHeader": "Cookie",
"requestHeaders": {
"Origin": "https://borderlands.com",
"Referer": "https://borderlands.com/en-US/"
"Origin": "https://shift.gearboxsoftware.com",
"Referer": "https://shift.gearboxsoftware.com/home"
},
"shiftConfig": {
"codeListUrl": "https://raw.githubusercontent.com/ugoogalizer/autoshift-codes/main/shiftcodes.json",
"codeInfoUrl": "https://api.2k.com/borderlands/code/",
"userInfoUrl": "https://api.2k.com/borderlands/users/me",
"codeInfoUrl": "https://shift.gearboxsoftware.com/rewards",
"userInfoUrl": "https://shift.gearboxsoftware.com/rewards",
"gameCodename": "oak"
}
}
Loading