节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
4.94 KiB
Stardust
using Microsoft.AspNetCore.Mvc;
using NewLife;
using NewLife.Log;
using NewLife.Remoting;
using NewLife.Remoting.Extensions;
using NewLife.Remoting.Services;
using Stardust.Data;
using Stardust.Models;
using Stardust.Server.Services;

namespace Stardust.Server.Controllers;

/// <summary>StarAgent远程部署服务</summary>
[ApiFilter]
[Route("[controller]/[action]")]
public class AgentDeployController(AgentDeployService deployService, ITokenService tokenService, ITracer tracer) : ControllerBase
{
    /// <summary>验证调用方。远程部署把服务器当作SSH跳板,仅允许已配置密钥且具控制权的管理应用(如StarWeb)调用</summary>
    /// <returns>通过校验的应用</returns>
    private App Valid()
    {
        var token = ApiFilterAttribute.GetToken(HttpContext);
        if (token.IsNullOrEmpty()) throw new ApiException(ApiCode.Unauthorized, L.T("No token provided."));

        var (jwt, ex) = tokenService.DecodeToken(token);
        if (ex != null) throw ex;

        var app = App.FindByName(jwt?.Subject);
        if (app == null || !app.Enable) throw new ApiException(ApiCode.Unauthorized, L.T("Invalid application token"));

        // 空密钥应用可被匿名冒用(如种子StarWeb),禁止使用远程部署等高危能力
        if (app.Secret.IsNullOrEmpty())
            throw new ApiException(ApiCode.Forbidden, L.T("[{0}] has no secret configured; remote deployment is forbidden. Set a Secret for this application in the registry.", app));

        // 仅具控制权的管理应用(配置了AllowControlNodes)可远程部署
        if (app.AllowControlNodes.IsNullOrEmpty())
            throw new ApiException(ApiCode.Forbidden, L.T("[{0}] has no control permission; remote deployment is forbidden.", app));

        return app;
    }

    /// <summary>执行远程部署</summary>
    /// <param name="model">部署参数</param>
    /// <returns>部署结果</returns>
    [HttpPost]
    public List<AgentDeployResult> Deploy([FromBody] AgentDeployModel model)
    {
        Valid();

        using var span = tracer?.NewSpan("AgentDeploy-Deploy", model);

        try
        {
            XTrace.WriteLine(L.TLog("Received remote deployment request: Hosts={0}, User={1}, OS={2}, Server={3}", model.Hosts, model.UserName, model.OSType, model.ServerUrl));

            var results = deployService.Deploy(model);

            var success = results.Count(r => r.Success);
            var failed = results.Count - success;

            XTrace.WriteLine(L.TLog("Deployment completed: succeeded={0}, failed={1}", success, failed));

            return results;
        }
        catch (Exception ex)
        {
            span?.SetError(ex, null);
            XTrace.WriteException(ex);
            throw;
        }
    }

    /// <summary>测试连接</summary>
    /// <param name="host">目标主机</param>
    /// <param name="port">SSH端口</param>
    /// <param name="userName">用户名</param>
    /// <param name="password">密码</param>
    /// <returns>连接测试结果</returns>
    [HttpPost]
    public Object TestConnection(String host, Int32 port, String userName, String password)
    {
        Valid();

        if (host.IsNullOrEmpty()) throw new ArgumentNullException(nameof(host));
        if (userName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(userName));
        if (password.IsNullOrEmpty()) throw new ArgumentNullException(nameof(password));

        try
        {
            // 简单测试:尝试SSH连接并执行echo命令
            var cmd = $"sshpass -p '{password}' ssh -p {port} -o StrictHostKeyChecking=no -o ConnectTimeout=10 {userName}@{host} 'echo connected'";

            var psi = new System.Diagnostics.ProcessStartInfo
            {
                FileName = "/bin/bash",
                Arguments = $"-c \"{cmd.Replace("\"", "\\\"")}\"",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };

            using var process = System.Diagnostics.Process.Start(psi);
            if (process == null) return new { success = false, message = L.T("Unable to start the process.") };

            var output = process.StandardOutput.ReadToEnd();
            var error = process.StandardError.ReadToEnd();

            if (!process.WaitForExit(15000))
            {
                process.Kill();
                return new { success = false, message = L.T("Connection timed out.") };
            }

            if (process.ExitCode == 0 && output.Contains("connected"))
            {
                return new { success = true, message = L.T("Connected.") };
            }
            else
            {
                return new { success = false, message = L.T("Connection failed: {0}", error) };
            }
        }
        catch (Exception ex)
        {
            XTrace.WriteException(ex);
            return new { success = false, message = ex.Message };
        }
    }
}