Compare commits

..

No commits in common. "master" and "v0.3" have entirely different histories.
master ... v0.3

7 changed files with 16 additions and 528 deletions

View File

@ -4,7 +4,6 @@
на данный момент реализованы: на данный момент реализованы:
- RuCaptcha - RuCaptcha
- AntiGateV2 - AntiGateV2
- CapSolver
## установка и запуск (windows) ## установка и запуск (windows)
- скачайте мост со [страницы релизов](https://git.sati.ac/sati.ac/bridge/releases) - скачайте мост со [страницы релизов](https://git.sati.ac/sati.ac/bridge/releases)

View File

@ -2,11 +2,9 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"reflect" "reflect"
"strconv"
"strings" "strings"
"git.sati.ac/sati.ac/sati-go" "git.sati.ac/sati.ac/sati-go"
@ -24,7 +22,10 @@ func (a *antigateV2Api) Name() string {
} }
func (a *antigateV2Api) Domains() []string { func (a *antigateV2Api) Domains() []string {
return []string{"api.anti-captcha.com"} return []string{
"api.anti-captcha.com",
"api.capsolver.com",
}
} }
func (a *antigateV2Api) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (a *antigateV2Api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -32,7 +33,7 @@ func (a *antigateV2Api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
type antigateError struct { type antigateError struct {
ErrorId uint32 `json:"errorId"` ErrorId uint32 `json:"erorrId"`
ErrorCode string `json:"errorCode"` ErrorCode string `json:"errorCode"`
ErrorDescription string `json:"errorDescription"` ErrorDescription string `json:"errorDescription"`
} }
@ -79,32 +80,12 @@ var (
ErrorCode: "ERROR_BAD_REQUEST", ErrorCode: "ERROR_BAD_REQUEST",
ErrorDescription: "Bad request", ErrorDescription: "Bad request",
} }
errorProxyConnectRefused = &antigateError{
ErrorId: 25,
ErrorCode: "ERROR_PROXY_CONNECT_REFUSED",
ErrorDescription: "Bad proxy",
}
) )
func (a *antigateV2Api) getTaskResult(request struct { func (a *antigateV2Api) getTaskResult(request struct {
TaskId any `json:"taskId"` TaskId uint32 `json:"taskId"`
}) any { }) any {
var taskId uint32 task := a.ctx.Registry.Get(request.TaskId)
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 { if task == nil {
return errorNoSuchCaptchaId return errorNoSuchCaptchaId
} }
@ -157,35 +138,6 @@ func (a *antigateV2Api) getTaskResult(request struct {
return response return response
} }
func collectAntiGateV2Proxy(task any) *string {
var proxy struct {
ProxyType string `json:"proxyType"`
ProxyAddress string `json:"proxyAddress"`
ProxyPort uint16 `json:"proxyPort"`
ProxyLogin *string `json:"proxyLogin"`
ProxyPassword *string `json:"proxyPassword"`
}
if err := mapstructure.Decode(task, proxy); err != nil {
return nil
}
if proxy.ProxyType == "" || proxy.ProxyAddress == "" || proxy.ProxyPort == 0 {
return nil
}
credentials := ""
if proxy.ProxyLogin != nil {
credentials += *proxy.ProxyLogin
if proxy.ProxyPassword != nil {
credentials += ":" + *proxy.ProxyPassword
}
credentials += "@"
}
proxyStr := fmt.Sprintf("%s://%s%s:%d", proxy.ProxyType, credentials, proxy.ProxyAddress, proxy.ProxyPort)
return &proxyStr
}
func (a *antigateV2Api) createTask(request struct { func (a *antigateV2Api) createTask(request struct {
Task map[string]any `json:"task"` Task map[string]any `json:"task"`
}) any { }) any {
@ -193,11 +145,10 @@ func (a *antigateV2Api) createTask(request struct {
if !ok { if !ok {
return errorTaskAbsent return errorTaskAbsent
} }
taskType = strings.ToLower(taskType)
var satiTask sati.AnyTask var id uint32
switch taskType { switch strings.ToLower(taskType) {
case "turnstiletask", "turnstiletaskproxyless": case "turnstiletask", "turnstiletaskproxyless":
var task struct { var task struct {
WebsiteURL string `json:"websiteURL"` WebsiteURL string `json:"websiteURL"`
@ -205,35 +156,21 @@ func (a *antigateV2Api) createTask(request struct {
Action *string `json:"action"` Action *string `json:"action"`
} }
mapstructure.Decode(request.Task, &task) mapstructure.Decode(request.Task, &task)
satiTask = &sati.TurnstileTask{ id = a.ctx.Registry.CreateTask(&sati.TurnstileTask{
PageUrl: task.WebsiteURL, PageUrl: task.WebsiteURL,
SiteKey: task.WebsiteKey, SiteKey: task.WebsiteKey,
Action: task.Action, Action: task.Action,
} })
if taskType == "turnstiletask" {
proxy := collectAntiGateV2Proxy(request.Task)
if proxy == nil {
return errorProxyConnectRefused
}
satiTask.(*sati.TurnstileTask).Proxy = proxy
}
case "recaptchav2task", "recaptchav2taskproxyless": case "recaptchav2task", "recaptchav2taskproxyless":
var task struct { var task struct {
WebsiteURL string `json:"websiteURL"` WebsiteURL string `json:"websiteURL"`
WebsiteKey string `json:"websiteKey"` WebsiteKey string `json:"websiteKey"`
} }
mapstructure.Decode(request.Task, &task) mapstructure.Decode(request.Task, &task)
satiTask = &sati.ReCaptcha2Task{ id = a.ctx.Registry.CreateTask(&sati.ReCaptcha2Task{
PageUrl: task.WebsiteURL, PageUrl: task.WebsiteURL,
SiteKey: task.WebsiteKey, SiteKey: task.WebsiteKey,
} })
if taskType == "funcaptchatask" {
proxy := collectAntiGateV2Proxy(request.Task)
if proxy == nil {
return errorProxyConnectRefused
}
satiTask.(*sati.ReCaptcha2Task).Proxy = proxy
}
case "funcaptchatask", "funcaptchataskproxyless": case "funcaptchatask", "funcaptchataskproxyless":
var task struct { var task struct {
WebsiteURL string `json:"websiteURL"` WebsiteURL string `json:"websiteURL"`
@ -241,54 +178,15 @@ func (a *antigateV2Api) createTask(request struct {
Data map[string]string `json:"data"` Data map[string]string `json:"data"`
} }
mapstructure.Decode(request.Task, &task) mapstructure.Decode(request.Task, &task)
satiTask = &sati.FunCaptchaTask{ id = a.ctx.Registry.CreateTask(&sati.FunCaptchaTask{
PageUrl: task.WebsiteURL, PageUrl: task.WebsiteURL,
SiteKey: task.WebsitePublicKey, SiteKey: task.WebsitePublicKey,
Data: task.Data, Data: task.Data,
} })
if taskType == "funcaptchatask" {
proxy := collectAntiGateV2Proxy(request.Task)
if proxy == nil {
return errorProxyConnectRefused
}
satiTask.(*sati.FunCaptchaTask).Proxy = proxy
}
case "geetesttask", "geetesttaskproxyless":
var task struct {
WebsiteURL string `json:"websiteURL"`
SiteKey string `json:"gt"`
Challenge string `json:"challenge"`
ApiServer *string `json:"geetestApiServerSubdomain"`
Version *int `json:"version"`
}
mapstructure.Decode(request.Task, &task)
if task.Version != nil && *task.Version != 3 {
a.ctx.Logger.WithFields(logrus.Fields{
"version": *task.Version,
"handler": a.Name(),
}).Warn("attempt to solve geetest with bad version")
return errorTaskNotSupported
}
satiTask = &sati.GeeTest3Task{
SiteKey: task.SiteKey,
Challenge: task.Challenge,
ApiServer: task.ApiServer,
PageUrl: task.WebsiteURL,
}
if taskType == "geetesttask" {
proxy := collectAntiGateV2Proxy(request.Task)
if proxy == nil {
return errorProxyConnectRefused
}
satiTask.(*sati.GeeTest3Task).Proxy = proxy
}
default: default:
return errorTaskNotSupported return errorTaskNotSupported
} }
id := a.ctx.Registry.CreateTask(satiTask)
return &struct { return &struct {
ErrorId uint32 `json:"errorId"` ErrorId uint32 `json:"errorId"`
TaskId uint32 `json:"taskId"` TaskId uint32 `json:"taskId"`
@ -331,11 +229,6 @@ func jsonHandler[T any](api *antigateV2Api, handler func(data T) any) func(http.
api.ctx.Logger.WithFields(logrus.Fields{"handler": api.Name(), "response": response}).Debug("response") api.ctx.Logger.WithFields(logrus.Fields{"handler": api.Name(), "response": response}).Debug("response")
marshaled, _ := json.Marshal(response) marshaled, _ := json.Marshal(response)
if _, ok := response.(*antigateError); ok {
w.WriteHeader(400)
}
w.Write(marshaled) w.Write(marshaled)
} }
} }

View File

@ -1,334 +0,0 @@
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
taskType = strings.ToLower(taskType)
switch 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
}
case "geetesttask", "geetesttaskproxyless":
var task struct {
WebsiteURL string `json:"websiteURL"`
SiteKey string `json:"gt"`
Challenge string `json:"challenge"`
CaptchaId *string `json:"captchaId"`
ApiServer *string `json:"geetestApiServerSubdomain"`
}
mapstructure.Decode(request.Task, &task)
if task.CaptchaId != nil {
a.ctx.Logger.WithField("handler", a.Name()).
Warn("attempt to solve geetest v4")
return csErrorTaskNotSupported
}
satiTask = &sati.GeeTest3Task{
SiteKey: task.SiteKey,
Challenge: task.Challenge,
ApiServer: task.ApiServer,
PageUrl: task.WebsiteURL,
}
if taskType == "geetesttask" {
proxy := collectCapSolverProxy(request.Task)
if proxy == nil {
return csErrorInvalidTaskData
}
satiTask.(*sati.GeeTest3Task).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
}

View File

@ -2,7 +2,6 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -37,27 +36,6 @@ type ruCaptchaResponse interface{ text() string }
func (a *ruCaptchaApi) wrap(handler func(url.Values) ruCaptchaResponse) func(http.ResponseWriter, *http.Request) { func (a *ruCaptchaApi) wrap(handler func(url.Values) ruCaptchaResponse) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query() 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 useJson := false
if val := query.Get("json"); val != "" && val != "0" { if val := query.Get("json"); val != "" && val != "0" {
useJson = true useJson = true
@ -112,21 +90,6 @@ func parsePhpAssociativeArray(values map[string][]string, arrayName string) map[
return result 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 { func (a *ruCaptchaApi) endpointIn(params url.Values) ruCaptchaResponse {
var id uint32 var id uint32
switch params.Get("method") { switch params.Get("method") {
@ -155,7 +118,6 @@ func (a *ruCaptchaApi) endpointIn(params url.Values) ruCaptchaResponse {
id = a.ctx.Registry.CreateTask(&sati.ReCaptcha2Task{ id = a.ctx.Registry.CreateTask(&sati.ReCaptcha2Task{
PageUrl: pageUrl, PageUrl: pageUrl,
SiteKey: siteKey, SiteKey: siteKey,
Proxy: extractRuCaptchaProxy(params),
}) })
case "turnstile": case "turnstile":
pageUrl := params.Get("pageurl") pageUrl := params.Get("pageurl")
@ -180,7 +142,6 @@ func (a *ruCaptchaApi) endpointIn(params url.Values) ruCaptchaResponse {
PageUrl: pageUrl, PageUrl: pageUrl,
Action: action, Action: action,
CData: cData, CData: cData,
Proxy: extractRuCaptchaProxy(params),
}) })
case "funcaptcha": case "funcaptcha":
siteKey := params.Get("publickey") siteKey := params.Get("publickey")
@ -201,25 +162,7 @@ func (a *ruCaptchaApi) endpointIn(params url.Values) ruCaptchaResponse {
PageUrl: pageUrl, PageUrl: pageUrl,
ServiceUrl: serviceUrl, ServiceUrl: serviceUrl,
Data: data, 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: default:
return &simpleResponse{0, "ERROR_ZERO_CAPTCHA_FILESIZE"} return &simpleResponse{0, "ERROR_ZERO_CAPTCHA_FILESIZE"}
} }
@ -245,14 +188,6 @@ func (a *ruCaptchaApi) convertTaskResult(task *Task) string {
return result.Token return result.Token
case *sati.FunCaptchaResult: case *sati.FunCaptchaResult:
return result.Token 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{ a.ctx.Logger.WithFields(logrus.Fields{

View File

@ -166,7 +166,6 @@ func NewApiServer(ctx *ApiContext) *ApiServer {
newStatsApi(ctx), newStatsApi(ctx),
newAntigateV2Api(ctx), newAntigateV2Api(ctx),
newRuCaptchaApi(ctx), newRuCaptchaApi(ctx),
newCapSolverApi(ctx),
} }
server := &ApiServer{ server := &ApiServer{

2
go.mod
View File

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

4
go.sum
View File

@ -6,10 +6,6 @@ git.sati.ac/sati.ac/sati-go v0.0.0-20230630184329-03a405a25122 h1:Uff2QZeRDk+3cm
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-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 h1:emjsk5AubG3EiCbiG0WAG6xAZXoNNwO/yteYj/J0TqA=
git.sati.ac/sati.ac/sati-go v0.0.0-20230713145537-57719018ca00/go.mod h1:dsLvwV5+2YUjWRAuTYFf/EMvoH/twUu/NWA0t5Yl3pQ= git.sati.ac/sati.ac/sati-go v0.0.0-20230713145537-57719018ca00/go.mod h1:dsLvwV5+2YUjWRAuTYFf/EMvoH/twUu/NWA0t5Yl3pQ=
git.sati.ac/sati.ac/sati-go v0.0.0-20230725102846-8e6b00348696 h1:ld33XeJOBd1skmlVz/TQrcuZPaTIuBt+J1n52oRfPAo=
git.sati.ac/sati.ac/sati-go v0.0.0-20230725102846-8e6b00348696/go.mod h1:dsLvwV5+2YUjWRAuTYFf/EMvoH/twUu/NWA0t5Yl3pQ=
git.sati.ac/sati.ac/sati-go v0.0.0-20230801043632-286e5aa6e5c9 h1:8BOOkuFUf3l9vJYc0bt+eWN7VAEZAVhbdiCBhdWtL0A=
git.sati.ac/sati.ac/sati-go v0.0.0-20230801043632-286e5aa6e5c9/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.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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=