package api import ( "encoding/json" "fmt" "io" "net/http" "net/url" "reflect" "strconv" "strings" "git.sati.ac/sati.ac/sati-go" "github.com/mitchellh/mapstructure" "github.com/sirupsen/logrus" ) type capSolverApi struct { ctx *ApiContext mux *http.ServeMux } func (a *capSolverApi) Name() string { return "CapSolver" } func (a *capSolverApi) Domains() []string { return []string{"api.capsolver.com"} } func (a *capSolverApi) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.mux.ServeHTTP(w, r) } type capsolverError struct { ErrorId uint32 `json:"errorId"` ErrorCode string `json:"errorCode"` ErrorDescription string `json:"errorDescription"` } var ( csErrorInvalidTaskData = &capsolverError{ ErrorId: 1, ErrorCode: "ERROR_INVALID_TASK_DATA", ErrorDescription: "parse request data error", } csErrorCaptchaUnsolvable = &capsolverError{ ErrorId: 1, ErrorCode: "ERROR_CAPTCHA_UNSOLVABLE", ErrorDescription: "Captcha unsolvable", } csErrorTaskNotSupported = &capsolverError{ ErrorId: 1, ErrorCode: "ERROR_TASK_NOT_SUPPORTED", ErrorDescription: "Task type is not supported or typed incorrectly", } csErrorInternal = &capsolverError{ ErrorId: 1, ErrorCode: "ERROR_INTERNAL", ErrorDescription: "Internal error, check bridge logs", } csErrorTaskIdInvalid = &capsolverError{ ErrorId: 1, ErrorCode: "ERROR_TASKID_INVALID", ErrorDescription: "Task ID does not exist or is invalid", } ) func (a *capSolverApi) getTaskResult(request struct { TaskId any `json:"taskId"` }) any { var taskId uint32 switch id := request.TaskId.(type) { case float64: taskId = uint32(id) case string: parsed, err := strconv.ParseUint(id, 10, 32) if err != nil { return csErrorTaskIdInvalid } taskId = uint32(parsed) default: return csErrorTaskIdInvalid } task := a.ctx.Registry.Get(taskId) if task == nil { return csErrorTaskIdInvalid } if task.State == StateError { return csErrorCaptchaUnsolvable } response := &struct { ErrorId uint `json:"errorId"` Status string `json:"status"` Solution any `json:"solution"` Cost string `json:"cost"` }{Cost: "0"} if task.State == StateProcessing { response.Status = "processing" } else { response.Status = "ready" response.Cost = task.Entity.Cost switch result := task.Result.(type) { case *sati.ReCaptcha2Result: response.Solution = struct { GRecaptchaResponse string `json:"gRecaptchaResponse"` }{result.Token} case *sati.TurnstileResult: response.Solution = struct { Type string `json:"type"` Token string `json:"token"` UserAgent string `json:"userAgent"` }{"turnstile", result.Token, a.ctx.Config.AntiGateV2.TurnstileUserAgent} case *sati.FunCaptchaResult: response.Solution = struct { Token string `json:"token"` }{result.Token} default: return csErrorTaskNotSupported } } return response } func collectCapSolverProxy(task any) *string { antigateLike := collectAntiGateV2Proxy(task) if antigateLike != nil { return antigateLike } input, _ := task.(map[string]any)["proxy"].(string) if input == "" { return nil } isUrl := strings.Contains(input, "://") if isUrl { // validate it url, err := url.Parse(input) if err != nil { return nil } output := url.String() return &output } fragments := strings.Split(input, ":") var output string switch len(fragments) { case 5: // proto:host:port:user:pwd output = fmt.Sprintf("%s://%s:%s@%s:%s", fragments[0], fragments[3], fragments[4], fragments[1], fragments[2]) case 4: // host:port:user:pwd, http used by default output = fmt.Sprintf("http://%s:%s@%s:%s", fragments[2], fragments[3], fragments[0], fragments[1]) default: return nil } return &output } func (a *capSolverApi) createTask(request struct { Task map[string]any `json:"task"` }) any { taskType, ok := request.Task["type"].(string) if !ok { return csErrorInvalidTaskData } var satiTask sati.AnyTask switch strings.ToLower(taskType) { case "anticloudflaretask": var task struct { WebsiteURL string `json:"websiteURL"` WebsiteKey string `json:"websiteKey"` Metadata struct { Action *string `json:"action"` CData *string `json:"cdata"` } `json:"metadata"` } mapstructure.Decode(request.Task, &task) proxy := collectCapSolverProxy(request.Task) if proxy == nil { return csErrorInvalidTaskData } satiTask = &sati.TurnstileTask{ PageUrl: task.WebsiteURL, SiteKey: task.WebsiteKey, Action: task.Metadata.Action, CData: task.Metadata.CData, Proxy: proxy, } case "recaptchav2task", "recaptchav2taskproxyless": var task struct { WebsiteURL string `json:"websiteURL"` WebsiteKey string `json:"websiteKey"` } mapstructure.Decode(request.Task, &task) satiTask = &sati.ReCaptcha2Task{ PageUrl: task.WebsiteURL, SiteKey: task.WebsiteKey, } if taskType == "recaptchav2task" { proxy := collectCapSolverProxy(request.Task) if proxy == nil { return csErrorInvalidTaskData } satiTask.(*sati.ReCaptcha2Task).Proxy = proxy } case "funcaptchatask", "funcaptchataskproxyless": var task struct { WebsiteURL string `json:"websiteURL"` WebsitePublicKey string `json:"websitePublicKey"` Data map[string]string `json:"data"` } mapstructure.Decode(request.Task, &task) satiTask = &sati.FunCaptchaTask{ PageUrl: task.WebsiteURL, SiteKey: task.WebsitePublicKey, Data: task.Data, } if taskType == "funcaptchatask" { proxy := collectCapSolverProxy(request.Task) if proxy == nil { return csErrorInvalidTaskData } satiTask.(*sati.FunCaptchaTask).Proxy = proxy } default: return csErrorTaskNotSupported } id := a.ctx.Registry.CreateTask(satiTask) return &struct { ErrorId uint32 `json:"errorId"` TaskId string `json:"taskId"` }{0, fmt.Sprint(id)} } func (a *capSolverApi) getBalance(struct{}) any { balance, err := a.ctx.Api.GetBalance() if err != nil { a.ctx.Logger.WithFields(logrus.Fields{ "handler": a.Name(), "method": "getBalance", }).Error(err.Error()) return csErrorInternal } return &struct { ErrorId uint `json:"errorId"` Balance float64 `json:"balance"` Packages []struct{} `json:"packages"` }{0, balance.InexactFloat64(), []struct{}{}} } func csJsonHandler[T any](api *capSolverApi, handler func(data T) any) func(http.ResponseWriter, *http.Request) { emptyRequest := reflect.Zero(reflect.TypeOf(handler).In(0)).Interface().(T) return func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") data, _ := io.ReadAll(r.Body) request := emptyRequest if err := json.Unmarshal(data, &request); err != nil { marshaled, _ := json.Marshal(csErrorInvalidTaskData) w.Write(marshaled) return } api.ctx.Logger.WithFields(logrus.Fields{"handler": api.Name(), "request": request}).Debug("request") response := handler(request) api.ctx.Logger.WithFields(logrus.Fields{"handler": api.Name(), "response": response}).Debug("response") marshaled, _ := json.Marshal(response) if csError, ok := response.(*capsolverError); ok && csError.ErrorId == 1 { w.WriteHeader(400) } w.Write(marshaled) } } func newCapSolverApi(ctx *ApiContext) ApiHandler { api := &capSolverApi{ctx, http.NewServeMux()} api.mux.HandleFunc("/createTask", csJsonHandler(api, api.createTask)) api.mux.HandleFunc("/getTaskResult", csJsonHandler(api, api.getTaskResult)) api.mux.HandleFunc("/getBalance", csJsonHandler(api, api.getBalance)) return api }