Browse Source

Feat(Captcha): Initial captcha solver

master
Samuel Pua 2 years ago
parent
commit
828571312b
  1. 4
      .env.example
  2. 3
      Jenkinsfile
  3. 5
      backend/cmd/server/main.go
  4. 1
      backend/internal/captchasolver/main.go
  5. 103
      backend/internal/captchasolver/provider2captcha.go
  6. 102
      backend/internal/captchasolver/providerdeathbycaptcha.go
  7. 27
      backend/internal/ktmtrainbot/backgroundbookingjob.go

4
.env.example

@ -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

@ -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'
}

5
backend/cmd/server/main.go

@ -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

@ -0,0 +1 @@
package captchasolver

103
backend/internal/captchasolver/provider2captcha.go

@ -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

@ -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
}

27
backend/internal/ktmtrainbot/backgroundbookingjob.go

@ -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

Loading…
Cancel
Save