bridge/api/RuCaptcha.go

268 lines
6.4 KiB
Go
Raw Normal View History

2023-07-02 18:00:59 +02:00
package api
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"strings"
"git.sati.ac/sati.ac/sati-go"
"github.com/sirupsen/logrus"
)
type ruCaptchaApi struct {
ctx *ApiContext
mux *http.ServeMux
}
func (a *ruCaptchaApi) Name() string {
return "RuCaptcha"
}
func (a *ruCaptchaApi) Domains() []string {
return []string{
"rucaptcha.com",
"2captcha.com",
}
}
func (a *ruCaptchaApi) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.mux.ServeHTTP(w, r)
}
type ruCaptchaResponse interface{ text() string }
func (a *ruCaptchaApi) wrap(handler func(url.Values) ruCaptchaResponse) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
useJson := false
if val := query.Get("json"); val != "" && val != "0" {
useJson = true
}
if val := query.Get("header_acao"); val != "" && val != "0" {
w.Header().Add("Access-Control-Allow-Origin", "*")
}
result := handler(query)
if useJson {
w.Header().Add("Content-Type", "application/json")
} else {
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
}
if useJson {
marshaled, _ := json.Marshal(result)
w.Write(marshaled)
} else {
w.Write([]byte(result.text()))
}
}
}
type simpleResponse struct {
Status uint `json:"status"`
Request string `json:"request"`
}
func (r *simpleResponse) text() string { return r.Request }
type okResponse simpleResponse
func (r *okResponse) text() string { return "OK|" + r.Request }
func (a *ruCaptchaApi) endpointIn(params url.Values) ruCaptchaResponse {
var id uint32
switch params.Get("method") {
case "userrecaptcha":
if val := params.Get("version"); val != "" && val != "v2" {
a.ctx.Logger.WithFields(logrus.Fields{
"handler": a.Name(),
"method": "in.php",
}).Warn("recaptcha v3 isn't supported yet")
return &simpleResponse{0, "ERROR_BAD_PARAMETERS"}
}
if val := params.Get("enterprise"); val != "" && val != "0" {
a.ctx.Logger.WithFields(logrus.Fields{
"handler": a.Name(),
"method": "in.php",
}).Warn("recaptcha enterprise isn't supported yet")
return &simpleResponse{0, "ERROR_BAD_PARAMETERS"}
}
pageUrl := params.Get("pageurl")
siteKey := params.Get("googlekey")
if pageUrl == "" || siteKey == "" {
return &simpleResponse{0, "ERROR_BAD_PARAMETERS"}
}
id = a.ctx.Registry.CreateTask(&sati.ReCaptcha2Task{
PageUrl: pageUrl,
SiteKey: siteKey,
})
case "turnstile":
pageUrl := params.Get("pageurl")
siteKey := params.Get("sitekey")
if pageUrl == "" || siteKey == "" {
return &simpleResponse{0, "ERROR_BAD_PARAMETERS"}
}
var action *string
var cData *string
if params.Has("action") {
val := params.Get("action")
action = &val
}
if params.Has("data") {
val := params.Get("data")
cData = &val
}
id = a.ctx.Registry.CreateTask(&sati.TurnstileTask{
SiteKey: siteKey,
PageUrl: pageUrl,
Action: action,
CData: cData,
})
default:
return &simpleResponse{0, "ERROR_ZERO_CAPTCHA_FILESIZE"}
}
return &okResponse{1, strconv.FormatUint(uint64(id), 10)}
}
type get2Response struct {
Status uint `json:"status"`
Request string `json:"request"`
Price string `json:"price,omitempty"`
}
func (r *get2Response) text() string {
return "OK|" + r.Request + "|" + r.Price
}
func (a *ruCaptchaApi) convertTaskResult(task *Task) string {
switch task.Result.(type) {
case *sati.ReCaptcha2Result:
return task.Result.(*sati.ReCaptcha2Result).Token
case *sati.TurnstileResult:
return task.Result.(*sati.TurnstileResult).Token
}
a.ctx.Logger.WithFields(logrus.Fields{
"handler": a.Name(),
"method": "convertTaskResult",
}).Warn("unsupported result type")
return ""
}
func (a *ruCaptchaApi) handleMultiIdRes(rawIds string) ruCaptchaResponse {
responses := []string{}
for _, rawId := range strings.Split(rawIds, ",") {
id, err := strconv.ParseUint(rawId, 10, 32)
if err != nil {
responses = append(responses, "ERROR_NO_SUCH_CAPCHA_ID")
continue
}
task := a.ctx.Registry.Get(uint32(id))
if task == nil {
return &simpleResponse{0, "ERROR_NO_SUCH_CAPCHA_ID"}
}
switch task.State {
case StateProcessing:
responses = append(responses, "CAPCHA_NOT_READY")
case StateError:
responses = append(responses, "ERROR_CAPTCHA_UNSOLVABLE")
case StateSuccess:
responses = append(responses, a.convertTaskResult(task))
}
}
return &simpleResponse{1, strings.Join(responses, "|")}
}
func (a *ruCaptchaApi) safelyGetTask(rawId string) (*Task, string) {
id, err := strconv.ParseUint(rawId, 10, 32)
if err != nil {
return nil, "ERROR_WRONG_CAPTCHA_ID"
}
task := a.ctx.Registry.Get(uint32(id))
if task == nil {
return nil, "ERROR_WRONG_CAPTCHA_ID"
}
switch task.State {
case StateProcessing:
return nil, "CAPCHA_NOT_READY"
case StateError:
return nil, "ERROR_CAPTCHA_UNSOLVABLE"
case StateSuccess:
return task, ""
}
return nil, "ERROR_INTERNAL" // this should never happen
}
func (a *ruCaptchaApi) endpointRes(params url.Values) ruCaptchaResponse {
switch params.Get("action") {
case "getbalance":
balance, err := a.ctx.Api.GetBalance()
if err != nil {
a.ctx.Logger.WithFields(logrus.Fields{
"handler": a.Name(),
"method": "getbalance",
}).Error(err.Error())
return &simpleResponse{0, "ERROR_INTERNAL"}
}
return &simpleResponse{1, balance.String()}
case "get":
if rawId := params.Get("id"); rawId != "" && rawId != "0" {
task, err := a.safelyGetTask(rawId)
if err != "" {
return &simpleResponse{0, err}
}
return &okResponse{1, a.convertTaskResult(task)}
}
if rawIds := params.Get("ids"); rawIds != "" && rawIds != "0" {
return a.handleMultiIdRes(rawIds)
}
return &simpleResponse{0, "ERROR_WRONG_CAPTCHA_ID"}
case "get2":
if rawId := params.Get("id"); rawId != "" && rawId != "0" {
task, err := a.safelyGetTask(rawId)
if err != "" {
return &simpleResponse{0, err}
}
return &get2Response{1, a.convertTaskResult(task), task.Entity.Cost}
}
if rawIds := params.Get("ids"); rawIds != "" && rawIds != "0" {
return a.handleMultiIdRes(rawIds)
}
return &simpleResponse{0, "ERROR_WRONG_CAPTCHA_ID"}
case "reportgood":
fallthrough
case "reportbad":
return &simpleResponse{1, "OK_REPORT_RECORDED"}
}
return &simpleResponse{0, "ERROR_EMPTY_METHOD"}
}
func newRuCaptchaApi(ctx *ApiContext) ApiHandler {
api := &ruCaptchaApi{ctx, http.NewServeMux()}
api.mux.HandleFunc("/in.php", api.wrap(api.endpointIn))
api.mux.HandleFunc("/res.php", api.wrap(api.endpointRes))
return api
}