Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
bd24760d7d | |||
dd10f15834 | |||
81193a05ef | |||
5f60eb8752 | |||
5a40fb593c | |||
e47a96a597 | |||
4d861f037e |
@ -4,6 +4,7 @@
|
||||
на данный момент реализованы:
|
||||
- RuCaptcha
|
||||
- AntiGateV2
|
||||
- CapSolver
|
||||
|
||||
## установка и запуск (windows)
|
||||
- скачайте мост со [страницы релизов](https://git.sati.ac/sati.ac/bridge/releases)
|
||||
|
@ -5,6 +5,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.sati.ac/sati.ac/sati-go"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
@ -21,10 +23,7 @@ func (a *antigateV2Api) Name() string {
|
||||
}
|
||||
|
||||
func (a *antigateV2Api) Domains() []string {
|
||||
return []string{
|
||||
"api.anti-captcha.com",
|
||||
"api.capsolver.com",
|
||||
}
|
||||
return []string{"api.anti-captcha.com"}
|
||||
}
|
||||
|
||||
func (a *antigateV2Api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@ -32,7 +31,7 @@ func (a *antigateV2Api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type antigateError struct {
|
||||
ErrorId uint32 `json:"erorrId"`
|
||||
ErrorId uint32 `json:"errorId"`
|
||||
ErrorCode string `json:"errorCode"`
|
||||
ErrorDescription string `json:"errorDescription"`
|
||||
}
|
||||
@ -82,9 +81,23 @@ var (
|
||||
)
|
||||
|
||||
func (a *antigateV2Api) getTaskResult(request struct {
|
||||
TaskId uint32 `json:"taskId"`
|
||||
TaskId any `json:"taskId"`
|
||||
}) any {
|
||||
task := a.ctx.Registry.Get(request.TaskId)
|
||||
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 errorBadRequest
|
||||
}
|
||||
taskId = uint32(parsed)
|
||||
default:
|
||||
return errorBadRequest
|
||||
}
|
||||
|
||||
task := a.ctx.Registry.Get(taskId)
|
||||
if task == nil {
|
||||
return errorNoSuchCaptchaId
|
||||
}
|
||||
@ -147,8 +160,8 @@ func (a *antigateV2Api) createTask(request struct {
|
||||
|
||||
var id uint32
|
||||
|
||||
switch taskType {
|
||||
case "TurnstileTask", "TurnstileTaskProxyless":
|
||||
switch strings.ToLower(taskType) {
|
||||
case "turnstiletask", "turnstiletaskproxyless":
|
||||
var task struct {
|
||||
WebsiteURL string `json:"websiteURL"`
|
||||
WebsiteKey string `json:"websiteKey"`
|
||||
@ -160,7 +173,7 @@ func (a *antigateV2Api) createTask(request struct {
|
||||
SiteKey: task.WebsiteKey,
|
||||
Action: task.Action,
|
||||
})
|
||||
case "RecaptchaV2Task", "RecaptchaV2TaskProxyless":
|
||||
case "recaptchav2task", "recaptchav2taskproxyless":
|
||||
var task struct {
|
||||
WebsiteURL string `json:"websiteURL"`
|
||||
WebsiteKey string `json:"websiteKey"`
|
||||
@ -170,7 +183,7 @@ func (a *antigateV2Api) createTask(request struct {
|
||||
PageUrl: task.WebsiteURL,
|
||||
SiteKey: task.WebsiteKey,
|
||||
})
|
||||
case "FunCaptchaTask", "FunCaptchaTaskProxyless":
|
||||
case "funcaptchatask", "funcaptchataskproxyless":
|
||||
var task struct {
|
||||
WebsiteURL string `json:"websiteURL"`
|
||||
WebsitePublicKey string `json:"websitePublicKey"`
|
||||
@ -209,7 +222,7 @@ func (a *antigateV2Api) getBalance(struct{}) any {
|
||||
}{0, 0, balance.InexactFloat64()}
|
||||
}
|
||||
|
||||
func jsonHandler[T any](handler func(data T) any) func(http.ResponseWriter, *http.Request) {
|
||||
func jsonHandler[T any](api *antigateV2Api, 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) {
|
||||
@ -223,9 +236,16 @@ func jsonHandler[T any](handler func(data T) any) func(http.ResponseWriter, *htt
|
||||
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 _, ok := response.(*antigateError); ok {
|
||||
w.WriteHeader(400)
|
||||
}
|
||||
|
||||
w.Write(marshaled)
|
||||
}
|
||||
}
|
||||
@ -233,9 +253,9 @@ func jsonHandler[T any](handler func(data T) any) func(http.ResponseWriter, *htt
|
||||
func newAntigateV2Api(ctx *ApiContext) ApiHandler {
|
||||
api := &antigateV2Api{ctx, http.NewServeMux()}
|
||||
|
||||
api.mux.HandleFunc("/createTask", jsonHandler(api.createTask))
|
||||
api.mux.HandleFunc("/getTaskResult", jsonHandler(api.getTaskResult))
|
||||
api.mux.HandleFunc("/getBalance", jsonHandler(api.getBalance))
|
||||
api.mux.HandleFunc("/createTask", jsonHandler(api, api.createTask))
|
||||
api.mux.HandleFunc("/getTaskResult", jsonHandler(api, api.getTaskResult))
|
||||
api.mux.HandleFunc("/getBalance", jsonHandler(api, api.getBalance))
|
||||
|
||||
return api
|
||||
}
|
||||
|
245
api/CapSolver.go
Normal file
245
api/CapSolver.go
Normal file
@ -0,0 +1,245 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"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 (a *capSolverApi) createTask(request struct {
|
||||
Task map[string]any `json:"task"`
|
||||
}) any {
|
||||
taskType, ok := request.Task["type"].(string)
|
||||
if !ok {
|
||||
return csErrorInvalidTaskData
|
||||
}
|
||||
|
||||
var id uint32
|
||||
|
||||
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)
|
||||
id = a.ctx.Registry.CreateTask(&sati.TurnstileTask{
|
||||
PageUrl: task.WebsiteURL,
|
||||
SiteKey: task.WebsiteKey,
|
||||
Action: task.Metadata.Action,
|
||||
CData: task.Metadata.CData,
|
||||
})
|
||||
case "recaptchav2task", "recaptchav2taskproxyless":
|
||||
var task struct {
|
||||
WebsiteURL string `json:"websiteURL"`
|
||||
WebsiteKey string `json:"websiteKey"`
|
||||
}
|
||||
mapstructure.Decode(request.Task, &task)
|
||||
id = a.ctx.Registry.CreateTask(&sati.ReCaptcha2Task{
|
||||
PageUrl: task.WebsiteURL,
|
||||
SiteKey: task.WebsiteKey,
|
||||
})
|
||||
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)
|
||||
id = a.ctx.Registry.CreateTask(&sati.FunCaptchaTask{
|
||||
PageUrl: task.WebsiteURL,
|
||||
SiteKey: task.WebsitePublicKey,
|
||||
Data: task.Data,
|
||||
})
|
||||
default:
|
||||
return csErrorTaskNotSupported
|
||||
}
|
||||
|
||||
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
|
||||
}
|
19
api/api.go
19
api/api.go
@ -35,19 +35,19 @@ type RegistryStats struct {
|
||||
|
||||
type TaskRegistry struct {
|
||||
mu *sync.RWMutex
|
||||
lifetime time.Duration
|
||||
config *config.Config
|
||||
tasks map[uint32]*Task
|
||||
idCounter uint32
|
||||
api *sati.Api
|
||||
stats RegistryStats
|
||||
}
|
||||
|
||||
func NewTaskRegistry(api *sati.Api, lifetime time.Duration) *TaskRegistry {
|
||||
func NewTaskRegistry(api *sati.Api, config *config.Config) *TaskRegistry {
|
||||
return &TaskRegistry{
|
||||
mu: &sync.RWMutex{},
|
||||
tasks: map[uint32]*Task{},
|
||||
api: api,
|
||||
lifetime: lifetime,
|
||||
mu: &sync.RWMutex{},
|
||||
tasks: map[uint32]*Task{},
|
||||
api: api,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ func (t *TaskRegistry) CreateTask(task sati.AnyTask) uint32 {
|
||||
entry.EndTime = time.Now().Unix()
|
||||
t.mu.Unlock()
|
||||
|
||||
time.Sleep(t.lifetime)
|
||||
time.Sleep(time.Millisecond * time.Duration(t.config.TaskLifetime))
|
||||
t.mu.Lock()
|
||||
delete(t.tasks, id)
|
||||
t.mu.Unlock()
|
||||
@ -98,6 +98,10 @@ func (t *TaskRegistry) Stats() RegistryStats {
|
||||
}
|
||||
|
||||
func (t *TaskRegistry) Get(id uint32) *Task {
|
||||
if t.config.TaskGetDelay != 0 {
|
||||
time.Sleep(time.Millisecond * time.Duration(t.config.TaskGetDelay))
|
||||
}
|
||||
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
task := t.tasks[id]
|
||||
@ -162,6 +166,7 @@ func NewApiServer(ctx *ApiContext) *ApiServer {
|
||||
newStatsApi(ctx),
|
||||
newAntigateV2Api(ctx),
|
||||
newRuCaptchaApi(ctx),
|
||||
newCapSolverApi(ctx),
|
||||
}
|
||||
|
||||
server := &ApiServer{
|
||||
|
@ -16,6 +16,8 @@ type Config struct {
|
||||
TlsKeyPath string `json:"tlsKeyPath"`
|
||||
Host string `json:"host"`
|
||||
TlsHost string `json:"tlsHost"`
|
||||
TaskLifetime int64 `json:"taskLifetime"`
|
||||
TaskGetDelay int64 `json:"taskGetDelay"`
|
||||
AntiGateV2 struct {
|
||||
TurnstileUserAgent string `json:"turnstileUserAgent"`
|
||||
Ip string `json:"ip"`
|
||||
@ -32,6 +34,8 @@ func Default() *Config {
|
||||
TlsKeyPath: "./data/ca.key",
|
||||
Host: "127.0.0.1:80",
|
||||
TlsHost: "127.0.0.1:443",
|
||||
TaskLifetime: 60000,
|
||||
TaskGetDelay: 0,
|
||||
AntiGateV2: struct {
|
||||
TurnstileUserAgent string `json:"turnstileUserAgent"`
|
||||
Ip string `json:"ip"`
|
||||
|
4
main.go
4
main.go
@ -207,7 +207,7 @@ func addDomainsToHosts(ctx *api.ApiContext) error {
|
||||
|
||||
hosts = hostsModRE.ReplaceAll(hosts, []byte{}) // remove old entries
|
||||
|
||||
hostIp := "127.0.0.1"
|
||||
hostIp := strings.SplitN(ctx.Config.Host, ":", 2)[0]
|
||||
suffix := "\r\n#sati-bridge start, DO NOT MODIFY\r\n"
|
||||
for _, domain := range ctx.Server.GetDomains() {
|
||||
suffix += hostIp + " " + domain + "\r\n"
|
||||
@ -289,7 +289,7 @@ func main() {
|
||||
satiConfig.Debug = cfg.Debug
|
||||
satiApi := sati.NewApi(satiConfig)
|
||||
|
||||
registry := api.NewTaskRegistry(satiApi, time.Minute)
|
||||
registry := api.NewTaskRegistry(satiApi, cfg)
|
||||
|
||||
ctx := api.ApiContext{
|
||||
Config: cfg,
|
||||
|
Reference in New Issue
Block a user