package api import ( "encoding/json" "fmt" "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() if r.Method != "GET" { var err error switch strings.ToLower(r.Header.Get("Content-Type")) { case "application/x-www-form-urlencoded": err = r.ParseForm() case "multipart/form-data": err = r.ParseMultipartForm(0) } if err != nil { // 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 } } } } 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 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 } 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 } 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, Proxy: extractRuCaptchaProxy(params), }) 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, Proxy: extractRuCaptchaProxy(params), }) 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, Proxy: extractRuCaptchaProxy(params), }) case "geetest": task := &sati.GeeTest3Task{ Proxy: extractRuCaptchaProxy(params), SiteKey: params.Get("gt"), PageUrl: params.Get("pageurl"), Challenge: params.Get("challenge"), } if task.SiteKey == "" || task.PageUrl == "" || task.Challenge == "" { return &simpleResponse{0, "ERROR_BAD_PARAMETERS"} } if params.Has("api_server") { apiServer := params.Get("api_server") task.ApiServer = &apiServer } id = a.ctx.Registry.CreateTask(task) 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 result := task.Result.(type) { case *sati.ReCaptcha2Result: return result.Token case *sati.TurnstileResult: return result.Token case *sati.FunCaptchaResult: return result.Token case *sati.GeeTest3Result: data, _ := json.Marshal(&struct { Challenge string `json:"geetest_challenge"` Validate string `json:"geetest_validate"` Seccode string `json:"geetest_seccode"` }{result.Challenge, result.Validate, result.Seccode}) return string(data) } 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 }