Feat(Captcha): Initial captcha solver
All checks were successful
ktm-booking-bot/ktm-booking-bot/pipeline/head This commit looks good
All checks were successful
ktm-booking-bot/ktm-booking-bot/pipeline/head This commit looks good
This commit is contained in:
@@ -14,4 +14,6 @@ TEST_DB_SSL=
|
||||
COOKIE_STRING=
|
||||
ALLOW_REGISTRATION=
|
||||
HEADLESS=
|
||||
LOGGER_WEBHOOK_URL=
|
||||
LOGGER_WEBHOOK_URL=
|
||||
FORCE_START_BOOKING=
|
||||
TWOCAPTCHA_API_KEY=
|
||||
3
Jenkinsfile
vendored
3
Jenkinsfile
vendored
@@ -13,6 +13,7 @@ pipeline {
|
||||
COOKIE_STRING = credentials("COOKIE_STRING")
|
||||
ALLOW_REGISTRATION = credentials("ALLOW_REGISTRATION")
|
||||
LOGGER_WEBHOOK_URL = credentials("LOGGER_WEBHOOK_URL")
|
||||
TWOCAPTCHA_API_KEY = credentials("TWOCAPTCHA_API_KEY")
|
||||
}
|
||||
|
||||
stages {
|
||||
@@ -29,6 +30,8 @@ pipeline {
|
||||
sh 'echo COOKIE_STRING=$COOKIE_STRING >> .env'
|
||||
sh 'echo ALLOW_REGISTRATION=$ALLOW_REGISTRATION >> .env'
|
||||
sh 'echo LOGGER_WEBHOOK_URL=$LOGGER_WEBHOOK_URL >> .env'
|
||||
sh 'echo FORCE_START_BOOKING=false >> .env'
|
||||
sh 'echo TWOCAPTCHA_API_KEY=$TWOCAPTCHA_API_KEY >> .env'
|
||||
echo 'Clearing Git directory'
|
||||
sh 'rm -rf ./.git'
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ func main() {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
environment := os.Getenv("ENVIRONMENT")
|
||||
_ = environment
|
||||
|
||||
db := common.InitDB()
|
||||
db.AutoMigrate(&user.User{})
|
||||
@@ -48,9 +49,7 @@ func main() {
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
if environment == "dev" {
|
||||
r.Mount("/docs", httpSwagger.WrapHandler)
|
||||
}
|
||||
r.Mount("/docs", httpSwagger.WrapHandler)
|
||||
|
||||
r.Mount("/api/v1/user", user.UserRoutes(db))
|
||||
r.Mount("/api/v1/ktmtrainbot", ktmtrainbot.KTMTrainBotRoutes(db))
|
||||
|
||||
1
backend/internal/captchasolver/main.go
Normal file
1
backend/internal/captchasolver/main.go
Normal file
@@ -0,0 +1 @@
|
||||
package captchasolver
|
||||
103
backend/internal/captchasolver/provider2captcha.go
Normal file
103
backend/internal/captchasolver/provider2captcha.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package captchasolver
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Provider2CaptchaCreateCaptchaRequestV3(apiKey string, url string, googleKey string, googleAction string) string {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "https://2captcha.com/in.php", nil)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("key", apiKey)
|
||||
q.Add("method", "userrecaptcha")
|
||||
q.Add("version", "3")
|
||||
q.Add("action", googleAction)
|
||||
q.Add("min_score", "0.3")
|
||||
q.Add("googlekey", googleKey)
|
||||
q.Add("pageurl", url)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
respBody, _ := io.ReadAll(res.Body)
|
||||
respBodyStr := string(respBody)
|
||||
|
||||
return respBodyStr[3:]
|
||||
}
|
||||
|
||||
func Provider2CaptchaCreateCaptchaRequestV2(apiKey string, url string, googleKey string) string {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "https://2captcha.com/in.php", nil)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("key", apiKey)
|
||||
q.Add("method", "userrecaptcha")
|
||||
q.Add("googlekey", googleKey)
|
||||
q.Add("pageurl", url)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
respBody, _ := io.ReadAll(res.Body)
|
||||
respBodyStr := string(respBody)
|
||||
|
||||
return respBodyStr[3:]
|
||||
}
|
||||
|
||||
func Provider2CaptchaGetCaptchaResult(apiKey string, captchaID string) string {
|
||||
client := &http.Client{}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://2captcha.com/res.php", nil)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("key", apiKey)
|
||||
q.Add("action", "get")
|
||||
q.Add("id", captchaID)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
respBody, _ := io.ReadAll(res.Body)
|
||||
respBodyStr := string(respBody)
|
||||
|
||||
return respBodyStr
|
||||
}
|
||||
|
||||
func Provider2CaptchaV2E2E(apiKey string, url string, dataSiteKey string) string {
|
||||
captchaReqID := Provider2CaptchaCreateCaptchaRequestV2(apiKey, url, dataSiteKey)
|
||||
log.Println(captchaReqID)
|
||||
captchaAnswer := "CAPCHA_NOT_READY"
|
||||
for captchaAnswer == "CAPCHA_NOT_READY" {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
captchaAnswer = Provider2CaptchaGetCaptchaResult(apiKey, captchaReqID)
|
||||
}
|
||||
log.Printf("Captcha ID: %s; Captcha Answer: %s\n", captchaReqID, captchaAnswer)
|
||||
captchaAnswer = captchaAnswer[3:]
|
||||
|
||||
return captchaAnswer
|
||||
}
|
||||
102
backend/internal/captchasolver/providerdeathbycaptcha.go
Normal file
102
backend/internal/captchasolver/providerdeathbycaptcha.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package captchasolver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ProviderDeathByCaptchaCreateCaptchaRequestV2(deathByCaptchaUsername string, deathByCaptchaPassword string, captchaUrl string, googleKey string) string {
|
||||
httpClient := http.Client{}
|
||||
|
||||
formValues := url.Values{
|
||||
"username": {deathByCaptchaUsername},
|
||||
"password": {deathByCaptchaPassword},
|
||||
"type": {"4"},
|
||||
"token_params": {fmt.Sprintf(`{"proxy": "", "proxytype": "", "googlekey": "%s","pageurl": "%s"}`, googleKey, captchaUrl)},
|
||||
}
|
||||
req, err := http.NewRequest("POST", "http://api.dbcapi.me/api/captcha", strings.NewReader(formValues.Encode()))
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Expect", "")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
var resBodyJson map[string]any
|
||||
err = json.Unmarshal(respBody, &resBodyJson)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
if _, exists := resBodyJson["captcha"]; !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
captchaIDFloat := resBodyJson["captcha"].(float64)
|
||||
captchaID := int(captchaIDFloat)
|
||||
captchaIDStr := fmt.Sprintf("%d", captchaID)
|
||||
|
||||
return captchaIDStr
|
||||
}
|
||||
|
||||
func ProviderDeathByCaptchaGetCaptchaResult(captchaID string) string {
|
||||
client := &http.Client{}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("http://api.dbcapi.me/api/captcha/%s", captchaID), nil)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "application/json")
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
respBody, _ := io.ReadAll(res.Body)
|
||||
var resBodyJson map[string]any
|
||||
err = json.Unmarshal(respBody, &resBodyJson)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
if _, exists := resBodyJson["text"]; !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return resBodyJson["text"].(string)
|
||||
}
|
||||
|
||||
func ProviderDeathByCaptchaV2E2E(deathByCaptchaUsername string, deathByCaptchaPassword string, url string, dataSiteKey string) string {
|
||||
captchaReqID := ProviderDeathByCaptchaCreateCaptchaRequestV2(deathByCaptchaUsername, deathByCaptchaPassword, url, dataSiteKey)
|
||||
log.Println(captchaReqID)
|
||||
captchaAnswer := ""
|
||||
for captchaAnswer == "" {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
captchaAnswer = ProviderDeathByCaptchaGetCaptchaResult(captchaReqID)
|
||||
}
|
||||
log.Printf("Captcha ID: %s; Captcha Answer: %s\n", captchaReqID, captchaAnswer)
|
||||
|
||||
return captchaAnswer
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.samuelpua.com/telboon/ktm-train-bot/backend/internal/captchasolver"
|
||||
"git.samuelpua.com/telboon/ktm-train-bot/backend/internal/user"
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/devices"
|
||||
@@ -38,6 +39,12 @@ func (env *Env) BackgroundJobRunner() {
|
||||
},
|
||||
)
|
||||
|
||||
forceStartBookingString := os.Getenv("FORCE_START_BOOKING")
|
||||
forceStartBooking := false
|
||||
if strings.ToUpper(forceStartBookingString) == "TRUE" {
|
||||
forceStartBooking = true
|
||||
}
|
||||
|
||||
tx := env.DB.Session(&gorm.Session{Logger: newLogger})
|
||||
for {
|
||||
|
||||
@@ -80,7 +87,7 @@ func (env *Env) BackgroundJobRunner() {
|
||||
startTime := time.Date(timeNow.Year(), timeNow.Month(), timeNow.Day(), 00, 10, 0, 0, timeNow.Location())
|
||||
endTime := startTime.Add(15 * time.Minute)
|
||||
|
||||
if timeNow.After(startTime) && timeNow.Before(endTime) {
|
||||
if forceStartBooking || (timeNow.After(startTime) && timeNow.Before(endTime)) {
|
||||
err := env.DB.Where(&user.Profile{UserID: jobToDo.UserID}).First(&jobToDo.User.Profile).Error
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
@@ -425,6 +432,8 @@ func getBookingSlots(browser *rod.Browser, onwardDate string, reverse bool) *rod
|
||||
|
||||
func selectBookingSlot(ctx context.Context, page *rod.Page, timeCode string) *rod.Page {
|
||||
time.Sleep(5 * time.Second)
|
||||
twoCaptchaAPIKey := os.Getenv("TWOCAPTCHA_API_KEY")
|
||||
|
||||
// Initial closing of maintenance modal
|
||||
bodyText := page.MustElement("body").MustText()
|
||||
if strings.Contains(bodyText, "System maintenance scheduled at 23:00 to 00:15 (UTC+8)") {
|
||||
@@ -491,6 +500,22 @@ func selectBookingSlot(ctx context.Context, page *rod.Page, timeCode string) *ro
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Check if there is captcha
|
||||
if strings.Contains(bodyText, "Please complete the reCAPTCHA") {
|
||||
// Reset Body text
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
log.Println("Captcha detected")
|
||||
currURL := "https://shuttleonline.ktmb.com.my/ShuttleTrip"
|
||||
// Regex research for Google Captcha V2 API Key
|
||||
gcaptchaElement := page.MustElement(".g-recaptcha")
|
||||
googleRecaptchaKey := gcaptchaElement.MustAttribute("data-sitekey")
|
||||
log.Printf("Google CAPTCHA V2 Key: %s", *googleRecaptchaKey)
|
||||
captchaAnswer := captchasolver.Provider2CaptchaV2E2E(twoCaptchaAPIKey, currURL, *googleRecaptchaKey)
|
||||
page.MustElement("#g-recaptcha-response").Eval(`this.innerHTML = "` + captchaAnswer + `"`)
|
||||
page.Eval(`RecaptchaCallback()`)
|
||||
}
|
||||
|
||||
// Repeat if there's no seats -- seats might be released later
|
||||
if strings.Contains(bodyText, "Not enough seat for onward trip") {
|
||||
completed = false
|
||||
|
||||
Reference in New Issue
Block a user