This commit is contained in:
228
api/AntiGateV2.go
Normal file
228
api/AntiGateV2.go
Normal file
@ -0,0 +1,228 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"git.sati.ac/sati.ac/sati-go"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type antigateV2Api struct {
|
||||
ctx *ApiContext
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func (a *antigateV2Api) Name() string {
|
||||
return "AntiGateV2"
|
||||
}
|
||||
|
||||
func (a *antigateV2Api) Domains() []string {
|
||||
return []string{
|
||||
"api.anti-captcha.com",
|
||||
"api.capsolver.com",
|
||||
}
|
||||
}
|
||||
|
||||
func (a *antigateV2Api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
type antigateError struct {
|
||||
ErrorId uint32 `json:"erorrId"`
|
||||
ErrorCode string `json:"errorCode"`
|
||||
ErrorDescription string `json:"errorDescription"`
|
||||
}
|
||||
|
||||
var (
|
||||
errorNoSuchMethod = &antigateError{
|
||||
ErrorId: 14,
|
||||
ErrorCode: "ERROR_NO_SUCH_METHOD",
|
||||
ErrorDescription: "Request made to API with a method that does not exist",
|
||||
}
|
||||
|
||||
errorCaptchaUnsolvable = &antigateError{
|
||||
ErrorId: 12,
|
||||
ErrorCode: "ERROR_CAPTCHA_UNSOLVABLE",
|
||||
ErrorDescription: "Captcha unsolvable",
|
||||
}
|
||||
|
||||
errorNoSuchCaptchaId = &antigateError{
|
||||
ErrorId: 16,
|
||||
ErrorCode: "ERROR_NO_SUCH_CAPCHA_ID",
|
||||
ErrorDescription: "The captcha you are requesting does not exist in your active captchas list or has expired",
|
||||
}
|
||||
|
||||
errorTaskAbsent = &antigateError{
|
||||
ErrorId: 22,
|
||||
ErrorCode: "ERROR_TASK_ABSENT",
|
||||
ErrorDescription: `"task" property is empty or not set in the createTask method`,
|
||||
}
|
||||
|
||||
errorTaskNotSupported = &antigateError{
|
||||
ErrorId: 23,
|
||||
ErrorCode: "ERROR_TASK_NOT_SUPPORTED",
|
||||
ErrorDescription: "Task type is not supported or typed incorrectly",
|
||||
}
|
||||
|
||||
errorInternal = &antigateError{
|
||||
ErrorId: 1000,
|
||||
ErrorCode: "ERROR_INTERNAL",
|
||||
ErrorDescription: "Internal error, check bridge logs",
|
||||
}
|
||||
|
||||
errorBadRequest = &antigateError{
|
||||
ErrorId: 1001,
|
||||
ErrorCode: "ERROR_BAD_REQUEST",
|
||||
ErrorDescription: "Bad request",
|
||||
}
|
||||
)
|
||||
|
||||
func (a *antigateV2Api) getTaskResult(request struct {
|
||||
TaskId uint32 `json:"taskId"`
|
||||
}) any {
|
||||
task := a.ctx.Registry.Get(request.TaskId)
|
||||
if task == nil {
|
||||
return errorNoSuchCaptchaId
|
||||
}
|
||||
|
||||
if task.State == StateError {
|
||||
return errorCaptchaUnsolvable
|
||||
}
|
||||
|
||||
response := &struct {
|
||||
ErrorId uint `json:"errorId"`
|
||||
Status string `json:"status"`
|
||||
Solution any `json:"solution"`
|
||||
Cost string `json:"cost"`
|
||||
Ip string `json:"ip"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
EndTime *int64 `json:"endTime,omitempty"`
|
||||
SolveCount uint `json:"solveCount"`
|
||||
}{
|
||||
CreateTime: task.CreateTime,
|
||||
SolveCount: 1,
|
||||
Ip: a.ctx.Config.AntiGateV2.Ip,
|
||||
Cost: "0",
|
||||
}
|
||||
|
||||
if task.State == StateProcessing {
|
||||
response.Status = "processing"
|
||||
} else {
|
||||
response.Status = "ready"
|
||||
response.EndTime = &task.EndTime
|
||||
response.Cost = task.Entity.Cost
|
||||
switch task.Result.(type) {
|
||||
case *sati.ReCaptcha2Result:
|
||||
response.Solution = struct {
|
||||
GRecaptchaResponse string `json:"gRecaptchaResponse"`
|
||||
}{task.Result.(*sati.ReCaptcha2Result).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,
|
||||
}
|
||||
default:
|
||||
return errorTaskNotSupported
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func (a *antigateV2Api) createTask(request struct {
|
||||
Task map[string]any `json:"task"`
|
||||
}) any {
|
||||
taskType, ok := request.Task["type"].(string)
|
||||
if !ok {
|
||||
return errorTaskAbsent
|
||||
}
|
||||
|
||||
var id uint32
|
||||
|
||||
switch taskType {
|
||||
case "TurnstileTask", "TurnstileTaskProxyless":
|
||||
var task struct {
|
||||
WebsiteURL string `json:"websiteURL"`
|
||||
WebsiteKey string `json:"websiteKey"`
|
||||
Action *string `json:"action"`
|
||||
}
|
||||
mapstructure.Decode(request.Task, &task)
|
||||
id = a.ctx.Registry.CreateTask(&sati.TurnstileTask{
|
||||
PageUrl: task.WebsiteURL,
|
||||
SiteKey: task.WebsiteKey,
|
||||
Action: task.Action,
|
||||
})
|
||||
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,
|
||||
})
|
||||
default:
|
||||
return errorTaskNotSupported
|
||||
}
|
||||
|
||||
return &struct {
|
||||
ErrorId uint32 `json:"errorId"`
|
||||
TaskId uint32 `json:"taskId"`
|
||||
}{0, id}
|
||||
}
|
||||
|
||||
func (a *antigateV2Api) 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 errorInternal
|
||||
}
|
||||
|
||||
return &struct {
|
||||
ErrorId uint `json:"errorId"`
|
||||
CaptchaCredits uint `json:"captchaCredits"`
|
||||
Balance float64 `json:"balance"`
|
||||
}{0, 0, balance.InexactFloat64()}
|
||||
}
|
||||
|
||||
func jsonHandler[T any](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(errorBadRequest)
|
||||
w.Write(marshaled)
|
||||
return
|
||||
}
|
||||
|
||||
response := handler(request)
|
||||
|
||||
marshaled, _ := json.Marshal(response)
|
||||
w.Write(marshaled)
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
return api
|
||||
}
|
267
api/RuCaptcha.go
Normal file
267
api/RuCaptcha.go
Normal file
@ -0,0 +1,267 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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()
|
||||
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 (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,
|
||||
})
|
||||
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,
|
||||
})
|
||||
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 task.Result.(type) {
|
||||
case *sati.ReCaptcha2Result:
|
||||
return task.Result.(*sati.ReCaptcha2Result).Token
|
||||
case *sati.TurnstileResult:
|
||||
return task.Result.(*sati.TurnstileResult).Token
|
||||
}
|
||||
|
||||
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
|
||||
}
|
108
api/Stats.go
Normal file
108
api/Stats.go
Normal file
@ -0,0 +1,108 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type statsApi struct {
|
||||
ctx *ApiContext
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func (a *statsApi) Name() string {
|
||||
return "Stats"
|
||||
}
|
||||
|
||||
func (a *statsApi) Domains() []string {
|
||||
return []string{"bridge.sati.ac"}
|
||||
}
|
||||
|
||||
func (a *statsApi) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
var statsTemplate = func() *template.Template {
|
||||
template, err := template.New("stats").Parse(`
|
||||
<html>
|
||||
<head>
|
||||
<title>sati bridge</title>
|
||||
<style>
|
||||
.highlight {
|
||||
color: #FF7143;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 5px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-size: 14px;
|
||||
background: #0f0805;
|
||||
color: #ddd;
|
||||
font-family: "Segoe UI","Noto Sans",sans-serif;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid #592D25;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<span class="highlight">it works!</span> statistics of this instance:
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>domain</th><th>handler</th></tr>
|
||||
{{range $domain, $handler := .Domains}}
|
||||
<tr><td>{{$domain}}</td><td>{{$handler}}</td></tr>
|
||||
{{end}}
|
||||
</table>
|
||||
<br/>
|
||||
<table>
|
||||
<tr><th>task registry</th></tr>
|
||||
<tr><td>Total</td><td>{{.Registry.Total}}</td></tr>
|
||||
<tr><td>Success</td><td>{{.Registry.Success}}</td></tr>
|
||||
<tr><td>Error</td><td>{{.Registry.Error}}</td></tr>
|
||||
<tr><td>Processing</td><td>{{.Registry.Processing}}</td></tr>
|
||||
</table>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return template
|
||||
}()
|
||||
|
||||
func (a *statsApi) stats(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
|
||||
domains := make(map[string]string, len(a.ctx.Server.domains))
|
||||
for domain, handler := range a.ctx.Server.domains {
|
||||
domains[domain] = handler.Name()
|
||||
}
|
||||
|
||||
statsTemplate.Execute(w, &struct {
|
||||
Domains map[string]string
|
||||
Registry RegistryStats
|
||||
}{domains, a.ctx.Registry.Stats()})
|
||||
}
|
||||
|
||||
func newStatsApi(ctx *ApiContext) ApiHandler {
|
||||
api := &statsApi{ctx, http.NewServeMux()}
|
||||
|
||||
api.mux.HandleFunc("/", api.stats)
|
||||
|
||||
return api
|
||||
}
|
195
api/api.go
Normal file
195
api/api.go
Normal file
@ -0,0 +1,195 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.sati.ac/sati.ac/bridge/config"
|
||||
"git.sati.ac/sati.ac/sati-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type TaskState uint
|
||||
|
||||
const (
|
||||
StateProcessing TaskState = iota
|
||||
StateSuccess
|
||||
StateError
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
State TaskState
|
||||
Entity *sati.TaskEntity
|
||||
Result any
|
||||
CreateTime int64
|
||||
EndTime int64
|
||||
}
|
||||
|
||||
type RegistryStats struct {
|
||||
Total uint32
|
||||
Success uint32
|
||||
Error uint32
|
||||
Processing uint32
|
||||
}
|
||||
|
||||
type TaskRegistry struct {
|
||||
mu *sync.RWMutex
|
||||
lifetime time.Duration
|
||||
tasks map[uint32]*Task
|
||||
idCounter uint32
|
||||
api *sati.Api
|
||||
stats RegistryStats
|
||||
}
|
||||
|
||||
func NewTaskRegistry(api *sati.Api, lifetime time.Duration) *TaskRegistry {
|
||||
return &TaskRegistry{
|
||||
mu: &sync.RWMutex{},
|
||||
tasks: map[uint32]*Task{},
|
||||
api: api,
|
||||
lifetime: lifetime,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TaskRegistry) CreateTask(task sati.AnyTask) uint32 {
|
||||
t.mu.Lock()
|
||||
t.idCounter++
|
||||
id := t.idCounter
|
||||
entry := &Task{
|
||||
State: StateProcessing,
|
||||
CreateTime: time.Now().Unix(),
|
||||
}
|
||||
t.stats.Total++
|
||||
t.stats.Processing++
|
||||
t.tasks[id] = entry
|
||||
t.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
result := task.Result()
|
||||
entity, err := t.api.Solve(task, result)
|
||||
|
||||
t.mu.Lock()
|
||||
t.stats.Processing--
|
||||
if err != nil {
|
||||
t.stats.Error++
|
||||
entry.State = StateError
|
||||
} else {
|
||||
t.stats.Success++
|
||||
entry.State = StateSuccess
|
||||
entry.Entity = entity
|
||||
entry.Result = result
|
||||
}
|
||||
entry.EndTime = time.Now().Unix()
|
||||
t.mu.Unlock()
|
||||
|
||||
time.Sleep(t.lifetime)
|
||||
t.mu.Lock()
|
||||
delete(t.tasks, id)
|
||||
t.mu.Unlock()
|
||||
}()
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (t *TaskRegistry) Stats() RegistryStats {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
return t.stats
|
||||
}
|
||||
|
||||
func (t *TaskRegistry) Get(id uint32) *Task {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
task := t.tasks[id]
|
||||
if task == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := *task
|
||||
return &cloned
|
||||
}
|
||||
|
||||
type ApiContext struct {
|
||||
Config *config.Config
|
||||
Api *sati.Api
|
||||
Registry *TaskRegistry
|
||||
Logger *logrus.Logger
|
||||
Server *ApiServer
|
||||
}
|
||||
|
||||
type ApiHandler interface {
|
||||
Name() string
|
||||
Domains() []string
|
||||
ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
type ApiServer struct {
|
||||
handlers map[string]ApiHandler
|
||||
domains map[string]ApiHandler
|
||||
ctx *ApiContext
|
||||
}
|
||||
|
||||
func (a *ApiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.ctx.Logger.WithFields(logrus.Fields{
|
||||
"method": r.Method,
|
||||
"host": r.Host,
|
||||
"path": r.URL.Path,
|
||||
}).Debug("request")
|
||||
|
||||
handler, ok := a.domains[r.Host]
|
||||
if !ok {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte("domain not found"))
|
||||
return
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (a *ApiServer) GetDomains() []string {
|
||||
domains := make([]string, 0, len(a.domains))
|
||||
|
||||
for domain := range a.domains {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
|
||||
return domains
|
||||
}
|
||||
|
||||
func NewApiServer(ctx *ApiContext) *ApiServer {
|
||||
handlers := []ApiHandler{
|
||||
newStatsApi(ctx),
|
||||
newAntigateV2Api(ctx),
|
||||
newRuCaptchaApi(ctx),
|
||||
}
|
||||
|
||||
server := &ApiServer{
|
||||
handlers: map[string]ApiHandler{},
|
||||
domains: map[string]ApiHandler{},
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
for _, handler := range handlers {
|
||||
server.handlers[handler.Name()] = handler
|
||||
for _, domain := range handler.Domains() {
|
||||
server.domains[domain] = handler
|
||||
}
|
||||
}
|
||||
|
||||
for domain, handlerName := range ctx.Config.ExtraDomains {
|
||||
handler, ok := server.handlers[handlerName]
|
||||
if !ok {
|
||||
ctx.Logger.WithFields(logrus.Fields{
|
||||
"domain": domain,
|
||||
"handler": handlerName,
|
||||
}).Warn("extraDomains: handler not found, ignoring it")
|
||||
continue
|
||||
}
|
||||
|
||||
server.domains[domain] = handler
|
||||
}
|
||||
|
||||
ctx.Server = server
|
||||
return server
|
||||
}
|
Reference in New Issue
Block a user