586 lines
19 KiB
Go
586 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/cipher"
|
|
"crypto/des"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
|
|
"isphere-ai-bridge/internal/mcpserver"
|
|
)
|
|
|
|
var expectedTools = []string{
|
|
"win_helper_version",
|
|
"win_helper_self_check",
|
|
"win_helper_scan_windows",
|
|
"win_helper_dump_uia",
|
|
"isphere_receive_messages",
|
|
"isphere_search_contacts",
|
|
"isphere_search_groups",
|
|
"isphere_receive_files",
|
|
"isphere_send_message",
|
|
}
|
|
|
|
type smokeResult struct {
|
|
OK bool `json:"ok"`
|
|
Package string `json:"package"`
|
|
Transport string `json:"transport"`
|
|
Initialized bool `json:"initialized"`
|
|
ToolCount int `json:"tool_count"`
|
|
Tools []string `json:"tools"`
|
|
HelperName string `json:"helper_name"`
|
|
ReceiveMessageCount int `json:"receive_message_count"`
|
|
ContactCount int `json:"contact_count"`
|
|
GroupCount int `json:"group_count"`
|
|
FileCount int `json:"file_count"`
|
|
FixtureReceiveMessageCount int `json:"fixture_receive_message_count"`
|
|
FixtureContactCount int `json:"fixture_contact_count"`
|
|
FixtureGroupCount int `json:"fixture_group_count"`
|
|
FixtureFileCount int `json:"fixture_file_count"`
|
|
FixtureDirReceiveMessageCount int `json:"fixture_dir_receive_message_count"`
|
|
FixtureDirContactCount int `json:"fixture_dir_contact_count"`
|
|
FixtureDirGroupCount int `json:"fixture_dir_group_count"`
|
|
FixtureDirFileCount int `json:"fixture_dir_file_count"`
|
|
SendPreviewToolPresent bool `json:"send_preview_tool_present"`
|
|
ProductionSendEnabled bool `json:"production_send_enabled"`
|
|
LocalLoginRequired bool `json:"local_login_required"`
|
|
RealISphereLoginRequired bool `json:"real_isphere_login_required"`
|
|
PythonRuntimeRequired bool `json:"python_runtime_required"`
|
|
NoRealSend bool `json:"no_real_send"`
|
|
ActionToolsPresent bool `json:"action_tools_present"`
|
|
PacketLogFileConfiguredAfterRun bool `json:"packet_log_file_configured_after_run"`
|
|
PacketLogDirConfiguredAfterRun bool `json:"packet_log_dir_configured_after_run"`
|
|
MsgLibConfigured bool `json:"msglib_configured"`
|
|
SendAuditPath string `json:"send_audit_path"`
|
|
}
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
fail("capability_smoke", err)
|
|
}
|
|
}
|
|
|
|
func run() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
clearRuntimeEnv()
|
|
auditPath := filepath.Join("runs", "capability-smoke", "send-preview.jsonl")
|
|
if err := os.Setenv("ISPHERE_SEND_AUDIT_PATH", auditPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
session, cleanup, err := newSession(ctx, "isphere-capability-smoke")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
toolNames, err := listToolNames(ctx, session)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := requireExpectedTools(toolNames); err != nil {
|
|
return err
|
|
}
|
|
|
|
helperName, err := verifyHelperVersion(ctx, session)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
receiveCount, err := verifyReceiveMessages(ctx, session, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
contactCount, err := verifySearchContacts(ctx, session, "sender", 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
groupCount, err := verifySearchGroups(ctx, session, "project", 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileCount, err := verifyReceiveFiles(ctx, session, "report", 0, "", "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sendPreviewOK, productionSendEnabled, err := verifySendMessage(ctx, session)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fixtureReceiveCount, fixtureContactCount, fixtureGroupCount, fixtureFileCount, err := verifyPacketLogFileFixture(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fixtureDirReceiveCount, fixtureDirContactCount, fixtureDirGroupCount, fixtureDirFileCount, err := verifyPacketLogDirFixture(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result := smokeResult{
|
|
OK: true,
|
|
Package: "send-capability-test",
|
|
Transport: "sdk_in_memory",
|
|
Initialized: true,
|
|
ToolCount: len(toolNames),
|
|
Tools: toolNames,
|
|
HelperName: helperName,
|
|
ReceiveMessageCount: receiveCount,
|
|
ContactCount: contactCount,
|
|
GroupCount: groupCount,
|
|
FileCount: fileCount,
|
|
FixtureReceiveMessageCount: fixtureReceiveCount,
|
|
FixtureContactCount: fixtureContactCount,
|
|
FixtureGroupCount: fixtureGroupCount,
|
|
FixtureFileCount: fixtureFileCount,
|
|
FixtureDirReceiveMessageCount: fixtureDirReceiveCount,
|
|
FixtureDirContactCount: fixtureDirContactCount,
|
|
FixtureDirGroupCount: fixtureDirGroupCount,
|
|
FixtureDirFileCount: fixtureDirFileCount,
|
|
SendPreviewToolPresent: sendPreviewOK,
|
|
ProductionSendEnabled: productionSendEnabled,
|
|
LocalLoginRequired: false,
|
|
RealISphereLoginRequired: false,
|
|
PythonRuntimeRequired: false,
|
|
NoRealSend: true,
|
|
ActionToolsPresent: false,
|
|
PacketLogFileConfiguredAfterRun: os.Getenv("ISPHERE_PACKET_LOG_FILE") != "",
|
|
PacketLogDirConfiguredAfterRun: os.Getenv("ISPHERE_PACKET_LOG_DIR") != "",
|
|
MsgLibConfigured: false,
|
|
SendAuditPath: auditPath,
|
|
}
|
|
encoded, err := json.Marshal(result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(encoded))
|
|
return nil
|
|
}
|
|
|
|
func clearRuntimeEnv() {
|
|
for _, key := range []string{
|
|
"ISPHERE_PACKET_LOG_FILE",
|
|
"ISPHERE_PACKET_LOG_DIR",
|
|
"ISPHERE_MSGLIB_SIDECAR_EXE",
|
|
"ISPHERE_MSGLIB_SQLITE_DLL",
|
|
"ISPHERE_MSGLIB_DB",
|
|
"ISPHERE_MSGLIB_PASSWORD",
|
|
} {
|
|
_ = os.Unsetenv(key)
|
|
}
|
|
}
|
|
|
|
func newSession(ctx context.Context, name string) (*mcp.ClientSession, func(), error) {
|
|
serverTransport, clientTransport := mcp.NewInMemoryTransports()
|
|
server, err := mcpserver.NewServerFromEnv()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("server config: %w", err)
|
|
}
|
|
serverCtx, cancel := context.WithCancel(ctx)
|
|
serverErr := make(chan error, 1)
|
|
go func() {
|
|
serverErr <- server.Run(serverCtx, serverTransport)
|
|
}()
|
|
|
|
client := mcp.NewClient(&mcp.Implementation{Name: name, Version: "0.1.0"}, nil)
|
|
session, err := client.Connect(serverCtx, clientTransport, nil)
|
|
if err != nil {
|
|
cancel()
|
|
return nil, nil, fmt.Errorf("connect: %w", err)
|
|
}
|
|
cleanup := func() {
|
|
_ = session.Close()
|
|
cancel()
|
|
select {
|
|
case <-serverErr:
|
|
case <-time.After(2 * time.Second):
|
|
fmt.Fprintln(os.Stderr, `{"ok":false,"step":"server_shutdown","error":"server did not stop before timeout"}`)
|
|
}
|
|
}
|
|
return session, cleanup, nil
|
|
}
|
|
|
|
func listToolNames(ctx context.Context, session *mcp.ClientSession) ([]string, error) {
|
|
result, err := session.ListTools(ctx, &mcp.ListToolsParams{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list tools: %w", err)
|
|
}
|
|
names := make([]string, 0, len(result.Tools))
|
|
for _, tool := range result.Tools {
|
|
names = append(names, tool.Name)
|
|
}
|
|
sort.Strings(names)
|
|
return names, nil
|
|
}
|
|
|
|
func requireExpectedTools(got []string) error {
|
|
want := append([]string(nil), expectedTools...)
|
|
sort.Strings(want)
|
|
if !reflect.DeepEqual(got, want) {
|
|
return fmt.Errorf("tool names = %#v, want %#v", got, want)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func verifyHelperVersion(ctx context.Context, session *mcp.ClientSession) (string, error) {
|
|
payload, err := callToolObject(ctx, session, "win_helper_version", map[string]any{})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if payload["ok"] != true {
|
|
return "", fmt.Errorf("win_helper_version ok = %#v, want true", payload["ok"])
|
|
}
|
|
data, ok := payload["data"].(map[string]any)
|
|
if !ok {
|
|
return "", fmt.Errorf("win_helper_version data missing: %#v", payload["data"])
|
|
}
|
|
helperName, _ := data["helper_name"].(string)
|
|
if helperName != "ISphereWinHelper" {
|
|
return "", fmt.Errorf("helper_name = %q, want ISphereWinHelper", helperName)
|
|
}
|
|
return helperName, nil
|
|
}
|
|
|
|
func verifyReceiveMessages(ctx context.Context, session *mcp.ClientSession, wantCount int) (int, error) {
|
|
payload, err := callToolObject(ctx, session, "isphere_receive_messages", map[string]any{
|
|
"source_preference": "local_readonly",
|
|
"preview": true,
|
|
"cursor": "",
|
|
"include_attachment_metadata": true,
|
|
"limit": 5,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if payload["ok"] != true {
|
|
return 0, fmt.Errorf("isphere_receive_messages ok = %#v, want true", payload["ok"])
|
|
}
|
|
messages, ok := payload["messages"].([]any)
|
|
if !ok {
|
|
return 0, fmt.Errorf("messages missing or not array: %#v", payload["messages"])
|
|
}
|
|
if len(messages) != wantCount {
|
|
return 0, fmt.Errorf("message count = %d, want %d", len(messages), wantCount)
|
|
}
|
|
return len(messages), nil
|
|
}
|
|
|
|
func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, wantCount int) (int, error) {
|
|
payload, err := callToolObject(ctx, session, "isphere_search_contacts", map[string]any{
|
|
"query": query,
|
|
"source_preference": "local_readonly",
|
|
"include_inactive": true,
|
|
"cursor": "",
|
|
"limit": 5,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if payload["ok"] != true {
|
|
return 0, fmt.Errorf("isphere_search_contacts ok = %#v, want true", payload["ok"])
|
|
}
|
|
contacts, ok := payload["contacts"].([]any)
|
|
if !ok {
|
|
return 0, fmt.Errorf("contacts missing or not array: %#v", payload["contacts"])
|
|
}
|
|
if len(contacts) != wantCount {
|
|
return 0, fmt.Errorf("contact count = %d, want %d", len(contacts), wantCount)
|
|
}
|
|
return len(contacts), nil
|
|
}
|
|
|
|
func verifySearchGroups(ctx context.Context, session *mcp.ClientSession, query string, wantCount int) (int, error) {
|
|
payload, err := callToolObject(ctx, session, "isphere_search_groups", map[string]any{
|
|
"query": query,
|
|
"source_preference": "local_readonly",
|
|
"include_archived": true,
|
|
"cursor": "",
|
|
"limit": 5,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if payload["ok"] != true {
|
|
return 0, fmt.Errorf("isphere_search_groups ok = %#v, want true", payload["ok"])
|
|
}
|
|
groups, ok := payload["groups"].([]any)
|
|
if !ok {
|
|
return 0, fmt.Errorf("groups missing or not array: %#v", payload["groups"])
|
|
}
|
|
if len(groups) != wantCount {
|
|
return 0, fmt.Errorf("group count = %d, want %d", len(groups), wantCount)
|
|
}
|
|
return len(groups), nil
|
|
}
|
|
|
|
func verifyReceiveFiles(ctx context.Context, session *mcp.ClientSession, nameContains string, wantCount int, wantFileID string, wantFileName string) (int, error) {
|
|
payload, err := callToolObject(ctx, session, "isphere_receive_files", map[string]any{
|
|
"mode": "list",
|
|
"name_contains": nameContains,
|
|
"source_preference": "local_readonly",
|
|
"preview": true,
|
|
"cursor": "",
|
|
"output_dir": "",
|
|
"limit": 5,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if payload["ok"] != true {
|
|
return 0, fmt.Errorf("isphere_receive_files ok = %#v, want true", payload["ok"])
|
|
}
|
|
files, ok := payload["files"].([]any)
|
|
if !ok {
|
|
return 0, fmt.Errorf("files missing or not array: %#v", payload["files"])
|
|
}
|
|
if len(files) != wantCount {
|
|
return 0, fmt.Errorf("file count = %d, want %d", len(files), wantCount)
|
|
}
|
|
if wantCount > 0 {
|
|
first, ok := files[0].(map[string]any)
|
|
if !ok {
|
|
return 0, fmt.Errorf("first file not object: %#v", files[0])
|
|
}
|
|
if first["file_id"] != wantFileID || first["file_name"] != wantFileName {
|
|
return 0, fmt.Errorf("unexpected file: %#v", first)
|
|
}
|
|
if first["download_ref"] != nil || first["saved_path"] != nil || first["sha256"] != nil {
|
|
return 0, fmt.Errorf("file list should not download or save: %#v", first)
|
|
}
|
|
}
|
|
return len(files), nil
|
|
}
|
|
|
|
func verifySendMessage(ctx context.Context, session *mcp.ClientSession) (bool, bool, error) {
|
|
content := "redacted preview body"
|
|
contentHash := sha256Hex(content)
|
|
preview, err := callToolObject(ctx, session, "isphere_send_message", map[string]any{
|
|
"target_type": "direct",
|
|
"target_id": "sender@imopenfire1-lanzhou",
|
|
"content_text": content,
|
|
"content_sha256": contentHash,
|
|
"idempotency_key": "capability-preview-idempotency-key",
|
|
"execution_mode": "preview",
|
|
})
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
if preview["ok"] != true || preview["send_status"] != "planned" || preview["connector_stage"] != "preview" {
|
|
return false, false, fmt.Errorf("unexpected send preview payload: %#v", preview)
|
|
}
|
|
sideEffects, ok := preview["side_effects"].(map[string]any)
|
|
if !ok || sideEffects["sent_message"] != false || sideEffects["clicked_ui"] != false || sideEffects["typed_text"] != false {
|
|
return false, false, fmt.Errorf("send preview side effects mismatch: %#v", preview["side_effects"])
|
|
}
|
|
|
|
production, err := callToolObject(ctx, session, "isphere_send_message", map[string]any{
|
|
"target_type": "direct",
|
|
"target_id": "sender@imopenfire1-lanzhou",
|
|
"content_text": content,
|
|
"content_sha256": contentHash,
|
|
"idempotency_key": "capability-production-idempotency-key",
|
|
"execution_mode": "production",
|
|
})
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
if production["ok"] != false || production["send_status"] != "blocked" || production["connector_stage"] != "blocked" {
|
|
return false, false, fmt.Errorf("production send was not blocked: %#v", production)
|
|
}
|
|
productionSideEffects, ok := production["side_effects"].(map[string]any)
|
|
if !ok || productionSideEffects["sent_message"] != false || productionSideEffects["uploaded_file"] != false || productionSideEffects["captured_network"] != false {
|
|
return false, false, fmt.Errorf("production side effects mismatch: %#v", production["side_effects"])
|
|
}
|
|
return true, false, nil
|
|
}
|
|
|
|
func verifyPacketLogFileFixture(ctx context.Context) (int, int, int, int, error) {
|
|
plaintext := fixturePacketLogPlaintext("msg-fixture-1", "redacted-report.docx")
|
|
line, err := encryptPacketLogLine(plaintext)
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
file, err := os.CreateTemp("", "isphere-capability-packet-*.log")
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
fixturePath := file.Name()
|
|
defer os.Remove(fixturePath)
|
|
if _, err := file.WriteString("\n" + line + "\n"); err != nil {
|
|
_ = file.Close()
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
|
|
restore := replaceEnv("ISPHERE_PACKET_LOG_FILE", fixturePath)
|
|
defer restore()
|
|
_ = os.Unsetenv("ISPHERE_PACKET_LOG_DIR")
|
|
|
|
session, cleanup, err := newSession(ctx, "isphere-capability-smoke-file-fixture")
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
defer cleanup()
|
|
receiveCount, err := verifyReceiveMessages(ctx, session, 1)
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
contactCount, err := verifySearchContacts(ctx, session, "sender", 1)
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
groupCount, err := verifySearchGroups(ctx, session, "project", 1)
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
fileCount, err := verifyReceiveFiles(ctx, session, "report", 1, "msg-fixture-1:redacted-report.docx", "redacted-report.docx")
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
return receiveCount, contactCount, groupCount, fileCount, nil
|
|
}
|
|
|
|
func verifyPacketLogDirFixture(ctx context.Context) (int, int, int, int, error) {
|
|
plaintext := fixturePacketLogPlaintext("msg-dir-fixture-1", "redacted-dir-report.docx")
|
|
line, err := encryptPacketLogLine(plaintext)
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
dir, err := os.MkdirTemp("", "isphere-capability-packet-dir-*")
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
defer os.RemoveAll(dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "a-packet-reader.log"), []byte("\n"+line+"\n"), 0o600); err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "ignored.tmp"), []byte("ignored\n"), 0o600); err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
|
|
restoreFile := replaceEnv("ISPHERE_PACKET_LOG_FILE", "")
|
|
restoreDir := replaceEnv("ISPHERE_PACKET_LOG_DIR", dir)
|
|
defer restoreFile()
|
|
defer restoreDir()
|
|
|
|
session, cleanup, err := newSession(ctx, "isphere-capability-smoke-dir-fixture")
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
defer cleanup()
|
|
receiveCount, err := verifyReceiveMessages(ctx, session, 1)
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
contactCount, err := verifySearchContacts(ctx, session, "sender", 1)
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
groupCount, err := verifySearchGroups(ctx, session, "project", 1)
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
fileCount, err := verifyReceiveFiles(ctx, session, "dir-report", 1, "msg-dir-fixture-1:redacted-dir-report.docx", "redacted-dir-report.docx")
|
|
if err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
return receiveCount, contactCount, groupCount, fileCount, nil
|
|
}
|
|
|
|
func callToolObject(ctx context.Context, session *mcp.ClientSession, name string, args map[string]any) (map[string]any, error) {
|
|
result, err := session.CallTool(ctx, &mcp.CallToolParams{Name: name, Arguments: args})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
if result.IsError {
|
|
return nil, fmt.Errorf("%s returned IsError=true: %#v", name, result.Content)
|
|
}
|
|
payload, err := json.Marshal(result.StructuredContent)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s marshal structuredContent: %w", name, err)
|
|
}
|
|
var object map[string]any
|
|
if err := json.Unmarshal(payload, &object); err != nil {
|
|
return nil, fmt.Errorf("%s decode structuredContent %s: %w", name, payload, err)
|
|
}
|
|
return object, nil
|
|
}
|
|
|
|
func fixturePacketLogPlaintext(messageID string, body string) string {
|
|
return "--------------------------------------------------------------------------------------------------------------------------------------------\n" +
|
|
"2026/7/7 15:30:07\n" +
|
|
"<message id=\"" + messageID + "\" from=\"project-room@conference.imopenfire1-lanzhou/imp_pc_4.1.2.6842\" to=\"sender@imopenfire1-lanzhou\" type=\"groupchat\">\n" +
|
|
" <body>" + body + "</body>\n" +
|
|
" <received xmlns=\"urn:xmpp:receipts\" id=\"receipt-" + messageID + "\" type=\"1\" stamp=\"\" />\n" +
|
|
" <subject>FILE_TRANSFER_CANCEL</subject>\n" +
|
|
" <isphere xmlns=\"isphere.im\" type=\"1001\" sendtime=\"1783423807000\" version=\"1\" />\n" +
|
|
" <readed />\n" +
|
|
"</message>"
|
|
}
|
|
|
|
func encryptPacketLogLine(plaintext string) (string, error) {
|
|
block, err := des.NewCipher([]byte("hyhccdtm"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
plain := padPKCS7([]byte(plaintext), des.BlockSize)
|
|
ciphertext := make([]byte, len(plain))
|
|
iv := []byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF}
|
|
cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, plain)
|
|
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
|
}
|
|
|
|
func padPKCS7(data []byte, blockSize int) []byte {
|
|
pad := blockSize - len(data)%blockSize
|
|
out := append([]byte(nil), data...)
|
|
for i := 0; i < pad; i++ {
|
|
out = append(out, byte(pad))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sha256Hex(value string) string {
|
|
sum := sha256.Sum256([]byte(value))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func replaceEnv(key string, value string) func() {
|
|
oldValue, hadOldValue := os.LookupEnv(key)
|
|
if value == "" {
|
|
_ = os.Unsetenv(key)
|
|
} else {
|
|
_ = os.Setenv(key, value)
|
|
}
|
|
return func() {
|
|
if hadOldValue {
|
|
_ = os.Setenv(key, oldValue)
|
|
} else {
|
|
_ = os.Unsetenv(key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func fail(step string, err error) {
|
|
encoded, _ := json.Marshal(map[string]any{
|
|
"ok": false,
|
|
"step": step,
|
|
"error": err.Error(),
|
|
})
|
|
fmt.Fprintln(os.Stderr, string(encoded))
|
|
os.Exit(1)
|
|
}
|