package proxy import ( "bytes" "context" "encoding/json" "errors" "fmt" "github.com/sirupsen/logrus" "hy2xs-admin/model/bo" "hy2xs-admin/model/constant" "io" "net/http" "time" ) type Hysteria2Api struct { apiPort int64 } // describeFailure объясняет, ПОЧЕМУ обращение к Traffic Stats API не удалось. // // Отказ здесь бывает двух видов, и раньше журнал различал их ровно наоборот // тому, как нужно. Запись делалась так: // // if err != nil || resp.StatusCode != http.StatusOK { // logrus.Errorf("Hysteria2 KickUsers err: %v", err) // // то есть при отказе САМОГО API (200 не пришёл, а транспорт сработал) в журнал // уезжало `err: ` — сообщение без единого факта именно в том случае, // когда Hysteria что-то ответила и её ответ был бы полезен. Транспортная // ошибка при этом печаталась нормально, и внешне обе выглядели одинаково. // // Различать их стало обязательно: на этом пути теперь стоит отзыв доступа, и // «сессию разорвать не удалось» — единственный след, по которому оператор // поймёт, почему отключённый пир остался на связи. func describeFailure(err error, resp *http.Response) string { if err != nil { return err.Error() } if resp != nil { return fmt.Sprintf("unexpected status %s", resp.Status) } return "no response" } func NewHysteria2Api(apiPort int64) *Hysteria2Api { return &Hysteria2Api{ apiPort: apiPort, } } // ListUsers Информация о трафике каждого пользователя func (h *Hysteria2Api) ListUsers(clear bool, secret string) (map[string]bo.Hysteria2UserTraffic, error) { var users map[string]bo.Hysteria2UserTraffic ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() url := fmt.Sprintf("http://127.0.0.1:%d/traffic", h.apiPort) if clear { url = fmt.Sprintf("%s?clear=1", url) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { logrus.Errorf("Hysteria2 ListUsers NewRequest err: %v", err) return nil, errors.New(constant.SysError) } req.Header.Set("Authorization", secret) resp, err := http.DefaultClient.Do(req) defer func() { if resp != nil { _ = resp.Body.Close() } }() if err != nil || resp.StatusCode != http.StatusOK { logrus.Errorf("Hysteria2 ListUsers failed: %s", describeFailure(err, resp)) return nil, errors.New("http connection error") } body, err := io.ReadAll(resp.Body) if err != nil { logrus.Errorf("Hysteria2 io read err: %v", err) return nil, errors.New("http connection error") } if err = json.Unmarshal(body, &users); err != nil { logrus.Errorf("Hysteria2 ListUsers Unmarshal err: %v", err) return nil, errors.New(constant.SysError) } return users, nil } // KickUsers Принудительное отключение func (h *Hysteria2Api) KickUsers(keys []string, secret string) error { usernamesByte, err := json.Marshal(keys) if err != nil { logrus.Errorf("Hysteria2 KickUsers Marshal err: %v", err) return errors.New(constant.SysError) } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() url := fmt.Sprintf("http://127.0.0.1:%d/kick", h.apiPort) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(usernamesByte)) if err != nil { logrus.Errorf("Hysteria2 KickUsers NewRequest err: %v", err) return errors.New(constant.SysError) } req.Header.Set("Authorization", secret) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) defer func() { if resp != nil { resp.Body.Close() } }() if err != nil || resp.StatusCode != http.StatusOK { logrus.Errorf("Hysteria2 KickUsers failed: %s", describeFailure(err, resp)) return errors.New("http connection error") } return nil } // OnlineUsers Пользователи онлайн func (h *Hysteria2Api) OnlineUsers(secret string) (map[string]int64, error) { var onlineUsers map[string]int64 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() url := fmt.Sprintf("http://127.0.0.1:%d/online", h.apiPort) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { logrus.Errorf("Hysteria2 OnlineUsers NewRequest err: %v", err) return nil, errors.New(constant.SysError) } req.Header.Set("Authorization", secret) resp, err := http.DefaultClient.Do(req) defer func() { if resp != nil { _ = resp.Body.Close() } }() if err != nil || resp.StatusCode != http.StatusOK { logrus.Errorf("Hysteria2 OnlineUsers failed: %s", describeFailure(err, resp)) return nil, errors.New("http connection error") } body, err := io.ReadAll(resp.Body) if err != nil { logrus.Errorf("Hysteria2 io read err: %v", err) return nil, errors.New("http connection error") } if err = json.Unmarshal(body, &onlineUsers); err != nil { logrus.Errorf("Hysteria2 OnlineUsers Unmarshal err: %v", err) return nil, errors.New(constant.SysError) } return onlineUsers, nil }