2023-07-02 18:00:59 +02:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2023-07-26 07:08:31 +02:00
|
|
|
"fmt"
|
2023-07-02 18:00:59 +02:00
|
|
|
"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()
|
2023-07-26 07:14:35 +02:00
|
|
|
if r.Method != "GET" {
|
|
|
|
switch strings.ToLower(r.Header.Get("Content-Type")) {
|
|
|
|
case "application/x-www-form-urlencoded":
|
|
|
|
r.ParseForm()
|
|
|
|
case "multipart/form-data":
|
|
|
|
r.ParseMultipartForm(0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// merge form with url params
|
|
|
|
for key, values := range r.Form {
|
|
|
|
if _, ok := query[key]; ok {
|
|
|
|
query[key] = append(query[key], values...)
|
|
|
|
} else {
|
|
|
|
query[key] = values
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-02 18:00:59 +02:00
|
|
|
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 }
|
|
|
|
|
2023-07-13 18:38:05 +02:00
|
|
|
func parsePhpAssociativeArray(values map[string][]string, arrayName string) map[string]string {
|
|
|
|
result := make(map[string]string)
|
|
|
|
prefix := arrayName + "["
|
|
|
|
suffix := "]"
|
|
|
|
|
|
|
|
for key, values := range values {
|
|
|
|
if strings.HasPrefix(key, prefix) && strings.HasSuffix(key, suffix) && len(values) > 0 {
|
|
|
|
result[key[len(prefix):len(key)-len(suffix)]] = values[0]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(result) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2023-07-26 07:08:31 +02:00
|
|
|
func extractRuCaptchaProxy(params url.Values) *string {
|
|
|
|
if !params.Has("proxytype") || !params.Has("proxy") {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
proxyUrl := fmt.Sprintf("%s://%s", params.Get("proxytype"), params.Get("proxy"))
|
|
|
|
url, err := url.Parse(proxyUrl)
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
formatted := url.String()
|
|
|
|
return &formatted
|
|
|
|
}
|
|
|
|
|
2023-07-02 18:00:59 +02:00
|
|
|
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,
|
2023-07-26 07:08:31 +02:00
|
|
|
Proxy: extractRuCaptchaProxy(params),
|
2023-07-02 18:00:59 +02:00
|
|
|
})
|
|
|
|
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,
|
2023-07-26 07:08:31 +02:00
|
|
|
Proxy: extractRuCaptchaProxy(params),
|
2023-07-02 18:00:59 +02:00
|
|
|
})
|
2023-07-13 18:38:05 +02:00
|
|
|
case "funcaptcha":
|
|
|
|
siteKey := params.Get("publickey")
|
|
|
|
pageUrl := params.Get("pageurl")
|
|
|
|
data := parsePhpAssociativeArray(params, "data")
|
|
|
|
if siteKey == "" || pageUrl == "" {
|
|
|
|
return &simpleResponse{0, "ERROR_BAD_PARAMETERS"}
|
|
|
|
}
|
|
|
|
|
|
|
|
var serviceUrl *string
|
|
|
|
if params.Has("surl") {
|
|
|
|
val := params.Get("surl")
|
|
|
|
serviceUrl = &val
|
|
|
|
}
|
|
|
|
|
|
|
|
id = a.ctx.Registry.CreateTask(&sati.FunCaptchaTask{
|
|
|
|
SiteKey: siteKey,
|
|
|
|
PageUrl: pageUrl,
|
|
|
|
ServiceUrl: serviceUrl,
|
|
|
|
Data: data,
|
2023-07-26 07:08:31 +02:00
|
|
|
Proxy: extractRuCaptchaProxy(params),
|
2023-07-13 18:38:05 +02:00
|
|
|
})
|
2023-07-02 18:00:59 +02:00
|
|
|
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 {
|
2023-07-13 18:38:05 +02:00
|
|
|
switch result := task.Result.(type) {
|
2023-07-02 18:00:59 +02:00
|
|
|
case *sati.ReCaptcha2Result:
|
2023-07-13 18:38:05 +02:00
|
|
|
return result.Token
|
2023-07-02 18:00:59 +02:00
|
|
|
case *sati.TurnstileResult:
|
2023-07-13 18:38:05 +02:00
|
|
|
return result.Token
|
|
|
|
case *sati.FunCaptchaResult:
|
|
|
|
return result.Token
|
2023-07-02 18:00:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|