Files

162 lines
4.3 KiB
Go

package harness
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"os/exec"
"strings"
"time"
)
// ErrAdminInitUnsupported 表示当前二进制还没有 admin init 子命令。
var ErrAdminInitUnsupported = errors.New("admin init unsupported")
// AdminInitializer 在启动 serve 前初始化管理员密码。
// P1 实现 admin init 后,默认实现会解析终端输出中的密码。
type AdminInitializer interface {
Init(binPath, configPath string) (password string, err error)
}
// CLIAdminInit 调用 `nixmsg admin init`;若命令不存在则返回 ErrAdminInitUnsupported。
type CLIAdminInit struct{}
// Init 执行 admin init。
func (CLIAdminInit) Init(binPath, configPath string) (string, error) {
cmd := exec.Command(binPath, "admin", "init")
cmd.Env = append(os.Environ(), "NIXMSG_CONFIG="+configPath)
out, err := cmd.CombinedOutput()
text := string(out)
if err != nil {
if isAdminInitUnsupported(text) {
return "", ErrAdminInitUnsupported
}
return "", fmt.Errorf("admin init: %w\n%s", err, text)
}
pass := parseAdminPassword(text)
if pass == "" {
return "", fmt.Errorf("admin init succeeded but password not found in output:\n%s", text)
}
return pass, nil
}
func isAdminInitUnsupported(text string) bool {
lower := strings.ToLower(text)
return strings.Contains(lower, "unknown command: admin") ||
strings.Contains(lower, "unknown command") && strings.Contains(lower, "admin")
}
func parseAdminPassword(out string) string {
// 约定:P1 实现后密码单独占一行,或出现在 "password:" 之后。
lines := strings.Split(out, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
lower := strings.ToLower(line)
switch {
case strings.HasPrefix(lower, "password:"):
return strings.TrimSpace(line[len("password:"):])
case strings.HasPrefix(lower, "admin password:"):
return strings.TrimSpace(line[len("admin password:"):])
}
}
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if len(line) >= 12 && !strings.Contains(strings.ToLower(line), "error") {
fields := strings.Fields(line)
if len(fields) == 1 {
return fields[0]
}
}
}
return ""
}
// AdminClient 管理接口 HTTP 客户端。
// Cookie 会话模式下,改变状态的方法会自动加 X-Nixmsg-Request: 1。
// 设置 APIToken 后走 Bearer,不再要求该头。
type AdminClient struct {
BaseURL string
HTTP *http.Client
APIToken string
}
// NewAdminClient 创建带 CookieJar 的管理客户端。baseURL 形如 http://127.0.0.1:12345。
func NewAdminClient(baseURL string) (*AdminClient, error) {
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
return &AdminClient{
BaseURL: strings.TrimRight(baseURL, "/"),
HTTP: &http.Client{
Timeout: 30 * time.Second,
Jar: jar,
},
}, nil
}
// SetSessionCookie 手动写入管理员会话 Cookie(测试辅助)。
func (c *AdminClient) SetSessionCookie(value string) error {
u, err := url.Parse(c.BaseURL)
if err != nil {
return err
}
c.HTTP.Jar.SetCookies(u, []*http.Cookie{{
Name: "nixmsg_admin",
Value: value,
Path: "/",
}})
return nil
}
// Do 发送请求。method 为 POST/PUT/PATCH/DELETE 且未使用 API 令牌时,自动加 X-Nixmsg-Request: 1。
func (c *AdminClient) Do(method, path string, body []byte, contentType string) (*http.Response, error) {
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
var rdr io.Reader
if body != nil {
rdr = bytes.NewReader(body)
}
req, err := http.NewRequest(method, c.BaseURL+path, rdr)
if err != nil {
return nil, err
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if c.APIToken != "" {
req.Header.Set("Authorization", "Bearer "+c.APIToken)
} else if isMutatingMethod(method) {
req.Header.Set("X-Nixmsg-Request", "1")
}
return c.HTTP.Do(req)
}
// Get JSON GET。
func (c *AdminClient) Get(path string) (*http.Response, error) {
return c.Do(http.MethodGet, path, nil, "")
}
// PostJSON POST application/json。
func (c *AdminClient) PostJSON(path string, body []byte) (*http.Response, error) {
return c.Do(http.MethodPost, path, body, "application/json")
}
func isMutatingMethod(method string) bool {
switch strings.ToUpper(method) {
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
return true
default:
return false
}
}