ServerApiService.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. using System.Net;
  2. using System.Net.Http;
  3. using System.Net.Http.Json;
  4. using System.Net.Sockets;
  5. using System.Text.Json;
  6. using NetworkTool.Client.Models;
  7. namespace NetworkTool.Client.Services;
  8. public sealed class ServerApiService
  9. {
  10. private readonly JsonSerializerOptions _jsonOptions = new()
  11. {
  12. PropertyNameCaseInsensitive = true,
  13. };
  14. public async Task<HealthCheckResult> CheckHealthAsync(string baseAddress, string password, string localIPv4, CancellationToken cancellationToken = default)
  15. {
  16. try
  17. {
  18. using var handler = new SocketsHttpHandler();
  19. if (!string.IsNullOrWhiteSpace(localIPv4))
  20. {
  21. handler.ConnectCallback = async (context, token) =>
  22. {
  23. var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  24. socket.Bind(new IPEndPoint(IPAddress.Parse(localIPv4), 0));
  25. await socket.ConnectAsync(context.DnsEndPoint, token);
  26. return new NetworkStream(socket, ownsSocket: true);
  27. };
  28. }
  29. using var client = new HttpClient(handler) { BaseAddress = new Uri(baseAddress), Timeout = TimeSpan.FromSeconds(5) };
  30. client.DefaultRequestHeaders.Add("X-Admin-Password", password);
  31. using var response = await client.GetAsync("/api/health", cancellationToken);
  32. return new HealthCheckResult
  33. {
  34. Success = response.IsSuccessStatusCode,
  35. StatusCode = (int)response.StatusCode,
  36. Message = response.IsSuccessStatusCode
  37. ? "HTTP 健康检查通过。"
  38. : $"HTTP 健康检查返回状态码 {(int)response.StatusCode}。",
  39. };
  40. }
  41. catch (Exception ex)
  42. {
  43. return new HealthCheckResult
  44. {
  45. Success = false,
  46. StatusCode = null,
  47. Message = ex.Message,
  48. };
  49. }
  50. }
  51. public async Task<RemoteDeviceInfo?> GetDeviceInfoAsync(string baseAddress, string password, string localIPv4, CancellationToken cancellationToken = default)
  52. {
  53. try
  54. {
  55. using var client = CreateClient(baseAddress, password, localIPv4);
  56. using var response = await client.GetAsync("/api/device/info", cancellationToken);
  57. if (!response.IsSuccessStatusCode)
  58. {
  59. return null;
  60. }
  61. await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
  62. var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteDeviceInfo>>(stream, _jsonOptions, cancellationToken);
  63. return wrapper?.Data;
  64. }
  65. catch
  66. {
  67. return null;
  68. }
  69. }
  70. public async Task<RemoteInterfacesInfo?> GetInterfacesAsync(string baseAddress, string password, string localIPv4, CancellationToken cancellationToken = default)
  71. {
  72. try
  73. {
  74. using var client = CreateClient(baseAddress, password, localIPv4);
  75. using var response = await client.GetAsync("/api/network/interfaces", cancellationToken);
  76. if (!response.IsSuccessStatusCode)
  77. {
  78. return null;
  79. }
  80. await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
  81. var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteInterfacesInfo>>(stream, _jsonOptions, cancellationToken);
  82. return wrapper?.Data;
  83. }
  84. catch
  85. {
  86. return null;
  87. }
  88. }
  89. public async Task<ApiCallResult<RemoteInterfaceConfigsResponse>> GetInterfaceConfigsAsync(string baseAddress, string password, string localIPv4, CancellationToken cancellationToken = default)
  90. {
  91. try
  92. {
  93. using var client = CreateClient(baseAddress, password, localIPv4);
  94. using var response = await client.GetAsync("/api/network/configs", cancellationToken);
  95. var content = await response.Content.ReadAsStringAsync(cancellationToken);
  96. var wrapper = DeserializeEnvelope<RemoteInterfaceConfigsResponse>(content);
  97. if (wrapper is null)
  98. {
  99. return CreateInvalidJsonResult<RemoteInterfaceConfigsResponse>(response.StatusCode, content, "批量读取配置");
  100. }
  101. if (wrapper?.Data is null)
  102. {
  103. return new ApiCallResult<RemoteInterfaceConfigsResponse>
  104. {
  105. Success = false,
  106. StatusCode = (int)response.StatusCode,
  107. Message = "接口配置返回内容为空。",
  108. };
  109. }
  110. return new ApiCallResult<RemoteInterfaceConfigsResponse>
  111. {
  112. Success = response.IsSuccessStatusCode,
  113. StatusCode = (int)response.StatusCode,
  114. Message = wrapper.Message,
  115. Data = wrapper.Data,
  116. };
  117. }
  118. catch (Exception ex)
  119. {
  120. return new ApiCallResult<RemoteInterfaceConfigsResponse>
  121. {
  122. Success = false,
  123. Message = ex.Message,
  124. };
  125. }
  126. }
  127. public async Task<ApiCallResult<RemoteValidateResult>> ValidateInterfaceConfigAsync(string baseAddress, string password, string localIPv4, RemoteInterfaceConfig input, CancellationToken cancellationToken = default)
  128. {
  129. try
  130. {
  131. using var client = CreateClient(baseAddress, password, localIPv4);
  132. using var response = await client.PostAsJsonAsync("/api/network/validate", input, _jsonOptions, cancellationToken);
  133. await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
  134. var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteValidateResult>>(stream, _jsonOptions, cancellationToken);
  135. return new ApiCallResult<RemoteValidateResult>
  136. {
  137. Success = response.IsSuccessStatusCode && wrapper?.Data is not null,
  138. StatusCode = (int)response.StatusCode,
  139. Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "校验通过" : $"校验失败,HTTP 状态码 {(int)response.StatusCode}。"),
  140. Data = wrapper?.Data,
  141. };
  142. }
  143. catch (Exception ex)
  144. {
  145. return new ApiCallResult<RemoteValidateResult>
  146. {
  147. Success = false,
  148. Message = ex.Message,
  149. };
  150. }
  151. }
  152. public Task<ApiCallResult<RemoteValidateResult>> ValidateInterfaceConfigsAsync(string baseAddress, string password, string localIPv4, IReadOnlyList<RemoteInterfaceConfig> inputs, CancellationToken cancellationToken = default)
  153. {
  154. return ValidateInterfaceConfigsAsync(baseAddress, password, localIPv4, new RemoteInterfaceConfigsRequest { Configs = inputs }, cancellationToken);
  155. }
  156. private async Task<ApiCallResult<RemoteValidateResult>> ValidateInterfaceConfigsAsync(string baseAddress, string password, string localIPv4, RemoteInterfaceConfigsRequest input, CancellationToken cancellationToken = default)
  157. {
  158. try
  159. {
  160. using var client = CreateClient(baseAddress, password, localIPv4);
  161. using var response = await client.PostAsJsonAsync("/api/network/validate-all", input, _jsonOptions, cancellationToken);
  162. var content = await response.Content.ReadAsStringAsync(cancellationToken);
  163. var wrapper = DeserializeEnvelope<RemoteValidateResult>(content);
  164. if (wrapper is null)
  165. {
  166. return CreateInvalidJsonResult<RemoteValidateResult>(response.StatusCode, content, "批量校验");
  167. }
  168. return new ApiCallResult<RemoteValidateResult>
  169. {
  170. Success = response.IsSuccessStatusCode && wrapper?.Data is not null,
  171. StatusCode = (int)response.StatusCode,
  172. Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "校验通过" : $"校验失败,HTTP 状态码 {(int)response.StatusCode}。"),
  173. Data = wrapper?.Data,
  174. };
  175. }
  176. catch (Exception ex)
  177. {
  178. return new ApiCallResult<RemoteValidateResult>
  179. {
  180. Success = false,
  181. Message = ex.Message,
  182. };
  183. }
  184. }
  185. public async Task<ApiCallResult<RemoteApplyTaskResponse>> ApplyInterfaceConfigAsync(string baseAddress, string password, string localIPv4, RemoteInterfaceConfig input, CancellationToken cancellationToken = default)
  186. {
  187. try
  188. {
  189. using var client = CreateClient(baseAddress, password, localIPv4);
  190. using var response = await client.PostAsJsonAsync("/api/network/apply", input, _jsonOptions, cancellationToken);
  191. await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
  192. var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteApplyTaskResponse>>(stream, _jsonOptions, cancellationToken);
  193. return new ApiCallResult<RemoteApplyTaskResponse>
  194. {
  195. Success = response.IsSuccessStatusCode && wrapper?.Data is not null,
  196. StatusCode = (int)response.StatusCode,
  197. Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "配置任务已提交" : $"提交失败,HTTP 状态码 {(int)response.StatusCode}。"),
  198. Data = wrapper?.Data,
  199. };
  200. }
  201. catch (Exception ex)
  202. {
  203. return new ApiCallResult<RemoteApplyTaskResponse>
  204. {
  205. Success = false,
  206. Message = ex.Message,
  207. };
  208. }
  209. }
  210. public Task<ApiCallResult<RemoteApplyTaskResponse>> ApplyInterfaceConfigsAsync(string baseAddress, string password, string localIPv4, IReadOnlyList<RemoteInterfaceConfig> inputs, CancellationToken cancellationToken = default)
  211. {
  212. return ApplyInterfaceConfigsAsync(baseAddress, password, localIPv4, new RemoteInterfaceConfigsRequest { Configs = inputs }, cancellationToken);
  213. }
  214. private async Task<ApiCallResult<RemoteApplyTaskResponse>> ApplyInterfaceConfigsAsync(string baseAddress, string password, string localIPv4, RemoteInterfaceConfigsRequest input, CancellationToken cancellationToken = default)
  215. {
  216. try
  217. {
  218. using var client = CreateClient(baseAddress, password, localIPv4);
  219. using var response = await client.PostAsJsonAsync("/api/network/apply-all", input, _jsonOptions, cancellationToken);
  220. var content = await response.Content.ReadAsStringAsync(cancellationToken);
  221. var wrapper = DeserializeEnvelope<RemoteApplyTaskResponse>(content);
  222. if (wrapper is null)
  223. {
  224. return CreateInvalidJsonResult<RemoteApplyTaskResponse>(response.StatusCode, content, "批量应用");
  225. }
  226. return new ApiCallResult<RemoteApplyTaskResponse>
  227. {
  228. Success = response.IsSuccessStatusCode && wrapper?.Data is not null,
  229. StatusCode = (int)response.StatusCode,
  230. Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "配置任务已提交" : $"提交失败,HTTP 状态码 {(int)response.StatusCode}。"),
  231. Data = wrapper?.Data,
  232. };
  233. }
  234. catch (Exception ex)
  235. {
  236. return new ApiCallResult<RemoteApplyTaskResponse>
  237. {
  238. Success = false,
  239. Message = ex.Message,
  240. };
  241. }
  242. }
  243. public async Task<ApiCallResult<RemoteTaskResult>> GetTaskAsync(string baseAddress, string password, string localIPv4, string taskId, CancellationToken cancellationToken = default)
  244. {
  245. try
  246. {
  247. using var client = CreateClient(baseAddress, password, localIPv4);
  248. using var response = await client.GetAsync($"/api/tasks/{Uri.EscapeDataString(taskId)}", cancellationToken);
  249. await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
  250. var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteTaskResult>>(stream, _jsonOptions, cancellationToken);
  251. return new ApiCallResult<RemoteTaskResult>
  252. {
  253. Success = response.IsSuccessStatusCode && wrapper?.Data is not null,
  254. StatusCode = (int)response.StatusCode,
  255. Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "成功" : $"任务查询失败,HTTP 状态码 {(int)response.StatusCode}。"),
  256. Data = wrapper?.Data,
  257. };
  258. }
  259. catch (Exception ex)
  260. {
  261. return new ApiCallResult<RemoteTaskResult>
  262. {
  263. Success = false,
  264. Message = ex.Message,
  265. };
  266. }
  267. }
  268. public async Task<ApiCallResult<RemoteApplyTaskResponse>> ConfirmApplyTaskAsync(string baseAddress, string password, string localIPv4, string taskId, CancellationToken cancellationToken = default)
  269. {
  270. try
  271. {
  272. using var client = CreateClient(baseAddress, password, localIPv4);
  273. using var response = await client.PostAsJsonAsync("/api/network/apply/confirm", new { task_id = taskId }, _jsonOptions, cancellationToken);
  274. await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
  275. var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteApplyTaskResponse>>(stream, _jsonOptions, cancellationToken);
  276. return new ApiCallResult<RemoteApplyTaskResponse>
  277. {
  278. Success = response.IsSuccessStatusCode,
  279. StatusCode = (int)response.StatusCode,
  280. Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "已确认保留配置" : $"确认失败,HTTP 状态码 {(int)response.StatusCode}。"),
  281. Data = wrapper?.Data,
  282. };
  283. }
  284. catch (Exception ex)
  285. {
  286. return new ApiCallResult<RemoteApplyTaskResponse>
  287. {
  288. Success = false,
  289. Message = ex.Message,
  290. };
  291. }
  292. }
  293. public async Task<ApiCallResult<RemoteApplyTaskResponse>> CancelApplyTaskAsync(string baseAddress, string password, string localIPv4, string taskId, CancellationToken cancellationToken = default)
  294. {
  295. try
  296. {
  297. using var client = CreateClient(baseAddress, password, localIPv4);
  298. using var response = await client.PostAsJsonAsync("/api/network/apply/cancel", new { task_id = taskId }, _jsonOptions, cancellationToken);
  299. await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
  300. var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteApplyTaskResponse>>(stream, _jsonOptions, cancellationToken);
  301. return new ApiCallResult<RemoteApplyTaskResponse>
  302. {
  303. Success = response.IsSuccessStatusCode,
  304. StatusCode = (int)response.StatusCode,
  305. Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "已取消保留配置" : $"取消失败,HTTP 状态码 {(int)response.StatusCode}。"),
  306. Data = wrapper?.Data,
  307. };
  308. }
  309. catch (Exception ex)
  310. {
  311. return new ApiCallResult<RemoteApplyTaskResponse>
  312. {
  313. Success = false,
  314. Message = ex.Message,
  315. };
  316. }
  317. }
  318. public Task<ApiCallResult<RemoteSystemActionResponse>> RebootAsync(string baseAddress, string password, string localIPv4, CancellationToken cancellationToken = default)
  319. {
  320. return PostSystemActionAsync(baseAddress, password, localIPv4, "/api/system/reboot", cancellationToken);
  321. }
  322. public Task<ApiCallResult<RemoteSystemActionResponse>> ShutdownAsync(string baseAddress, string password, string localIPv4, CancellationToken cancellationToken = default)
  323. {
  324. return PostSystemActionAsync(baseAddress, password, localIPv4, "/api/system/shutdown", cancellationToken);
  325. }
  326. private HttpClient CreateClient(string baseAddress, string password, string localIPv4)
  327. {
  328. var handler = new SocketsHttpHandler();
  329. if (!string.IsNullOrWhiteSpace(localIPv4))
  330. {
  331. handler.ConnectCallback = async (context, token) =>
  332. {
  333. var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  334. socket.Bind(new IPEndPoint(IPAddress.Parse(localIPv4), 0));
  335. await socket.ConnectAsync(context.DnsEndPoint, token);
  336. return new NetworkStream(socket, ownsSocket: true);
  337. };
  338. }
  339. var client = new HttpClient(handler) { BaseAddress = new Uri(baseAddress), Timeout = TimeSpan.FromSeconds(5) };
  340. client.DefaultRequestHeaders.Add("X-Admin-Password", password);
  341. return client;
  342. }
  343. private async Task<ApiCallResult<RemoteSystemActionResponse>> PostSystemActionAsync(string baseAddress, string password, string localIPv4, string path, CancellationToken cancellationToken)
  344. {
  345. try
  346. {
  347. using var client = CreateClient(baseAddress, password, localIPv4);
  348. using var response = await client.PostAsync(path, content: null, cancellationToken);
  349. await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
  350. var wrapper = await JsonSerializer.DeserializeAsync<ApiEnvelope<RemoteSystemActionResponse>>(stream, _jsonOptions, cancellationToken);
  351. return new ApiCallResult<RemoteSystemActionResponse>
  352. {
  353. Success = response.IsSuccessStatusCode && wrapper?.Data is not null,
  354. StatusCode = (int)response.StatusCode,
  355. Message = wrapper?.Message ?? (response.IsSuccessStatusCode ? "系统任务已提交" : $"提交失败,HTTP 状态码 {(int)response.StatusCode}。"),
  356. Data = wrapper?.Data,
  357. };
  358. }
  359. catch (Exception ex)
  360. {
  361. return new ApiCallResult<RemoteSystemActionResponse>
  362. {
  363. Success = false,
  364. Message = ex.Message,
  365. };
  366. }
  367. }
  368. private sealed class ApiEnvelope<T>
  369. {
  370. public int Code { get; set; }
  371. public string Message { get; set; } = string.Empty;
  372. public T? Data { get; set; }
  373. }
  374. private ApiEnvelope<T>? DeserializeEnvelope<T>(string content)
  375. {
  376. try
  377. {
  378. return JsonSerializer.Deserialize<ApiEnvelope<T>>(content, _jsonOptions);
  379. }
  380. catch (JsonException)
  381. {
  382. return null;
  383. }
  384. }
  385. private static ApiCallResult<T> CreateInvalidJsonResult<T>(HttpStatusCode statusCode, string content, string actionName)
  386. {
  387. var body = string.IsNullOrWhiteSpace(content) ? "响应为空" : content.Trim();
  388. if (body.Length > 160)
  389. {
  390. body = body[..160] + "...";
  391. }
  392. var status = (int)statusCode;
  393. var hint = status == 404
  394. ? $"Linux 端 Server 可能还未更新,不支持{actionName}接口。请重新发布并启动最新 Server。"
  395. : $"Linux 端 Server 返回了无法解析的{actionName}响应。";
  396. return new ApiCallResult<T>
  397. {
  398. Success = false,
  399. StatusCode = status,
  400. Message = $"{hint}HTTP {status}:{body}",
  401. };
  402. }
  403. }