From 90954671b046c8578dc5ae3baf6ac83aed21946e Mon Sep 17 00:00:00 2001 From: cncws <1031616423@qq.com> Date: Wed, 24 Jul 2024 11:08:02 +0800 Subject: [PATCH] 0.1.0 --- README.md | 20 ++++++++ client/auth.go | 48 +++++++++++++++++++ client/client.go | 90 ++++++++++++++++++++++++++++++++++++ go.mod | 10 ++++ go.sum | 12 +++++ main.go | 117 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 297 insertions(+) create mode 100644 README.md create mode 100644 client/auth.go create mode 100644 client/client.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..215195c --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# JumpServer RDP Launcher + +加速 JumpServer 连接 RDP 资产,无需登陆网页再连接资产。 + +在 MacOS(arm64) 和 Windows(amd64) 上测试通过。 + +## 使用方法 + +- MacOS 需安装 Microsoft Remote Desktop + - `brew install --cask microsoft-remote-desktop` +- 在 JumpServer 网页端获取 API Key +- 使用命令打开远程桌面 + +```bash +# 连接资产 +jms-rdp -url URL -ak AK -sk SK -account ACCOUNT IP + +# 查看使用帮助 +jms-rdp +``` diff --git a/client/auth.go b/client/auth.go new file mode 100644 index 0000000..adc76bc --- /dev/null +++ b/client/auth.go @@ -0,0 +1,48 @@ +package client + +import ( + "net/http" + "time" + + "gopkg.in/twindagger/httpsig.v1" +) + +type sigAuth struct { + KeyID string + SecretID string +} + +func (auth *sigAuth) Sign(r *http.Request) error { + headers := []string{"(request-target)", "date"} + signer, err := httpsig.NewRequestSigner(auth.KeyID, auth.SecretID, "hmac-sha256") + if err != nil { + return err + } + return signer.SignRequest(r, headers, nil) +} + +type sigAuthRoundTripper struct { + http.RoundTripper + SigAuth *sigAuth +} + +func (rt *sigAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + gmtFmt := "Mon, 02 Jan 2006 15:04:05 GMT" + req.Header.Add("Date", time.Now().Format(gmtFmt)) + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + if err := rt.SigAuth.Sign(req); err != nil { + return nil, err + } + return rt.RoundTripper.RoundTrip(req) +} + +func NewSigAuthRoundTripper(keyID, secretID string) *sigAuthRoundTripper { + return &sigAuthRoundTripper{ + RoundTripper: http.DefaultTransport, + SigAuth: &sigAuth{ + KeyID: keyID, + SecretID: secretID, + }, + } +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..d214fd5 --- /dev/null +++ b/client/client.go @@ -0,0 +1,90 @@ +package client + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" + "net/url" + "os" + "time" +) + +type JmsClient struct { + *http.Client + serverUrl string +} + +func NewJmsClient(serverUrl, accessKey, secretKey string, timeout int) *JmsClient { + return &JmsClient{ + Client: &http.Client{ + Transport: NewSigAuthRoundTripper(accessKey, secretKey), + Timeout: time.Duration(timeout) * time.Second, + }, + serverUrl: serverUrl, + } +} + +// 根据 IP 查询资产 ID, 支持的协议 +func (c *JmsClient) QueryIDByIP(ip string) (id string, protocols []string) { + endpoint, _ := url.JoinPath(c.serverUrl, "api/v1/assets/assets/suggestions/") + u, _ := url.Parse(endpoint) + u.RawQuery = url.Values{"address": []string{ip}}.Encode() + req, _ := http.NewRequest("GET", u.String(), nil) + resp, err := c.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + var data []map[string]any + _ = json.Unmarshal(body, &data) + if len(data) == 0 { + log.Fatal("❌查无资产") + } + id = data[0]["id"].(string) + // 取出支持的协议列表 + for _, v := range data[0]["protocols"].([]any) { + protocols = append(protocols, v.(map[string]any)["name"].(string)) + } + return +} + +// 下载用于连接资产的令牌 +func (c *JmsClient) GenRDPToken(asset, account string) (string, error) { + endpoint, _ := url.JoinPath(c.serverUrl, "api/v1/authentication/connection-token/") + reqData := map[string]string{ + "account": account, + "asset": asset, + "connect_method": "mstsc", + "protocol": "rdp", + } + payload, _ := json.Marshal(reqData) + req, _ := http.NewRequest("POST", endpoint, bytes.NewBuffer(payload)) + resp, err := c.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + var data map[string]any + _ = json.Unmarshal(body, &data) + return data["id"].(string), nil +} + +// 下载令牌对应的 RDP 文件 +func (c *JmsClient) DownRDP(token, fullscreen string) (string, error) { + endpoint, _ := url.JoinPath(c.serverUrl, "api/v1/authentication/connection-token", token, "rdp-file/") + req, _ := http.NewRequest("GET", endpoint+"?full_screen="+fullscreen, nil) + resp, err := c.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + f, _ := os.CreateTemp("", "*.rdp") + defer f.Close() + f.Write(body) + return f.Name(), nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..66fda55 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module jms-rdp + +go 1.22.5 + +require gopkg.in/twindagger/httpsig.v1 v1.2.0 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/stretchr/testify v1.9.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3b8ee8c --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/twindagger/httpsig.v1 v1.2.0 h1:GHT8oYp1sdRKr89MYwpixUcDOx4iEY5EO/Rk+A5FenY= +gopkg.in/twindagger/httpsig.v1 v1.2.0/go.mod h1:J1gOUnY2juidmnrHbYPnCoTacqx3oIAUsyKfASUXlU8= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..96c2358 --- /dev/null +++ b/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "flag" + "html/template" + "jms-rdp/client" + "log" + "os" + "os/exec" + "runtime" +) + +const ( + version = "0.1.0" + helpTemplate = ` +JumpServer RDP Launcher v{{.}} + +Support: MacOS(arm64), Windows(amd64) + +Usage: + jms-rdp -url URL -ak AK -sk SK -account ACCOUNT IP + 必需参数 + -url 服务器地址 + -ak Access Key,在 Web 页面 API Key 列表获取 + -sk Secret Key,在 Web 页面 API Key 列表获取 + -account 登陆账号 + 可选参数 + -fullscreen 是否全屏 + +` +) + +var ( + serverURL string + accessKey, secretKey string + account string + fullscreen string + cli *client.JmsClient +) + +func init() { + log.SetFlags(0) + + if len(os.Args) == 1 { + tpl, _ := template.New("help").Parse(helpTemplate) + tpl.Execute(os.Stdout, version) + os.Exit(0) + } + + flag.StringVar(&serverURL, "url", "", "server url") + flag.StringVar(&accessKey, "ak", "", "access key") + flag.StringVar(&secretKey, "sk", "", "secret key") + flag.StringVar(&account, "account", "", "登陆账号") + flag.StringVar(&fullscreen, "fullscreen", "1", "是否全屏") + flag.Parse() + + if serverURL == "" { + log.Fatal("需设置 -url 参数") + } + if accessKey == "" { + log.Fatal("需设置 -ak 参数") + } + if secretKey == "" { + log.Fatal("需设置 -sk 参数") + } + if account == "" { + log.Fatal("需设置 -account 参数") + } + cli = client.NewJmsClient(serverURL, accessKey, secretKey, 3) +} + +func handleRDP(asset_id string) error { + log.Println("下载连接令牌...") + token, err := cli.GenRDPToken(asset_id, account) + if err != nil { + return err + } + log.Println("✅", token) + log.Println("下载 RDP 文件...") + file, err := cli.DownRDP(token, fullscreen) + if err != nil { + return err + } + log.Println("✅", file) + log.Println("打开远程桌面...") + switch runtime.GOOS { + case "darwin": + cmd := exec.Command("open", file) + return cmd.Start() + case "windows": + cmd := exec.Command("mstsc.exe", file) + return cmd.Start() + default: + log.Println("❌尚未支持 ", runtime.GOOS) + } + return nil +} + +func main() { + ips := flag.Args() + if len(ips) == 0 { + log.Fatal("❌未指定连接的资产 IP") + } + + log.Println("查询资产 ID...") + id, protocols := cli.QueryIDByIP(ips[0]) + log.Println("✅", id, "supports", protocols) + + for _, p := range protocols { + switch p { + case "rdp": + handleRDP(id) + return + } + } + log.Fatal("❌未支持协议", protocols) +}