mirror of
https://github.com/komari-monitor/komari.git
synced 2025-10-23 03:31:56 +00:00
feat: 兼容哪吒Agent
This commit is contained in:
@@ -87,8 +87,8 @@ func GetClients(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已建立连接的客户端uuid列表
|
// 在线客户端uuid列表(WebSocket 与非 WebSocket)
|
||||||
for key := range ws.GetConnectedClients() {
|
for _, key := range ws.GetAllOnlineUUIDs() {
|
||||||
if !isLogin && hiddenMap[key] {
|
if !isLogin && hiddenMap[key] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
336
cmd/nezha_compat.go
Normal file
336
cmd/nezha_compat.go
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
apiClient "github.com/komari-monitor/komari/api/client"
|
||||||
|
"github.com/komari-monitor/komari/common"
|
||||||
|
"github.com/komari-monitor/komari/database/config"
|
||||||
|
"github.com/komari-monitor/komari/database/dbcore"
|
||||||
|
"github.com/komari-monitor/komari/database/models"
|
||||||
|
"github.com/komari-monitor/komari/utils/geoip"
|
||||||
|
"github.com/komari-monitor/komari/utils/notifier"
|
||||||
|
"github.com/komari-monitor/komari/ws"
|
||||||
|
|
||||||
|
"github.com/komari-monitor/komari/compat/nezha/proto"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/keepalive"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartNezhaCompatServer starts a gRPC server compatible with Nezha Agent.
|
||||||
|
func StartNezhaCompatServer(addr string) error {
|
||||||
|
boot := uint64(time.Now().Unix())
|
||||||
|
lis, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv := &nezhaCompatServer{bootTime: boot}
|
||||||
|
|
||||||
|
// Interceptors for metadata auth presence (lightweight; validation is inside handlers too)
|
||||||
|
unary := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||||
|
return handler(ctx, req)
|
||||||
|
}
|
||||||
|
stream := func(srvIface interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||||
|
return handler(srvIface, ss)
|
||||||
|
}
|
||||||
|
// Enable gRPC keepalive to tolerate long-idle streams
|
||||||
|
gs := grpc.NewServer(
|
||||||
|
grpc.UnaryInterceptor(unary),
|
||||||
|
grpc.StreamInterceptor(stream),
|
||||||
|
// Keepalive: allow client pings and keep connections stable without app traffic
|
||||||
|
grpc.KeepaliveParams(keepalive.ServerParameters{
|
||||||
|
Time: 2 * time.Minute,
|
||||||
|
Timeout: 20 * time.Second,
|
||||||
|
}),
|
||||||
|
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
|
||||||
|
MinTime: 20 * time.Second,
|
||||||
|
PermitWithoutStream: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
proto.RegisterNezhaServiceServer(gs, srv)
|
||||||
|
log.Printf("Nezha compat gRPC listening on %s", addr)
|
||||||
|
return gs.Serve(lis)
|
||||||
|
}
|
||||||
|
|
||||||
|
type nezhaCompatServer struct {
|
||||||
|
proto.UnimplementedNezhaServiceServer
|
||||||
|
bootTime uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// authentication helpers
|
||||||
|
func getAuth(ctx context.Context) (uuid string, secret string, err error) {
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return "", "", errors.New("missing metadata")
|
||||||
|
}
|
||||||
|
getFirst := func(key string) string {
|
||||||
|
vals := md.Get(key)
|
||||||
|
if len(vals) > 0 {
|
||||||
|
return vals[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
uuid = getFirst("client_uuid")
|
||||||
|
secret = getFirst("client_secret")
|
||||||
|
if uuid == "" || secret == "" {
|
||||||
|
return "", "", errors.New("unauthorized: missing client_uuid/client_secret")
|
||||||
|
}
|
||||||
|
return uuid, secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportSystemInfo: upsert static host info
|
||||||
|
func (s *nezhaCompatServer) ReportSystemInfo(ctx context.Context, host *proto.Host) (*proto.Receipt, error) {
|
||||||
|
uuid, secret, err := getAuth(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := upsertClientFromHost(uuid, secret, host); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &proto.Receipt{Proced: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportSystemInfo2: same as above but returns dashboard boot time
|
||||||
|
func (s *nezhaCompatServer) ReportSystemInfo2(ctx context.Context, host *proto.Host) (*proto.Uint64Receipt, error) {
|
||||||
|
uuid, secret, err := getAuth(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := upsertClientFromHost(uuid, secret, host); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &proto.Uint64Receipt{Data: s.bootTime}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportSystemState: ingest time-series records
|
||||||
|
func (s *nezhaCompatServer) ReportSystemState(stream proto.NezhaService_ReportSystemStateServer) error {
|
||||||
|
ctx := stream.Context()
|
||||||
|
uuid, _, err := getAuth(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// presence start
|
||||||
|
connID := time.Now().UnixNano()
|
||||||
|
ws.SetPresence(uuid, connID, true)
|
||||||
|
go notifier.OnlineNotification(uuid, connID)
|
||||||
|
defer func() {
|
||||||
|
ws.SetPresence(uuid, connID, false)
|
||||||
|
notifier.OfflineNotification(uuid, connID)
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
st, err := stream.Recv()
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// refresh presence TTL on every frame
|
||||||
|
ws.KeepAlivePresence(uuid, connID, 30*time.Second)
|
||||||
|
if err := ingestState(uuid, st); err != nil {
|
||||||
|
// still ack to avoid client stuck; log error
|
||||||
|
log.Printf("Nezha ingest state error: %v", err)
|
||||||
|
}
|
||||||
|
if err := stream.Send(&proto.Receipt{Proced: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestTask: do not dispatch tasks, just drain results to avoid Unimplemented
|
||||||
|
func (s *nezhaCompatServer) RequestTask(stream proto.NezhaService_RequestTaskServer) error {
|
||||||
|
ctx := stream.Context()
|
||||||
|
uuid, _, err := getAuth(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
connID := time.Now().UnixNano()
|
||||||
|
ws.SetPresence(uuid, connID, true)
|
||||||
|
defer ws.SetPresence(uuid, connID, false)
|
||||||
|
// receive results in background
|
||||||
|
recvErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
_, rerr := stream.Recv()
|
||||||
|
if rerr == io.EOF {
|
||||||
|
recvErr <- nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rerr != nil {
|
||||||
|
recvErr <- rerr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// refresh presence TTL when result received
|
||||||
|
ws.KeepAlivePresence(uuid, connID, 30*time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// send heartbeat tasks periodically
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case err := <-recvErr:
|
||||||
|
return err
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := stream.Send(&proto.Task{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unimplemented methods intentionally left as default (IOStream, ReportGeoIP)
|
||||||
|
|
||||||
|
// upsertClientFromHost maps Host into models.Client and upserts by UUID
|
||||||
|
func upsertClientFromHost(uuid, secret string, h *proto.Host) error {
|
||||||
|
db := dbcore.GetDBInstance()
|
||||||
|
now := models.FromTime(time.Now())
|
||||||
|
// token guard: if existing record has different token, reject
|
||||||
|
var exist models.Client
|
||||||
|
if err := db.Where("uuid = ?", uuid).First(&exist).Error; err == nil {
|
||||||
|
if exist.Token != "" && exist.Token != secret {
|
||||||
|
return errors.New("unauthorized: token mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cpuName := ""
|
||||||
|
if len(h.Cpu) > 0 {
|
||||||
|
cpuName = h.Cpu[0]
|
||||||
|
}
|
||||||
|
gpuName := strings.Join(h.Gpu, "; ")
|
||||||
|
osName := h.Platform
|
||||||
|
if h.PlatformVersion != "" {
|
||||||
|
osName = h.Platform + " " + h.PlatformVersion
|
||||||
|
}
|
||||||
|
// clamp uint64 to int64 safely
|
||||||
|
clamp := func(v uint64) int64 {
|
||||||
|
if v > uint64(math.MaxInt64) {
|
||||||
|
return math.MaxInt64
|
||||||
|
}
|
||||||
|
return int64(v)
|
||||||
|
}
|
||||||
|
c := models.Client{
|
||||||
|
UUID: uuid,
|
||||||
|
Token: secret,
|
||||||
|
Name: "nezha_" + uuid[0:8],
|
||||||
|
CpuName: cpuName,
|
||||||
|
Arch: h.Arch,
|
||||||
|
CpuCores: len(h.Cpu),
|
||||||
|
OS: osName,
|
||||||
|
KernelVersion: h.PlatformVersion,
|
||||||
|
Virtualization: h.Virtualization,
|
||||||
|
GpuName: gpuName,
|
||||||
|
MemTotal: clamp(h.MemTotal),
|
||||||
|
SwapTotal: clamp(h.SwapTotal),
|
||||||
|
DiskTotal: clamp(h.DiskTotal),
|
||||||
|
Version: h.Version,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
// Upsert by UUID; don't override existing Token if already set
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"cpu_name": c.CpuName,
|
||||||
|
"arch": c.Arch,
|
||||||
|
"cpu_cores": c.CpuCores,
|
||||||
|
"os": c.OS,
|
||||||
|
"kernel_version": c.KernelVersion,
|
||||||
|
"virtualization": c.Virtualization,
|
||||||
|
"gpu_name": c.GpuName,
|
||||||
|
"mem_total": c.MemTotal,
|
||||||
|
"swap_total": c.SwapTotal,
|
||||||
|
"disk_total": c.DiskTotal,
|
||||||
|
"version": c.Version,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
return db.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "uuid"}},
|
||||||
|
DoUpdates: clause.Assignments(updates),
|
||||||
|
}).Create(&c).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ingestState maps Nezha State into common.Report then saves a Record
|
||||||
|
func ingestState(uuid string, st *proto.State) error {
|
||||||
|
// we may need totals from client
|
||||||
|
db := dbcore.GetDBInstance()
|
||||||
|
var client models.Client
|
||||||
|
if err := db.Where("uuid = ?", uuid).First(&client).Error; err != nil {
|
||||||
|
// If missing, create with minimal defaults to avoid failing ingestion
|
||||||
|
client = models.Client{UUID: uuid, Token: "", Name: "nezha_" + uuid[0:8]}
|
||||||
|
_ = db.Create(&client).Error
|
||||||
|
}
|
||||||
|
rep := common.Report{
|
||||||
|
CPU: common.CPUReport{Usage: st.Cpu},
|
||||||
|
Ram: common.RamReport{Total: client.MemTotal, Used: int64(st.MemUsed)},
|
||||||
|
Swap: common.RamReport{Total: client.SwapTotal, Used: int64(st.SwapUsed)},
|
||||||
|
Load: common.LoadReport{Load1: st.Load1, Load5: st.Load5, Load15: st.Load15},
|
||||||
|
Disk: common.DiskReport{Total: client.DiskTotal, Used: int64(st.DiskUsed)},
|
||||||
|
Network: common.NetworkReport{
|
||||||
|
Up: int64(st.NetOutSpeed),
|
||||||
|
Down: int64(st.NetInSpeed),
|
||||||
|
TotalUp: int64(st.NetOutTransfer),
|
||||||
|
TotalDown: int64(st.NetInTransfer),
|
||||||
|
},
|
||||||
|
Uptime: int64(st.Uptime),
|
||||||
|
Process: int(st.ProcessCount),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
// 更新实时缓存供前端使用
|
||||||
|
ws.SetLatestReport(uuid, &rep)
|
||||||
|
// 写入内存缓存,入库交由定时聚合任务处理
|
||||||
|
return apiClient.SaveClientReport(uuid, rep)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportGeoIP: 保存 Agent 上报的 IP,并回填国家码/面板启动时间
|
||||||
|
func (s *nezhaCompatServer) ReportGeoIP(ctx context.Context, in *proto.GeoIP) (*proto.GeoIP, error) {
|
||||||
|
uuid, _, err := getAuth(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
var iso string
|
||||||
|
if in != nil && in.Ip != nil {
|
||||||
|
if v4 := strings.TrimSpace(in.Ip.Ipv4); v4 != "" {
|
||||||
|
updates["ipv4"] = v4
|
||||||
|
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
|
||||||
|
if ip := net.ParseIP(v4); ip != nil {
|
||||||
|
if gi, _ := geoip.GetGeoInfo(ip); gi != nil {
|
||||||
|
iso = gi.ISOCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v6 := strings.TrimSpace(in.Ip.Ipv6); v6 != "" {
|
||||||
|
updates["ipv6"] = v6
|
||||||
|
if iso == "" { // 优先使用 v4 的国家码
|
||||||
|
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
|
||||||
|
if ip := net.ParseIP(v6); ip != nil {
|
||||||
|
if gi, _ := geoip.GetGeoInfo(ip); gi != nil {
|
||||||
|
iso = gi.ISOCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if iso != "" {
|
||||||
|
// UI 使用旗帜 emoji
|
||||||
|
updates["region"] = geoip.GetRegionUnicodeEmoji(iso)
|
||||||
|
}
|
||||||
|
if len(updates) > 0 {
|
||||||
|
_ = dbcore.GetDBInstance().Model(&models.Client{}).Where("uuid = ?", uuid).Updates(updates).Error
|
||||||
|
}
|
||||||
|
// 回写 GeoIP(包含国家码与面板启动时间)
|
||||||
|
resp := &proto.GeoIP{Use6: in.GetUse6(), Ip: in.GetIp(), CountryCode: iso, DashboardBootTime: s.bootTime}
|
||||||
|
return resp, nil
|
||||||
|
}
|
@@ -328,6 +328,21 @@ func RunServer() {
|
|||||||
log.Fatalf("listen: %s\n", err)
|
log.Fatalf("listen: %s\n", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
// Optionally start Nezha gRPC compatibility server on separate port
|
||||||
|
if nzAddr := GetEnv("KOMARI_NEZHA_LISTEN", ""); nzAddr != "" {
|
||||||
|
go func() {
|
||||||
|
if err := StartNezhaCompatServer(nzAddr); err != nil {
|
||||||
|
log.Printf("Nezha compat server error: %v", err)
|
||||||
|
auditlog.EventLog("error", fmt.Sprintf("Nezha compat server error: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else if conf.NezhaCompatEnabled && conf.NezhaCompatListen != "" {
|
||||||
|
go func() {
|
||||||
|
if err := StartNezhaCompatServer(conf.NezhaCompatListen); err != nil {
|
||||||
|
auditlog.EventLog("error", fmt.Sprintf("Nezha compat server error: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||||
<-quit
|
<-quit
|
||||||
|
83
compat/nezha/nezha.proto
Normal file
83
compat/nezha/nezha.proto
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package proto;
|
||||||
|
option go_package = "github.com/komari-monitor/komari/compat/nezha/proto";
|
||||||
|
|
||||||
|
service NezhaService {
|
||||||
|
rpc ReportSystemState(stream State) returns (stream Receipt) {}
|
||||||
|
rpc ReportSystemInfo(Host) returns (Receipt) {}
|
||||||
|
rpc RequestTask(stream TaskResult) returns (stream Task) {}
|
||||||
|
rpc IOStream(stream IOStreamData) returns (stream IOStreamData) {}
|
||||||
|
rpc ReportGeoIP(GeoIP) returns (GeoIP) {}
|
||||||
|
rpc ReportSystemInfo2(Host) returns (Uint64Receipt) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Host {
|
||||||
|
string platform = 1;
|
||||||
|
string platform_version = 2;
|
||||||
|
repeated string cpu = 3;
|
||||||
|
uint64 mem_total = 4;
|
||||||
|
uint64 disk_total = 5;
|
||||||
|
uint64 swap_total = 6;
|
||||||
|
string arch = 7;
|
||||||
|
string virtualization = 8;
|
||||||
|
uint64 boot_time = 9;
|
||||||
|
string version = 10;
|
||||||
|
repeated string gpu = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
message State {
|
||||||
|
double cpu = 1;
|
||||||
|
uint64 mem_used = 2;
|
||||||
|
uint64 swap_used = 3;
|
||||||
|
uint64 disk_used = 4;
|
||||||
|
uint64 net_in_transfer = 5;
|
||||||
|
uint64 net_out_transfer = 6;
|
||||||
|
uint64 net_in_speed = 7;
|
||||||
|
uint64 net_out_speed = 8;
|
||||||
|
uint64 uptime = 9;
|
||||||
|
double load1 = 10;
|
||||||
|
double load5 = 11;
|
||||||
|
double load15 = 12;
|
||||||
|
uint64 tcp_conn_count = 13;
|
||||||
|
uint64 udp_conn_count = 14;
|
||||||
|
uint64 process_count = 15;
|
||||||
|
repeated State_SensorTemperature temperatures = 16;
|
||||||
|
repeated double gpu = 17;
|
||||||
|
}
|
||||||
|
|
||||||
|
message State_SensorTemperature {
|
||||||
|
string name = 1;
|
||||||
|
double temperature = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Task {
|
||||||
|
uint64 id = 1;
|
||||||
|
uint64 type = 2;
|
||||||
|
string data = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TaskResult {
|
||||||
|
uint64 id = 1;
|
||||||
|
uint64 type = 2;
|
||||||
|
float delay = 3;
|
||||||
|
string data = 4;
|
||||||
|
bool successful = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Receipt { bool proced = 1; }
|
||||||
|
|
||||||
|
message Uint64Receipt { uint64 data = 1; }
|
||||||
|
|
||||||
|
message IOStreamData { bytes data = 1; }
|
||||||
|
|
||||||
|
message GeoIP {
|
||||||
|
bool use6 = 1;
|
||||||
|
IP ip = 2;
|
||||||
|
string country_code = 3;
|
||||||
|
uint64 dashboard_boot_time = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IP {
|
||||||
|
string ipv4 = 1;
|
||||||
|
string ipv6 = 2;
|
||||||
|
}
|
1104
compat/nezha/proto/nezha.pb.go
Normal file
1104
compat/nezha/proto/nezha.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
374
compat/nezha/proto/nezha_grpc.pb.go
Normal file
374
compat/nezha/proto/nezha_grpc.pb.go
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
|
||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
const _ = grpc.SupportPackageIsVersion7
|
||||||
|
|
||||||
|
// NezhaServiceClient is the client API for NezhaService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type NezhaServiceClient interface {
|
||||||
|
ReportSystemState(ctx context.Context, opts ...grpc.CallOption) (NezhaService_ReportSystemStateClient, error)
|
||||||
|
ReportSystemInfo(ctx context.Context, in *Host, opts ...grpc.CallOption) (*Receipt, error)
|
||||||
|
RequestTask(ctx context.Context, opts ...grpc.CallOption) (NezhaService_RequestTaskClient, error)
|
||||||
|
IOStream(ctx context.Context, opts ...grpc.CallOption) (NezhaService_IOStreamClient, error)
|
||||||
|
ReportGeoIP(ctx context.Context, in *GeoIP, opts ...grpc.CallOption) (*GeoIP, error)
|
||||||
|
ReportSystemInfo2(ctx context.Context, in *Host, opts ...grpc.CallOption) (*Uint64Receipt, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type nezhaServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNezhaServiceClient(cc grpc.ClientConnInterface) NezhaServiceClient {
|
||||||
|
return &nezhaServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nezhaServiceClient) ReportSystemState(ctx context.Context, opts ...grpc.CallOption) (NezhaService_ReportSystemStateClient, error) {
|
||||||
|
stream, err := c.cc.NewStream(ctx, &_NezhaService_serviceDesc.Streams[0], "/proto.NezhaService/ReportSystemState", opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &nezhaServiceReportSystemStateClient{stream}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NezhaService_ReportSystemStateClient interface {
|
||||||
|
Send(*State) error
|
||||||
|
Recv() (*Receipt, error)
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type nezhaServiceReportSystemStateClient struct {
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceReportSystemStateClient) Send(m *State) error {
|
||||||
|
return x.ClientStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceReportSystemStateClient) Recv() (*Receipt, error) {
|
||||||
|
m := new(Receipt)
|
||||||
|
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nezhaServiceClient) ReportSystemInfo(ctx context.Context, in *Host, opts ...grpc.CallOption) (*Receipt, error) {
|
||||||
|
out := new(Receipt)
|
||||||
|
err := c.cc.Invoke(ctx, "/proto.NezhaService/ReportSystemInfo", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nezhaServiceClient) RequestTask(ctx context.Context, opts ...grpc.CallOption) (NezhaService_RequestTaskClient, error) {
|
||||||
|
stream, err := c.cc.NewStream(ctx, &_NezhaService_serviceDesc.Streams[1], "/proto.NezhaService/RequestTask", opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &nezhaServiceRequestTaskClient{stream}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NezhaService_RequestTaskClient interface {
|
||||||
|
Send(*TaskResult) error
|
||||||
|
Recv() (*Task, error)
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type nezhaServiceRequestTaskClient struct {
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceRequestTaskClient) Send(m *TaskResult) error {
|
||||||
|
return x.ClientStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceRequestTaskClient) Recv() (*Task, error) {
|
||||||
|
m := new(Task)
|
||||||
|
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nezhaServiceClient) IOStream(ctx context.Context, opts ...grpc.CallOption) (NezhaService_IOStreamClient, error) {
|
||||||
|
stream, err := c.cc.NewStream(ctx, &_NezhaService_serviceDesc.Streams[2], "/proto.NezhaService/IOStream", opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &nezhaServiceIOStreamClient{stream}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NezhaService_IOStreamClient interface {
|
||||||
|
Send(*IOStreamData) error
|
||||||
|
Recv() (*IOStreamData, error)
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type nezhaServiceIOStreamClient struct {
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceIOStreamClient) Send(m *IOStreamData) error {
|
||||||
|
return x.ClientStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceIOStreamClient) Recv() (*IOStreamData, error) {
|
||||||
|
m := new(IOStreamData)
|
||||||
|
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nezhaServiceClient) ReportGeoIP(ctx context.Context, in *GeoIP, opts ...grpc.CallOption) (*GeoIP, error) {
|
||||||
|
out := new(GeoIP)
|
||||||
|
err := c.cc.Invoke(ctx, "/proto.NezhaService/ReportGeoIP", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nezhaServiceClient) ReportSystemInfo2(ctx context.Context, in *Host, opts ...grpc.CallOption) (*Uint64Receipt, error) {
|
||||||
|
out := new(Uint64Receipt)
|
||||||
|
err := c.cc.Invoke(ctx, "/proto.NezhaService/ReportSystemInfo2", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NezhaServiceServer is the server API for NezhaService service.
|
||||||
|
// All implementations must embed UnimplementedNezhaServiceServer
|
||||||
|
// for forward compatibility
|
||||||
|
type NezhaServiceServer interface {
|
||||||
|
ReportSystemState(NezhaService_ReportSystemStateServer) error
|
||||||
|
ReportSystemInfo(context.Context, *Host) (*Receipt, error)
|
||||||
|
RequestTask(NezhaService_RequestTaskServer) error
|
||||||
|
IOStream(NezhaService_IOStreamServer) error
|
||||||
|
ReportGeoIP(context.Context, *GeoIP) (*GeoIP, error)
|
||||||
|
ReportSystemInfo2(context.Context, *Host) (*Uint64Receipt, error)
|
||||||
|
mustEmbedUnimplementedNezhaServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedNezhaServiceServer must be embedded to have forward compatible implementations.
|
||||||
|
type UnimplementedNezhaServiceServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedNezhaServiceServer) ReportSystemState(NezhaService_ReportSystemStateServer) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method ReportSystemState not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedNezhaServiceServer) ReportSystemInfo(context.Context, *Host) (*Receipt, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ReportSystemInfo not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedNezhaServiceServer) RequestTask(NezhaService_RequestTaskServer) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method RequestTask not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedNezhaServiceServer) IOStream(NezhaService_IOStreamServer) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method IOStream not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedNezhaServiceServer) ReportGeoIP(context.Context, *GeoIP) (*GeoIP, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ReportGeoIP not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedNezhaServiceServer) ReportSystemInfo2(context.Context, *Host) (*Uint64Receipt, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ReportSystemInfo2 not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedNezhaServiceServer) mustEmbedUnimplementedNezhaServiceServer() {}
|
||||||
|
|
||||||
|
// UnsafeNezhaServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to NezhaServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeNezhaServiceServer interface {
|
||||||
|
mustEmbedUnimplementedNezhaServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterNezhaServiceServer(s *grpc.Server, srv NezhaServiceServer) {
|
||||||
|
s.RegisterService(&_NezhaService_serviceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _NezhaService_ReportSystemState_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
return srv.(NezhaServiceServer).ReportSystemState(&nezhaServiceReportSystemStateServer{stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
type NezhaService_ReportSystemStateServer interface {
|
||||||
|
Send(*Receipt) error
|
||||||
|
Recv() (*State, error)
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type nezhaServiceReportSystemStateServer struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceReportSystemStateServer) Send(m *Receipt) error {
|
||||||
|
return x.ServerStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceReportSystemStateServer) Recv() (*State, error) {
|
||||||
|
m := new(State)
|
||||||
|
if err := x.ServerStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func _NezhaService_ReportSystemInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(Host)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(NezhaServiceServer).ReportSystemInfo(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/proto.NezhaService/ReportSystemInfo",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(NezhaServiceServer).ReportSystemInfo(ctx, req.(*Host))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _NezhaService_RequestTask_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
return srv.(NezhaServiceServer).RequestTask(&nezhaServiceRequestTaskServer{stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
type NezhaService_RequestTaskServer interface {
|
||||||
|
Send(*Task) error
|
||||||
|
Recv() (*TaskResult, error)
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type nezhaServiceRequestTaskServer struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceRequestTaskServer) Send(m *Task) error {
|
||||||
|
return x.ServerStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceRequestTaskServer) Recv() (*TaskResult, error) {
|
||||||
|
m := new(TaskResult)
|
||||||
|
if err := x.ServerStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func _NezhaService_IOStream_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
return srv.(NezhaServiceServer).IOStream(&nezhaServiceIOStreamServer{stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
type NezhaService_IOStreamServer interface {
|
||||||
|
Send(*IOStreamData) error
|
||||||
|
Recv() (*IOStreamData, error)
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type nezhaServiceIOStreamServer struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceIOStreamServer) Send(m *IOStreamData) error {
|
||||||
|
return x.ServerStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *nezhaServiceIOStreamServer) Recv() (*IOStreamData, error) {
|
||||||
|
m := new(IOStreamData)
|
||||||
|
if err := x.ServerStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func _NezhaService_ReportGeoIP_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GeoIP)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(NezhaServiceServer).ReportGeoIP(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/proto.NezhaService/ReportGeoIP",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(NezhaServiceServer).ReportGeoIP(ctx, req.(*GeoIP))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _NezhaService_ReportSystemInfo2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(Host)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(NezhaServiceServer).ReportSystemInfo2(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/proto.NezhaService/ReportSystemInfo2",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(NezhaServiceServer).ReportSystemInfo2(ctx, req.(*Host))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _NezhaService_serviceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "proto.NezhaService",
|
||||||
|
HandlerType: (*NezhaServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "ReportSystemInfo",
|
||||||
|
Handler: _NezhaService_ReportSystemInfo_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ReportGeoIP",
|
||||||
|
Handler: _NezhaService_ReportGeoIP_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ReportSystemInfo2",
|
||||||
|
Handler: _NezhaService_ReportSystemInfo2_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{
|
||||||
|
{
|
||||||
|
StreamName: "ReportSystemState",
|
||||||
|
Handler: _NezhaService_ReportSystemState_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
ClientStreams: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StreamName: "RequestTask",
|
||||||
|
Handler: _NezhaService_RequestTask_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
ClientStreams: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StreamName: "IOStream",
|
||||||
|
Handler: _NezhaService_IOStream_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
ClientStreams: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: "compat/nezha/nezha.proto",
|
||||||
|
}
|
@@ -25,15 +25,17 @@ func Get() (models.Config, error) {
|
|||||||
if err := db.First(&config).Error; err != nil {
|
if err := db.First(&config).Error; err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
config = models.Config{
|
config = models.Config{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Sitename: "Komari",
|
Sitename: "Komari",
|
||||||
Description: "Komari Monitor, a simple server monitoring tool.",
|
Description: "Komari Monitor, a simple server monitoring tool.",
|
||||||
AllowCors: false,
|
AllowCors: false,
|
||||||
OAuthEnabled: false,
|
OAuthEnabled: false,
|
||||||
GeoIpEnabled: true,
|
GeoIpEnabled: true,
|
||||||
GeoIpProvider: "ip-api",
|
GeoIpProvider: "ip-api",
|
||||||
UpdatedAt: models.FromTime(time.Now()),
|
NezhaCompatEnabled: false,
|
||||||
CreatedAt: models.FromTime(time.Now()),
|
NezhaCompatListen: "",
|
||||||
|
UpdatedAt: models.FromTime(time.Now()),
|
||||||
|
CreatedAt: models.FromTime(time.Now()),
|
||||||
}
|
}
|
||||||
if err := db.Create(&config).Error; err != nil {
|
if err := db.Create(&config).Error; err != nil {
|
||||||
log.Fatal("Failed to create default config:", err)
|
log.Fatal("Failed to create default config:", err)
|
||||||
|
@@ -12,6 +12,9 @@ type Config struct {
|
|||||||
// GeoIP 配置
|
// GeoIP 配置
|
||||||
GeoIpEnabled bool `json:"geo_ip_enabled" gorm:"default:true"`
|
GeoIpEnabled bool `json:"geo_ip_enabled" gorm:"default:true"`
|
||||||
GeoIpProvider string `json:"geo_ip_provider" gorm:"type:varchar(20);default:'ip-api'"` // empty, mmdb, ip-api, geojs
|
GeoIpProvider string `json:"geo_ip_provider" gorm:"type:varchar(20);default:'ip-api'"` // empty, mmdb, ip-api, geojs
|
||||||
|
// Nezha 兼容(Agent gRPC)
|
||||||
|
NezhaCompatEnabled bool `json:"nezha_compat_enabled" gorm:"default:false"`
|
||||||
|
NezhaCompatListen string `json:"nezha_compat_listen" gorm:"type:varchar(100);default:''"` // 例如 0.0.0.0:5555
|
||||||
// OAuth 配置
|
// OAuth 配置
|
||||||
OAuthEnabled bool `json:"o_auth_enabled" gorm:"default:false"`
|
OAuthEnabled bool `json:"o_auth_enabled" gorm:"default:false"`
|
||||||
OAuthProvider string `json:"o_auth_provider" gorm:"type:varchar(50);default:'github'"`
|
OAuthProvider string `json:"o_auth_provider" gorm:"type:varchar(50);default:'github'"`
|
||||||
|
16
go.mod
16
go.mod
@@ -12,6 +12,8 @@ require (
|
|||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
|
google.golang.org/grpc v1.75.0
|
||||||
|
google.golang.org/protobuf v1.36.6
|
||||||
gorm.io/driver/mysql v1.5.7
|
gorm.io/driver/mysql v1.5.7
|
||||||
gorm.io/driver/sqlite v1.5.7
|
gorm.io/driver/sqlite v1.5.7
|
||||||
gorm.io/gorm v1.26.1
|
gorm.io/gorm v1.26.1
|
||||||
@@ -20,11 +22,11 @@ require (
|
|||||||
|
|
||||||
// Misuse of ServerConfig.PublicKeyCallback may cause authorization bypass in golang.org/x/crypto #1
|
// Misuse of ServerConfig.PublicKeyCallback may cause authorization bypass in golang.org/x/crypto #1
|
||||||
// golang.org/x/crypto Vulnerable to Denial of Service (DoS) via Slow or Incomplete Key Exchange #3
|
// golang.org/x/crypto Vulnerable to Denial of Service (DoS) via Slow or Incomplete Key Exchange #3
|
||||||
require golang.org/x/crypto v0.37.0 // indirect
|
require golang.org/x/crypto v0.39.0 // indirect
|
||||||
|
|
||||||
// HTTP Proxy bypass using IPv6 Zone IDs in golang.org/x/net #2
|
// HTTP Proxy bypass using IPv6 Zone IDs in golang.org/x/net #2
|
||||||
// golang.org/x/net vulnerable to Cross-site Scripting #4
|
// golang.org/x/net vulnerable to Cross-site Scripting #4
|
||||||
require golang.org/x/net v0.39.0 // indirect
|
require golang.org/x/net v0.41.0 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
@@ -35,7 +37,7 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
@@ -59,10 +61,10 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
golang.org/x/arch v0.15.0 // indirect
|
golang.org/x/arch v0.15.0 // indirect
|
||||||
golang.org/x/oauth2 v0.28.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
52
go.sum
52
go.sum
@@ -23,8 +23,12 @@ github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E
|
|||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -38,8 +42,10 @@ github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRj
|
|||||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -111,19 +117,37 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
|
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||||
|
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
@@ -2,6 +2,7 @@ package ws
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/komari-monitor/komari/common"
|
"github.com/komari-monitor/komari/common"
|
||||||
@@ -11,7 +12,13 @@ var (
|
|||||||
connectedClients = make(map[string]*SafeConn)
|
connectedClients = make(map[string]*SafeConn)
|
||||||
ConnectedUsers = []*websocket.Conn{}
|
ConnectedUsers = []*websocket.Conn{}
|
||||||
latestReport = make(map[string]*common.Report)
|
latestReport = make(map[string]*common.Report)
|
||||||
mu = sync.RWMutex{}
|
// presenceOnly stores online state for non-WebSocket agents (e.g., Nezha gRPC)
|
||||||
|
// value keeps connectionID and a soft expiration to avoid flicker
|
||||||
|
presenceOnly = make(map[string]struct {
|
||||||
|
id int64
|
||||||
|
expire time.Time
|
||||||
|
})
|
||||||
|
mu = sync.RWMutex{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetConnectedClients() map[string]*SafeConn {
|
func GetConnectedClients() map[string]*SafeConn {
|
||||||
@@ -44,6 +51,57 @@ func DeleteConnectedClients(uuid string) {
|
|||||||
// 只从 map 中删除,不再负责关闭连接
|
// 只从 map 中删除,不再负责关闭连接
|
||||||
delete(connectedClients, uuid)
|
delete(connectedClients, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPresence sets or clears presence for non-WebSocket agents.
|
||||||
|
// When present=false, it only clears if the connectionID matches current one.
|
||||||
|
// KeepAlivePresence sets presence with TTL for non-WebSocket agents.
|
||||||
|
func KeepAlivePresence(uuid string, connectionID int64, ttl time.Duration) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
presenceOnly[uuid] = struct {
|
||||||
|
id int64
|
||||||
|
expire time.Time
|
||||||
|
}{id: connectionID, expire: time.Now().Add(ttl)}
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPresenceTTL = 20 * time.Second
|
||||||
|
|
||||||
|
// SetPresence keeps compatibility with existing callers.
|
||||||
|
func SetPresence(uuid string, connectionID int64, present bool) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if present {
|
||||||
|
presenceOnly[uuid] = struct {
|
||||||
|
id int64
|
||||||
|
expire time.Time
|
||||||
|
}{id: connectionID, expire: time.Now().Add(defaultPresenceTTL)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cur, ok := presenceOnly[uuid]; ok && cur.id == connectionID {
|
||||||
|
delete(presenceOnly, uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllOnlineUUIDs returns a de-duplicated list of online UUIDs from both WebSocket and non-WebSocket agents.
|
||||||
|
func GetAllOnlineUUIDs() []string {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
set := make(map[string]struct{})
|
||||||
|
for k := range connectedClients {
|
||||||
|
set[k] = struct{}{}
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
for k, v := range presenceOnly {
|
||||||
|
if v.expire.After(now) {
|
||||||
|
set[k] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res := make([]string, 0, len(set))
|
||||||
|
for k := range set {
|
||||||
|
res = append(res, k)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
func GetLatestReport() map[string]*common.Report {
|
func GetLatestReport() map[string]*common.Report {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
defer mu.RUnlock()
|
defer mu.RUnlock()
|
||||||
|
Reference in New Issue
Block a user