package services import ( "context" "errors" "fmt" "github.com/TeaOSLab/EdgeAPI/internal/db/models" "github.com/TeaOSLab/EdgeAPI/internal/remotelogs" rpcutils "github.com/TeaOSLab/EdgeAPI/internal/rpc/utils" "github.com/TeaOSLab/EdgeCommon/pkg/iplibrary" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/iwind/TeaGo/Tea" "os" "path/filepath" "strings" ) // IPLibraryService IP库服务 type IPLibraryService struct { BaseService } // CreateIPLibrary 创建IP库 func (this *IPLibraryService) CreateIPLibrary(ctx context.Context, req *pb.CreateIPLibraryRequest) (*pb.CreateIPLibraryResponse, error) { // 校验请求 _, _, _, err := rpcutils.ValidateRequest(ctx, rpcutils.UserTypeAdmin) if err != nil { return nil, err } var tx = this.NullTx() ipLibraryId, err := models.SharedIPLibraryDAO.CreateIPLibrary(tx, req.Type, req.FileId) if err != nil { return nil, err } return &pb.CreateIPLibraryResponse{ IpLibraryId: ipLibraryId, }, nil } // FindEnabledIPLibrary 查找单个IP库 func (this *IPLibraryService) FindEnabledIPLibrary(ctx context.Context, req *pb.FindEnabledIPLibraryRequest) (*pb.FindEnabledIPLibraryResponse, error) { // 校验请求 _, _, _, err := rpcutils.ValidateRequest(ctx, rpcutils.UserTypeAdmin) if err != nil { return nil, err } var tx = this.NullTx() ipLibrary, err := models.SharedIPLibraryDAO.FindEnabledIPLibrary(tx, req.IpLibraryId) if err != nil { return nil, err } if ipLibrary == nil { return &pb.FindEnabledIPLibraryResponse{IpLibrary: nil}, nil } // 文件相关 var pbFile *pb.File = nil file, err := models.SharedFileDAO.FindEnabledFile(tx, int64(ipLibrary.FileId)) if err != nil { return nil, err } if file != nil { pbFile = &pb.File{ Id: int64(file.Id), Filename: file.Filename, Size: int64(file.Size), } } return &pb.FindEnabledIPLibraryResponse{ IpLibrary: &pb.IPLibrary{ Id: int64(ipLibrary.Id), Type: ipLibrary.Type, File: pbFile, CreatedAt: int64(ipLibrary.CreatedAt), }, }, nil } // FindLatestIPLibraryWithType 查找最新的IP库 func (this *IPLibraryService) FindLatestIPLibraryWithType(ctx context.Context, req *pb.FindLatestIPLibraryWithTypeRequest) (*pb.FindLatestIPLibraryWithTypeResponse, error) { // 校验请求 _, _, _, err := rpcutils.ValidateRequest(ctx, rpcutils.UserTypeNode) if err != nil { return nil, err } var tx = this.NullTx() ipLibrary, err := models.SharedIPLibraryDAO.FindLatestIPLibraryWithType(tx, req.Type) if err != nil { return nil, err } if ipLibrary == nil { return &pb.FindLatestIPLibraryWithTypeResponse{IpLibrary: nil}, nil } // 文件相关 var pbFile *pb.File = nil file, err := models.SharedFileDAO.FindEnabledFile(tx, int64(ipLibrary.FileId)) if err != nil { return nil, err } if file != nil { pbFile = &pb.File{ Id: int64(file.Id), Filename: file.Filename, Size: int64(file.Size), } } return &pb.FindLatestIPLibraryWithTypeResponse{ IpLibrary: &pb.IPLibrary{ Id: int64(ipLibrary.Id), Type: ipLibrary.Type, File: pbFile, CreatedAt: int64(ipLibrary.CreatedAt), }, }, nil } // FindAllEnabledIPLibrariesWithType 列出某个类型的所有IP库 func (this *IPLibraryService) FindAllEnabledIPLibrariesWithType(ctx context.Context, req *pb.FindAllEnabledIPLibrariesWithTypeRequest) (*pb.FindAllEnabledIPLibrariesWithTypeResponse, error) { // 校验请求 _, _, _, err := rpcutils.ValidateRequest(ctx, rpcutils.UserTypeAdmin) if err != nil { return nil, err } var tx = this.NullTx() ipLibraries, err := models.SharedIPLibraryDAO.FindAllEnabledIPLibrariesWithType(tx, req.Type) if err != nil { return nil, err } result := []*pb.IPLibrary{} for _, library := range ipLibraries { // 文件相关 var pbFile *pb.File = nil file, err := models.SharedFileDAO.FindEnabledFile(tx, int64(library.FileId)) if err != nil { return nil, err } if file != nil { pbFile = &pb.File{ Id: int64(file.Id), Filename: file.Filename, Size: int64(file.Size), } } result = append(result, &pb.IPLibrary{ Id: int64(library.Id), Type: library.Type, File: pbFile, CreatedAt: int64(library.CreatedAt), }) } return &pb.FindAllEnabledIPLibrariesWithTypeResponse{IpLibraries: result}, nil } // DeleteIPLibrary 删除IP库 func (this *IPLibraryService) DeleteIPLibrary(ctx context.Context, req *pb.DeleteIPLibraryRequest) (*pb.RPCSuccess, error) { // 校验请求 _, err := this.ValidateAdmin(ctx) if err != nil { return nil, err } var tx = this.NullTx() err = models.SharedIPLibraryDAO.DisableIPLibrary(tx, req.IpLibraryId) if err != nil { return nil, err } return this.Success() } // LookupIPRegion 查询某个IP信息 func (this *IPLibraryService) LookupIPRegion(ctx context.Context, req *pb.LookupIPRegionRequest) (*pb.LookupIPRegionResponse, error) { // 校验请求 _, _, err := this.ValidateAdminAndUser(ctx, true) if err != nil { return nil, err } var result = iplibrary.LookupIP(req.Ip) if result == nil || !result.IsOk() { return &pb.LookupIPRegionResponse{IpRegion: nil}, nil } return &pb.LookupIPRegionResponse{IpRegion: &pb.IPRegion{ Country: result.CountryName(), Region: "", Province: result.ProvinceName(), City: result.CityName(), Isp: result.ProviderName(), CountryId: result.CountryId(), ProvinceId: result.ProvinceId(), CityId: result.CityId(), TownId: result.TownId(), ProviderId: result.ProviderId(), Summary: result.Summary(), }}, nil } // LookupIPRegions 查询一组IP信息 func (this *IPLibraryService) LookupIPRegions(ctx context.Context, req *pb.LookupIPRegionsRequest) (*pb.LookupIPRegionsResponse, error) { // 校验请求 _, _, err := this.ValidateAdminAndUser(ctx, true) if err != nil { return nil, err } var result = map[string]*pb.IPRegion{} if len(req.IpList) > 0 { for _, ip := range req.IpList { var info = iplibrary.LookupIP(ip) if info != nil && info.IsOk() { result[ip] = &pb.IPRegion{ Country: info.CountryName(), Region: "", Province: info.ProvinceName(), City: info.CityName(), Isp: info.ProviderName(), CountryId: info.CountryId(), ProvinceId: info.ProvinceId(), CityId: info.CityId(), TownId: info.TownId(), ProviderId: info.ProviderId(), Summary: info.Summary(), } } } } return &pb.LookupIPRegionsResponse{IpRegionMap: result}, nil } // ReloadIPLibrary 重新加载IP库 func (this *IPLibraryService) ReloadIPLibrary(ctx context.Context, req *pb.ReloadIPLibraryRequest) (*pb.RPCSuccess, error) { // 校验请求 _, err := this.ValidateAdmin(ctx) if err != nil { return nil, err } // 重新加载IP库 err = iplibrary.InitDefault() if err != nil { return nil, err } return this.Success() } // UploadMaxMindFile 上传MaxMind文件到EdgeAPI func (this *IPLibraryService) UploadMaxMindFile(ctx context.Context, req *pb.UploadMaxMindFileRequest) (*pb.RPCSuccess, error) { // 校验请求 _, err := this.ValidateAdmin(ctx) if err != nil { return nil, err } if len(req.Filename) == 0 || len(req.Data) == 0 { return nil, errors.New("filename and data are required") } // 检查文件名 filename := strings.ToLower(req.Filename) if !strings.HasSuffix(filename, ".mmdb") { return nil, errors.New("only MaxMind format files (.mmdb) are supported") } // 确定目标路径 iplibDir := Tea.Root + "/data/iplibrary" err = os.MkdirAll(iplibDir, 0755) if err != nil { return nil, fmt.Errorf("create IP library directory failed: %w", err) } var targetPath string if strings.Contains(filename, "city") { targetPath = filepath.Join(iplibDir, "maxmind-city.mmdb") } else if strings.Contains(filename, "asn") { targetPath = filepath.Join(iplibDir, "maxmind-asn.mmdb") } else { return nil, errors.New("MaxMind filename must contain 'city' or 'asn'") } // 保存文件(使用临时文件原子替换) tmpPath := targetPath + ".tmp" err = os.WriteFile(tmpPath, req.Data, 0644) if err != nil { return nil, fmt.Errorf("save IP library file failed: %w", err) } // 原子替换 err = os.Rename(tmpPath, targetPath) if err != nil { os.Remove(tmpPath) return nil, fmt.Errorf("replace IP library file failed: %w", err) } // 重新加载IP库 err = iplibrary.InitDefault() if err != nil { return nil, fmt.Errorf("reload IP library failed: %w", err) } return this.Success() } // FindMaxMindFileStatus 查询MaxMind文件状态 func (this *IPLibraryService) FindMaxMindFileStatus(ctx context.Context, req *pb.FindMaxMindFileStatusRequest) (*pb.FindMaxMindFileStatusResponse, error) { // 校验请求 _, err := this.ValidateAdmin(ctx) if err != nil { return nil, err } // 检查EdgeAPI的data/iplibrary/目录 // 使用与 UploadMaxMindFile 相同的路径逻辑 iplibDir := Tea.Root + "/data/iplibrary" cityDBPath := filepath.Join(iplibDir, "maxmind-city.mmdb") asnDBPath := filepath.Join(iplibDir, "maxmind-asn.mmdb") cityExists := false asnExists := false // 检查文件是否存在 if stat, err := os.Stat(cityDBPath); err == nil && stat != nil && !stat.IsDir() { cityExists = true } // 文件不存在是正常情况(会使用嵌入的库),不需要记录错误 if stat, err := os.Stat(asnDBPath); err == nil && stat != nil && !stat.IsDir() { asnExists = true } // 检查是否使用了MaxMind库(通过测试查询来判断) testIP := "8.8.8.8" testResult := iplibrary.LookupIP(testIP) usingMaxMind := false if testResult != nil && testResult.IsOk() { // MaxMind库的特征:CountryId 和 ProvinceId 通常为 0(因为MaxMind不使用ID系统) // 同时有国家名称,说明查询成功 if testResult.CountryId() == 0 && len(testResult.CountryName()) > 0 { usingMaxMind = true } } usingEmbeddedMaxMind := usingMaxMind && !cityExists return &pb.FindMaxMindFileStatusResponse{ CityExists: cityExists, AsnExists: asnExists, UsingMaxMind: usingMaxMind, UsingEmbeddedMaxMind: usingEmbeddedMaxMind, }, nil } // DeleteMaxMindFile 删除MaxMind文件 func (this *IPLibraryService) DeleteMaxMindFile(ctx context.Context, req *pb.DeleteMaxMindFileRequest) (*pb.RPCSuccess, error) { // 校验请求 _, err := this.ValidateAdmin(ctx) if err != nil { return nil, err } iplibDir := Tea.Root + "/data/iplibrary" cityDBPath := filepath.Join(iplibDir, "maxmind-city.mmdb") asnDBPath := filepath.Join(iplibDir, "maxmind-asn.mmdb") // 根据文件名删除对应的文件,如果为空则删除所有 filename := strings.ToLower(req.Filename) if len(filename) == 0 { // 删除所有文件 if err := os.Remove(cityDBPath); err != nil && !os.IsNotExist(err) { remotelogs.Error("IP_LIBRARY", "delete city file failed: "+err.Error()) } if err := os.Remove(asnDBPath); err != nil && !os.IsNotExist(err) { remotelogs.Error("IP_LIBRARY", "delete ASN file failed: "+err.Error()) } } else if strings.Contains(filename, "city") { // 只删除 City 文件 if err := os.Remove(cityDBPath); err != nil && !os.IsNotExist(err) { return nil, fmt.Errorf("delete city file failed: %w", err) } } else if strings.Contains(filename, "asn") { // 只删除 ASN 文件 if err := os.Remove(asnDBPath); err != nil && !os.IsNotExist(err) { return nil, fmt.Errorf("delete ASN file failed: %w", err) } } else { return nil, errors.New("filename must contain 'city' or 'asn', or be empty to delete all") } // 重新加载IP库(使用嵌入的默认库) err = iplibrary.InitDefault() if err != nil { remotelogs.Error("IP_LIBRARY", "reload IP library after deletion failed: "+err.Error()) // 不返回错误,因为文件已经删除成功 } return this.Success() }