diff --git a/requirements.txt b/requirements.txt index 4e56211..53904d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,4 @@ grpcio grpcio-tools<2.0.0 -protobuf==5.28.3 \ No newline at end of file +protobuf==5.29.0 diff --git a/test2.py b/test2.py new file mode 100644 index 0000000..b826020 --- /dev/null +++ b/test2.py @@ -0,0 +1,7 @@ +from xtlsapi import XrayClient + + +xray = XrayClient("127.0.0.1", 53357) + + +print(xray.stats_online_ip_list("1")) diff --git a/tests/test_stats_online_ip_list.py b/tests/test_stats_online_ip_list.py new file mode 100644 index 0000000..9c6d8f2 --- /dev/null +++ b/tests/test_stats_online_ip_list.py @@ -0,0 +1,9 @@ +import pytest +from unittest.mock import patch +from xtlsapi import XrayClient + +def test_stats_online_ip_list(): + xray = XrayClient("127.0.0.1", 53357) + with patch.object(xray, 'stats_online_ip_list', return_value={'212.58.119.246': 1763418412}): + result = xray.stats_online_ip_list("1") + assert result == {'212.58.119.246': 1763418412} diff --git a/xtlsapi/api_services/stats/__init__.py b/xtlsapi/api_services/stats/__init__.py index 49040af..8d9d67e 100644 --- a/xtlsapi/api_services/stats/__init__.py +++ b/xtlsapi/api_services/stats/__init__.py @@ -6,6 +6,7 @@ from .get_total_download_traffic import GetTotalDownloadTraffic from .get_statsquery import StatsQuery from .get_statsonline import StatsOnline +from .get_stats_online_ip_list import StatsOnlineIpList class StatsAPIService( GetClientUploadTraffic, @@ -15,6 +16,7 @@ class StatsAPIService( GetTotalUploadTraffic, GetTotalDownloadTraffic, StatsQuery, - StatsOnline + StatsOnline, + StatsOnlineIpList ): pass diff --git a/xtlsapi/api_services/stats/get_stats_online_ip_list.py b/xtlsapi/api_services/stats/get_stats_online_ip_list.py new file mode 100644 index 0000000..eaab96d --- /dev/null +++ b/xtlsapi/api_services/stats/get_stats_online_ip_list.py @@ -0,0 +1,28 @@ +import grpc +from xtlsapi.xray_api.app.stats.command import command_pb2 + +from .._base import BaseService + + +class StatsOnlineIpList(BaseService): + def stats_online_ip_list(self, user, reset=False) -> dict: + """ + Returns a dictionary of online IPs for the given user. + + Args: + user: The email of the user to query. + + Returns: + A dictionary mapping IP addresses to last cleanup time of expired ip's. + """ + + try: + return self.stats_stub.GetStatsOnlineIpList( + command_pb2.GetStatsRequest( + name=f"user>>>{user}>>>online", + reset=reset + ) + ).ips + except grpc.RpcError: + raise + return None diff --git a/xtlsapi/xray_api/app/stats/command/command.proto b/xtlsapi/xray_api/app/stats/command/command.proto new file mode 100644 index 0000000..58ed737 --- /dev/null +++ b/xtlsapi/xray_api/app/stats/command/command.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +package xray.app.stats.command; +option csharp_namespace = "Xray.App.Stats.Command"; +option go_package = "github.com/xtls/xray-core/app/stats/command"; +option java_package = "com.xray.app.stats.command"; +option java_multiple_files = true; + +message GetStatsRequest { + // Name of the stat counter. + string name = 1; + // Whether or not to reset the counter to fetching its value. + bool reset = 2; +} + +message Stat { + string name = 1; + int64 value = 2; +} + +message GetStatsResponse { + Stat stat = 1; +} + +message QueryStatsRequest { + string pattern = 1; + bool reset = 2; +} + +message QueryStatsResponse { + repeated Stat stat = 1; +} + +message SysStatsRequest {} + +message SysStatsResponse { + uint32 NumGoroutine = 1; + uint32 NumGC = 2; + uint64 Alloc = 3; + uint64 TotalAlloc = 4; + uint64 Sys = 5; + uint64 Mallocs = 6; + uint64 Frees = 7; + uint64 LiveObjects = 8; + uint64 PauseTotalNs = 9; + uint32 Uptime = 10; +} + +message GetStatsOnlineIpListResponse { + string name = 1; + map ips = 2; +} + +service StatsService { + rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {} + rpc GetStatsOnline(GetStatsRequest) returns (GetStatsResponse) {} + rpc QueryStats(QueryStatsRequest) returns (QueryStatsResponse) {} + rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {} + rpc GetStatsOnlineIpList(GetStatsRequest) returns (GetStatsOnlineIpListResponse) {} +} + +message Config {} diff --git a/xtlsapi/xray_api/app/stats/command/command_pb2.py b/xtlsapi/xray_api/app/stats/command/command_pb2.py index 3eaa093..6fc5afd 100644 --- a/xtlsapi/xray_api/app/stats/command/command_pb2.py +++ b/xtlsapi/xray_api/app/stats/command/command_pb2.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE -# source: app/stats/command/command.proto -# Protobuf Python Version: 5.28.3 +# source: command.proto +# Protobuf Python Version: 5.29.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -12,10 +12,10 @@ _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, 5, - 28, - 3, + 29, + 0, '', - 'app/stats/command/command.proto' + 'command.proto' ) # @@protoc_insertion_point(imports) @@ -24,30 +24,36 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x61pp/stats/command/command.proto\x12\x16xray.app.stats.command\".\n\x0fGetStatsRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"#\n\x04Stat\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03\">\n\x10GetStatsResponse\x12*\n\x04stat\x18\x01 \x01(\x0b\x32\x1c.xray.app.stats.command.Stat\"3\n\x11QueryStatsRequest\x12\x0f\n\x07pattern\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"@\n\x12QueryStatsResponse\x12*\n\x04stat\x18\x01 \x03(\x0b\x32\x1c.xray.app.stats.command.Stat\"\x11\n\x0fSysStatsRequest\"\xc2\x01\n\x10SysStatsResponse\x12\x14\n\x0cNumGoroutine\x18\x01 \x01(\r\x12\r\n\x05NumGC\x18\x02 \x01(\r\x12\r\n\x05\x41lloc\x18\x03 \x01(\x04\x12\x12\n\nTotalAlloc\x18\x04 \x01(\x04\x12\x0b\n\x03Sys\x18\x05 \x01(\x04\x12\x0f\n\x07Mallocs\x18\x06 \x01(\x04\x12\r\n\x05\x46rees\x18\x07 \x01(\x04\x12\x13\n\x0bLiveObjects\x18\x08 \x01(\x04\x12\x14\n\x0cPauseTotalNs\x18\t \x01(\x04\x12\x0e\n\x06Uptime\x18\n \x01(\r\"\x08\n\x06\x43onfig2\xa1\x03\n\x0cStatsService\x12_\n\x08GetStats\x12\'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12\x65\n\x0eGetStatsOnline\x12\'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12\x65\n\nQueryStats\x12).xray.app.stats.command.QueryStatsRequest\x1a*.xray.app.stats.command.QueryStatsResponse\"\x00\x12\x62\n\x0bGetSysStats\x12\'.xray.app.stats.command.SysStatsRequest\x1a(.xray.app.stats.command.SysStatsResponse\"\x00\x42\x64\n\x1a\x63om.xray.app.stats.commandP\x01Z+github.com/xtls/xray-core/app/stats/command\xaa\x02\x16Xray.App.Stats.Commandb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rcommand.proto\x12\x16xray.app.stats.command\".\n\x0fGetStatsRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"#\n\x04Stat\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03\">\n\x10GetStatsResponse\x12*\n\x04stat\x18\x01 \x01(\x0b\x32\x1c.xray.app.stats.command.Stat\"3\n\x11QueryStatsRequest\x12\x0f\n\x07pattern\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"@\n\x12QueryStatsResponse\x12*\n\x04stat\x18\x01 \x03(\x0b\x32\x1c.xray.app.stats.command.Stat\"\x11\n\x0fSysStatsRequest\"\xc2\x01\n\x10SysStatsResponse\x12\x14\n\x0cNumGoroutine\x18\x01 \x01(\r\x12\r\n\x05NumGC\x18\x02 \x01(\r\x12\r\n\x05\x41lloc\x18\x03 \x01(\x04\x12\x12\n\nTotalAlloc\x18\x04 \x01(\x04\x12\x0b\n\x03Sys\x18\x05 \x01(\x04\x12\x0f\n\x07Mallocs\x18\x06 \x01(\x04\x12\r\n\x05\x46rees\x18\x07 \x01(\x04\x12\x13\n\x0bLiveObjects\x18\x08 \x01(\x04\x12\x14\n\x0cPauseTotalNs\x18\t \x01(\x04\x12\x0e\n\x06Uptime\x18\n \x01(\r\"\xa4\x01\n\x1cGetStatsOnlineIpListResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12J\n\x03ips\x18\x02 \x03(\x0b\x32=.xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry\x1a*\n\x08IpsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03:\x02\x38\x01\"\x08\n\x06\x43onfig2\x9a\x04\n\x0cStatsService\x12_\n\x08GetStats\x12\'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12\x65\n\x0eGetStatsOnline\x12\'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12\x65\n\nQueryStats\x12).xray.app.stats.command.QueryStatsRequest\x1a*.xray.app.stats.command.QueryStatsResponse\"\x00\x12\x62\n\x0bGetSysStats\x12\'.xray.app.stats.command.SysStatsRequest\x1a(.xray.app.stats.command.SysStatsResponse\"\x00\x12w\n\x14GetStatsOnlineIpList\x12\'.xray.app.stats.command.GetStatsRequest\x1a\x34.xray.app.stats.command.GetStatsOnlineIpListResponse\"\x00\x42\x64\n\x1a\x63om.xray.app.stats.commandP\x01Z+github.com/xtls/xray-core/app/stats/command\xaa\x02\x16Xray.App.Stats.Commandb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'app.stats.command.command_pb2', _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'command_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\032com.xray.app.stats.commandP\001Z+github.com/xtls/xray-core/app/stats/command\252\002\026Xray.App.Stats.Command' - _globals['_GETSTATSREQUEST']._serialized_start=59 - _globals['_GETSTATSREQUEST']._serialized_end=105 - _globals['_STAT']._serialized_start=107 - _globals['_STAT']._serialized_end=142 - _globals['_GETSTATSRESPONSE']._serialized_start=144 - _globals['_GETSTATSRESPONSE']._serialized_end=206 - _globals['_QUERYSTATSREQUEST']._serialized_start=208 - _globals['_QUERYSTATSREQUEST']._serialized_end=259 - _globals['_QUERYSTATSRESPONSE']._serialized_start=261 - _globals['_QUERYSTATSRESPONSE']._serialized_end=325 - _globals['_SYSSTATSREQUEST']._serialized_start=327 - _globals['_SYSSTATSREQUEST']._serialized_end=344 - _globals['_SYSSTATSRESPONSE']._serialized_start=347 - _globals['_SYSSTATSRESPONSE']._serialized_end=541 - _globals['_CONFIG']._serialized_start=543 - _globals['_CONFIG']._serialized_end=551 - _globals['_STATSSERVICE']._serialized_start=554 - _globals['_STATSSERVICE']._serialized_end=971 + _globals['_GETSTATSONLINEIPLISTRESPONSE_IPSENTRY']._loaded_options = None + _globals['_GETSTATSONLINEIPLISTRESPONSE_IPSENTRY']._serialized_options = b'8\001' + _globals['_GETSTATSREQUEST']._serialized_start=41 + _globals['_GETSTATSREQUEST']._serialized_end=87 + _globals['_STAT']._serialized_start=89 + _globals['_STAT']._serialized_end=124 + _globals['_GETSTATSRESPONSE']._serialized_start=126 + _globals['_GETSTATSRESPONSE']._serialized_end=188 + _globals['_QUERYSTATSREQUEST']._serialized_start=190 + _globals['_QUERYSTATSREQUEST']._serialized_end=241 + _globals['_QUERYSTATSRESPONSE']._serialized_start=243 + _globals['_QUERYSTATSRESPONSE']._serialized_end=307 + _globals['_SYSSTATSREQUEST']._serialized_start=309 + _globals['_SYSSTATSREQUEST']._serialized_end=326 + _globals['_SYSSTATSRESPONSE']._serialized_start=329 + _globals['_SYSSTATSRESPONSE']._serialized_end=523 + _globals['_GETSTATSONLINEIPLISTRESPONSE']._serialized_start=526 + _globals['_GETSTATSONLINEIPLISTRESPONSE']._serialized_end=690 + _globals['_GETSTATSONLINEIPLISTRESPONSE_IPSENTRY']._serialized_start=648 + _globals['_GETSTATSONLINEIPLISTRESPONSE_IPSENTRY']._serialized_end=690 + _globals['_CONFIG']._serialized_start=692 + _globals['_CONFIG']._serialized_end=700 + _globals['_STATSSERVICE']._serialized_start=703 + _globals['_STATSSERVICE']._serialized_end=1241 # @@protoc_insertion_point(module_scope) diff --git a/xtlsapi/xray_api/app/stats/command/command_pb2_grpc.py b/xtlsapi/xray_api/app/stats/command/command_pb2_grpc.py index a42dfeb..f31e858 100644 --- a/xtlsapi/xray_api/app/stats/command/command_pb2_grpc.py +++ b/xtlsapi/xray_api/app/stats/command/command_pb2_grpc.py @@ -3,9 +3,9 @@ import grpc import warnings -from xtlsapi.xray_api.app.stats.command import command_pb2 as app_dot_stats_dot_command_dot_command__pb2 +from . import command_pb2 as command__pb2 -GRPC_GENERATED_VERSION = '1.67.1' +GRPC_GENERATED_VERSION = '1.71.2' GRPC_VERSION = grpc.__version__ _version_not_supported = False @@ -18,7 +18,7 @@ if _version_not_supported: raise RuntimeError( f'The grpc package installed is at version {GRPC_VERSION},' - + f' but the generated code in app/stats/command/command_pb2_grpc.py depends on' + + f' but the generated code in command_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' @@ -36,23 +36,28 @@ def __init__(self, channel): """ self.GetStats = channel.unary_unary( '/xray.app.stats.command.StatsService/GetStats', - request_serializer=app_dot_stats_dot_command_dot_command__pb2.GetStatsRequest.SerializeToString, - response_deserializer=app_dot_stats_dot_command_dot_command__pb2.GetStatsResponse.FromString, + request_serializer=command__pb2.GetStatsRequest.SerializeToString, + response_deserializer=command__pb2.GetStatsResponse.FromString, _registered_method=True) self.GetStatsOnline = channel.unary_unary( '/xray.app.stats.command.StatsService/GetStatsOnline', - request_serializer=app_dot_stats_dot_command_dot_command__pb2.GetStatsRequest.SerializeToString, - response_deserializer=app_dot_stats_dot_command_dot_command__pb2.GetStatsResponse.FromString, + request_serializer=command__pb2.GetStatsRequest.SerializeToString, + response_deserializer=command__pb2.GetStatsResponse.FromString, _registered_method=True) self.QueryStats = channel.unary_unary( '/xray.app.stats.command.StatsService/QueryStats', - request_serializer=app_dot_stats_dot_command_dot_command__pb2.QueryStatsRequest.SerializeToString, - response_deserializer=app_dot_stats_dot_command_dot_command__pb2.QueryStatsResponse.FromString, + request_serializer=command__pb2.QueryStatsRequest.SerializeToString, + response_deserializer=command__pb2.QueryStatsResponse.FromString, _registered_method=True) self.GetSysStats = channel.unary_unary( '/xray.app.stats.command.StatsService/GetSysStats', - request_serializer=app_dot_stats_dot_command_dot_command__pb2.SysStatsRequest.SerializeToString, - response_deserializer=app_dot_stats_dot_command_dot_command__pb2.SysStatsResponse.FromString, + request_serializer=command__pb2.SysStatsRequest.SerializeToString, + response_deserializer=command__pb2.SysStatsResponse.FromString, + _registered_method=True) + self.GetStatsOnlineIpList = channel.unary_unary( + '/xray.app.stats.command.StatsService/GetStatsOnlineIpList', + request_serializer=command__pb2.GetStatsRequest.SerializeToString, + response_deserializer=command__pb2.GetStatsOnlineIpListResponse.FromString, _registered_method=True) @@ -83,28 +88,39 @@ def GetSysStats(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GetStatsOnlineIpList(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_StatsServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'GetStats': grpc.unary_unary_rpc_method_handler( servicer.GetStats, - request_deserializer=app_dot_stats_dot_command_dot_command__pb2.GetStatsRequest.FromString, - response_serializer=app_dot_stats_dot_command_dot_command__pb2.GetStatsResponse.SerializeToString, + request_deserializer=command__pb2.GetStatsRequest.FromString, + response_serializer=command__pb2.GetStatsResponse.SerializeToString, ), 'GetStatsOnline': grpc.unary_unary_rpc_method_handler( servicer.GetStatsOnline, - request_deserializer=app_dot_stats_dot_command_dot_command__pb2.GetStatsRequest.FromString, - response_serializer=app_dot_stats_dot_command_dot_command__pb2.GetStatsResponse.SerializeToString, + request_deserializer=command__pb2.GetStatsRequest.FromString, + response_serializer=command__pb2.GetStatsResponse.SerializeToString, ), 'QueryStats': grpc.unary_unary_rpc_method_handler( servicer.QueryStats, - request_deserializer=app_dot_stats_dot_command_dot_command__pb2.QueryStatsRequest.FromString, - response_serializer=app_dot_stats_dot_command_dot_command__pb2.QueryStatsResponse.SerializeToString, + request_deserializer=command__pb2.QueryStatsRequest.FromString, + response_serializer=command__pb2.QueryStatsResponse.SerializeToString, ), 'GetSysStats': grpc.unary_unary_rpc_method_handler( servicer.GetSysStats, - request_deserializer=app_dot_stats_dot_command_dot_command__pb2.SysStatsRequest.FromString, - response_serializer=app_dot_stats_dot_command_dot_command__pb2.SysStatsResponse.SerializeToString, + request_deserializer=command__pb2.SysStatsRequest.FromString, + response_serializer=command__pb2.SysStatsResponse.SerializeToString, + ), + 'GetStatsOnlineIpList': grpc.unary_unary_rpc_method_handler( + servicer.GetStatsOnlineIpList, + request_deserializer=command__pb2.GetStatsRequest.FromString, + response_serializer=command__pb2.GetStatsOnlineIpListResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( @@ -132,8 +148,8 @@ def GetStats(request, request, target, '/xray.app.stats.command.StatsService/GetStats', - app_dot_stats_dot_command_dot_command__pb2.GetStatsRequest.SerializeToString, - app_dot_stats_dot_command_dot_command__pb2.GetStatsResponse.FromString, + command__pb2.GetStatsRequest.SerializeToString, + command__pb2.GetStatsResponse.FromString, options, channel_credentials, insecure, @@ -159,8 +175,8 @@ def GetStatsOnline(request, request, target, '/xray.app.stats.command.StatsService/GetStatsOnline', - app_dot_stats_dot_command_dot_command__pb2.GetStatsRequest.SerializeToString, - app_dot_stats_dot_command_dot_command__pb2.GetStatsResponse.FromString, + command__pb2.GetStatsRequest.SerializeToString, + command__pb2.GetStatsResponse.FromString, options, channel_credentials, insecure, @@ -186,8 +202,8 @@ def QueryStats(request, request, target, '/xray.app.stats.command.StatsService/QueryStats', - app_dot_stats_dot_command_dot_command__pb2.QueryStatsRequest.SerializeToString, - app_dot_stats_dot_command_dot_command__pb2.QueryStatsResponse.FromString, + command__pb2.QueryStatsRequest.SerializeToString, + command__pb2.QueryStatsResponse.FromString, options, channel_credentials, insecure, @@ -213,8 +229,35 @@ def GetSysStats(request, request, target, '/xray.app.stats.command.StatsService/GetSysStats', - app_dot_stats_dot_command_dot_command__pb2.SysStatsRequest.SerializeToString, - app_dot_stats_dot_command_dot_command__pb2.SysStatsResponse.FromString, + command__pb2.SysStatsRequest.SerializeToString, + command__pb2.SysStatsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetStatsOnlineIpList(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/xray.app.stats.command.StatsService/GetStatsOnlineIpList', + command__pb2.GetStatsRequest.SerializeToString, + command__pb2.GetStatsOnlineIpListResponse.FromString, options, channel_credentials, insecure,