9 Commits
v0.1 ... v0.3.3

Author SHA1 Message Date
bd24760d7d fix(AntiGateV2): typo in errorId json tag
All checks were successful
release-tag / release (push) Successful in 1m52s
2023-07-14 07:15:20 +03:00
dd10f15834 fix(CapSolver): separate CapSolver api from AntiGateV2
All checks were successful
release-tag / release (push) Successful in 1m53s
2023-07-14 06:49:25 +03:00
81193a05ef fix(AntiGateV2): also accept string taskId
All checks were successful
release-tag / release (push) Successful in 1m54s
2023-07-14 04:05:08 +03:00
5f60eb8752 feat: add option for delaying task get requests
All checks were successful
release-tag / release (push) Successful in 2m3s
2023-07-14 03:19:16 +03:00
5a40fb593c fix: use hostIp from config
All checks were successful
release-tag / release (push) Successful in 1m56s
2023-07-13 23:22:11 +03:00
e47a96a597 fix(AntiGateV2): match task type case-insensitively 2023-07-13 23:20:38 +03:00
4d861f037e feat(AntiGateV2): more verbose debug logging 2023-07-13 23:19:43 +03:00
00b07f7e2b feat: add funcaptcha support
All checks were successful
release-tag / release (push) Successful in 2m6s
2023-07-13 19:38:05 +03:00
197918a26d docs: add installation guide for windows 2023-07-09 20:56:39 +03:00
9 changed files with 374 additions and 35 deletions

View File

@ -1,6 +1,16 @@
### sati bridge
# sati bridge
прослойка, эмулирующая API других сервисов. для использования готового софта с [sati.ac](https://sati.ac)
на данный момент реализованы:
- RuCaptcha
- AntiGateV2
- CapSolver
## установка и запуск (windows)
- скачайте мост со [страницы релизов](https://git.sati.ac/sati.ac/bridge/releases)
- создайте новую папку и поместите в неё софт
- запустите exeшник от администратора, он закроется, это нормально. рядом должна появиться папка data, с конфигом `config.json` и сертификатом `ca.crt` внутри
- далее вам нужно добавить в конфиг ваш API токен: замените `"token": ""` на `"token": "ВАШ_ТОКЕН"`. взять его можно [тут](https://sati.ac/dashboard). если ваш софт использует API схожее с одним из тех, что поддерживаются, но на другом домене, то вы можете добавить его в extraDomains, например так: `{ "example.com": "AntiGateV2" }`
- добавьте корневой сертификат в хранилище доверенных корневых центров сертификации, кликнув на него, и выбрав нужное хранилище
- снова запустите exeшник с правами администратора, если в логе есть сообщение `starting api server`, то мост успешно запущен
- проверьте работу софта, открыв [bridge.sati.ac](https://bridge.sati.ac) в браузере. если все работает - там будет показана статистика

View File

@ -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
}
@ -115,19 +128,20 @@ func (a *antigateV2Api) getTaskResult(request struct {
response.Status = "ready"
response.EndTime = &task.EndTime
response.Cost = task.Entity.Cost
switch task.Result.(type) {
switch result := task.Result.(type) {
case *sati.ReCaptcha2Result:
response.Solution = struct {
GRecaptchaResponse string `json:"gRecaptchaResponse"`
}{task.Result.(*sati.ReCaptcha2Result).Token}
}{result.Token}
case *sati.TurnstileResult:
response.Solution = struct {
Token string `json:"token"`
UserAgent string `json:"userAgent"`
}{
task.Result.(*sati.TurnstileResult).Token,
a.ctx.Config.AntiGateV2.TurnstileUserAgent,
}
}{result.Token, a.ctx.Config.AntiGateV2.TurnstileUserAgent}
case *sati.FunCaptchaResult:
response.Solution = struct {
Token string `json:"token"`
}{result.Token}
default:
return errorTaskNotSupported
}
@ -146,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"`
@ -159,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"`
@ -169,6 +183,18 @@ func (a *antigateV2Api) createTask(request struct {
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 errorTaskNotSupported
}
@ -196,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) {
@ -210,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)
}
}
@ -220,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
View 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
}

View File

@ -72,6 +72,24 @@ 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 (a *ruCaptchaApi) endpointIn(params url.Values) ruCaptchaResponse {
var id uint32
switch params.Get("method") {
@ -125,6 +143,26 @@ func (a *ruCaptchaApi) endpointIn(params url.Values) ruCaptchaResponse {
Action: action,
CData: cData,
})
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,
})
default:
return &simpleResponse{0, "ERROR_ZERO_CAPTCHA_FILESIZE"}
}
@ -143,11 +181,13 @@ func (r *get2Response) text() string {
}
func (a *ruCaptchaApi) convertTaskResult(task *Task) string {
switch task.Result.(type) {
switch result := task.Result.(type) {
case *sati.ReCaptcha2Result:
return task.Result.(*sati.ReCaptcha2Result).Token
return result.Token
case *sati.TurnstileResult:
return task.Result.(*sati.TurnstileResult).Token
return result.Token
case *sati.FunCaptchaResult:
return result.Token
}
a.ctx.Logger.WithFields(logrus.Fields{

View File

@ -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{

View File

@ -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"`

2
go.mod
View File

@ -2,7 +2,7 @@ module git.sati.ac/sati.ac/bridge
go 1.20
require git.sati.ac/sati.ac/sati-go v0.0.0-20230630184329-03a405a25122
require git.sati.ac/sati.ac/sati-go v0.0.0-20230713145537-57719018ca00
require (
github.com/gorilla/websocket v1.5.0 // indirect

2
go.sum
View File

@ -4,6 +4,8 @@ git.sati.ac/sati.ac/sati-go v0.0.0-20230629103120-de6961c4f6ec h1:DHmxYp62PfGP+j
git.sati.ac/sati.ac/sati-go v0.0.0-20230629103120-de6961c4f6ec/go.mod h1:dsLvwV5+2YUjWRAuTYFf/EMvoH/twUu/NWA0t5Yl3pQ=
git.sati.ac/sati.ac/sati-go v0.0.0-20230630184329-03a405a25122 h1:Uff2QZeRDk+3cm3cN9tUiUoT2VoKcwbIf32wYgYSUNU=
git.sati.ac/sati.ac/sati-go v0.0.0-20230630184329-03a405a25122/go.mod h1:dsLvwV5+2YUjWRAuTYFf/EMvoH/twUu/NWA0t5Yl3pQ=
git.sati.ac/sati.ac/sati-go v0.0.0-20230713145537-57719018ca00 h1:emjsk5AubG3EiCbiG0WAG6xAZXoNNwO/yteYj/J0TqA=
git.sati.ac/sati.ac/sati-go v0.0.0-20230713145537-57719018ca00/go.mod h1:dsLvwV5+2YUjWRAuTYFf/EMvoH/twUu/NWA0t5Yl3pQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=

View File

@ -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,