@reegaviljoen/eldlock 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +285 -0
- package/bin/eldlock +11 -0
- package/docs/architecture.md +164 -0
- package/docs/threat-model.md +47 -0
- package/eldlock-cli/README.md +56 -0
- package/eldlock-cli/bin/eldlock +3 -0
- package/eldlock-cli/package-lock.json +805 -0
- package/eldlock-cli/package.json +71 -0
- package/eldlock-cli/src/api.ts +250 -0
- package/eldlock-cli/src/cli.ts +490 -0
- package/eldlock-cli/src/main.ts +10 -0
- package/eldlock-cli/src/tui.ts +676 -0
- package/eldlock-cli/tsconfig.json +13 -0
- package/eldlock-cli/vendor/npm/ansi-regex-6.2.2.tgz +0 -0
- package/eldlock-cli/vendor/npm/bun-ffi-structs-0.2.2.tgz +0 -0
- package/eldlock-cli/vendor/npm/diff-9.0.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/emoji-regex-10.6.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/esbuild-0.28.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/esbuild-darwin-arm64-0.28.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/esbuild-darwin-x64-0.28.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/esbuild-linux-arm64-0.28.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/esbuild-linux-x64-0.28.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/fsevents-2.3.3.tgz +0 -0
- package/eldlock-cli/vendor/npm/get-east-asian-width-1.6.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/marked-17.0.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/opentui-core-0.3.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/opentui-core-darwin-arm64-0.3.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/opentui-core-darwin-x64-0.3.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/opentui-core-linux-arm64-0.3.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/opentui-core-linux-x64-0.3.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/string-width-7.2.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/strip-ansi-7.1.2.tgz +0 -0
- package/eldlock-cli/vendor/npm/tsx-4.22.4.tgz +0 -0
- package/eldlock-cli/vendor/npm/types-node-22.19.19.tgz +0 -0
- package/eldlock-cli/vendor/npm/typescript-5.9.3.tgz +0 -0
- package/eldlock-cli/vendor/npm/undici-types-6.21.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/web-tree-sitter-0.25.10.tgz +0 -0
- package/eldlock-cli/vendor/npm/yoga-layout-3.2.1.tgz +0 -0
- package/eldlock-server/cmd/eldlock-server/main.go +132 -0
- package/eldlock-server/go.mod +10 -0
- package/eldlock-server/go.sum +11 -0
- package/eldlock-server/internal/api/README.md +14 -0
- package/eldlock-server/internal/api/core.go +126 -0
- package/eldlock-server/internal/api/exec.go +97 -0
- package/eldlock-server/internal/api/secrets.go +358 -0
- package/eldlock-server/internal/api/server.go +72 -0
- package/eldlock-server/internal/api/service_test.go +416 -0
- package/eldlock-server/internal/api/types.go +48 -0
- package/eldlock-server/internal/api/vault.go +69 -0
- package/eldlock-server/internal/api/vendor.go +44 -0
- package/eldlock-server/internal/libfido2/LICENSE +21 -0
- package/eldlock-server/internal/libfido2/README.md +127 -0
- package/eldlock-server/internal/libfido2/examples_test.go +614 -0
- package/eldlock-server/internal/libfido2/fido2.go +1234 -0
- package/eldlock-server/internal/libfido2/fido2_darwin.go +7 -0
- package/eldlock-server/internal/libfido2/fido2_other.go +9 -0
- package/eldlock-server/internal/libfido2/fido2_test.go +101 -0
- package/eldlock-server/internal/libfido2/go.mod +10 -0
- package/eldlock-server/internal/libfido2/go.sum +16 -0
- package/eldlock-server/internal/libfido2/log.go +87 -0
- package/eldlock-server/internal/store/README.md +7 -0
- package/eldlock-server/internal/store/store.go +434 -0
- package/eldlock-server/internal/store/store_test.go +125 -0
- package/eldlock-server/internal/yubikey/README.md +25 -0
- package/eldlock-server/internal/yubikey/default_fido2.go +7 -0
- package/eldlock-server/internal/yubikey/default_stub.go +7 -0
- package/eldlock-server/internal/yubikey/fido2_disabled.go +9 -0
- package/eldlock-server/internal/yubikey/fido2_libfido2.go +225 -0
- package/eldlock-server/internal/yubikey/fido2_libfido2_test.go +66 -0
- package/eldlock-server/internal/yubikey/passkey.go +139 -0
- package/eldlock-server/internal/yubikey/passkey_test.go +36 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/LICENSE +21 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/README.md +127 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/fido2.go +1234 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/fido2_darwin.go +7 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/fido2_other.go +9 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/log.go +87 -0
- package/eldlock-server/vendor/github.com/pkg/errors/.travis.yml +10 -0
- package/eldlock-server/vendor/github.com/pkg/errors/LICENSE +23 -0
- package/eldlock-server/vendor/github.com/pkg/errors/Makefile +44 -0
- package/eldlock-server/vendor/github.com/pkg/errors/README.md +59 -0
- package/eldlock-server/vendor/github.com/pkg/errors/appveyor.yml +32 -0
- package/eldlock-server/vendor/github.com/pkg/errors/errors.go +288 -0
- package/eldlock-server/vendor/github.com/pkg/errors/go113.go +38 -0
- package/eldlock-server/vendor/github.com/pkg/errors/stack.go +177 -0
- package/eldlock-server/vendor/modules.txt +7 -0
- package/examples/eldlock.toml +17 -0
- package/install.sh +66 -0
- package/package.json +66 -0
- package/scripts/build-production.mjs +177 -0
- package/scripts/postinstall-production.mjs +23 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"encoding/hex"
|
|
7
|
+
"fmt"
|
|
8
|
+
"os"
|
|
9
|
+
"os/signal"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"syscall"
|
|
12
|
+
|
|
13
|
+
"github.com/eldlock/eldlock-server/internal/api"
|
|
14
|
+
"github.com/eldlock/eldlock-server/internal/yubikey"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
func main() {
|
|
18
|
+
if len(os.Args) > 1 && os.Args[1] == "version" {
|
|
19
|
+
fmt.Println("eldlock-server dev")
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if len(os.Args) > 1 && os.Args[1] == "init" {
|
|
24
|
+
if err := initVault(argAt(2)); err != nil {
|
|
25
|
+
fmt.Fprintln(os.Stderr, err)
|
|
26
|
+
os.Exit(1)
|
|
27
|
+
}
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if len(os.Args) > 1 && os.Args[1] == "serve" {
|
|
32
|
+
if err := serve(); err != nil {
|
|
33
|
+
fmt.Fprintln(os.Stderr, err)
|
|
34
|
+
os.Exit(1)
|
|
35
|
+
}
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fmt.Println(`eldlock-server
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
eldlock-server version
|
|
43
|
+
eldlock-server init [name]
|
|
44
|
+
eldlock-server serve
|
|
45
|
+
|
|
46
|
+
The serve command starts the local Unix socket API used by eldlock-cli.`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func initVault(vaultName string) error {
|
|
50
|
+
stateDir := defaultStateDir()
|
|
51
|
+
provider, err := yubikey.NewProviderFromEnv()
|
|
52
|
+
if err != nil {
|
|
53
|
+
return err
|
|
54
|
+
}
|
|
55
|
+
resolver := api.FileStoreResolver{StateDir: stateDir, KeyProvider: provider}
|
|
56
|
+
normalizedName, err := api.NormalizeVaultName(vaultName)
|
|
57
|
+
if err != nil {
|
|
58
|
+
return err
|
|
59
|
+
}
|
|
60
|
+
secretStore, err := resolver.StoreForVault(normalizedName)
|
|
61
|
+
if err != nil {
|
|
62
|
+
return err
|
|
63
|
+
}
|
|
64
|
+
if err := secretStore.Init(); err != nil {
|
|
65
|
+
return err
|
|
66
|
+
}
|
|
67
|
+
fmt.Printf("initialized Eldlock vault %q at %s using %s\n", normalizedName, resolver.PathForVault(normalizedName), provider.Name())
|
|
68
|
+
return nil
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func serve() error {
|
|
72
|
+
socketPath := defaultSocketPath()
|
|
73
|
+
stateDir := defaultStateDir()
|
|
74
|
+
|
|
75
|
+
provider, err := yubikey.NewProviderFromEnv()
|
|
76
|
+
if err != nil {
|
|
77
|
+
return err
|
|
78
|
+
}
|
|
79
|
+
resolver := api.FileStoreResolver{StateDir: stateDir, KeyProvider: provider}
|
|
80
|
+
defaultStore, err := resolver.StoreForVault(api.DefaultVaultName)
|
|
81
|
+
if err != nil {
|
|
82
|
+
return err
|
|
83
|
+
}
|
|
84
|
+
service := api.NewService(defaultStore)
|
|
85
|
+
service.StoreResolver = resolver
|
|
86
|
+
server := api.Server{
|
|
87
|
+
SocketPath: socketPath,
|
|
88
|
+
Service: service,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
92
|
+
defer stop()
|
|
93
|
+
|
|
94
|
+
fmt.Printf("eldlock-server listening on %s\n", socketPath)
|
|
95
|
+
fmt.Printf("encrypted vault state at %s\n", stateDir)
|
|
96
|
+
fmt.Printf("passkey provider %s\n", provider.Name())
|
|
97
|
+
return server.ListenAndServe(ctx)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func argAt(index int) string {
|
|
101
|
+
if len(os.Args) > index {
|
|
102
|
+
return os.Args[index]
|
|
103
|
+
}
|
|
104
|
+
return ""
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func defaultSocketPath() string {
|
|
108
|
+
if value := os.Getenv("ELDLOCK_SOCKET"); value != "" {
|
|
109
|
+
return value
|
|
110
|
+
}
|
|
111
|
+
sum := sha256.Sum256([]byte(defaultStateDir()))
|
|
112
|
+
digest := hex.EncodeToString(sum[:])[:16]
|
|
113
|
+
return filepath.Join(os.TempDir(), "eldlock", digest, "daemon.sock")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func defaultStorePath() string {
|
|
117
|
+
if value := os.Getenv("ELDLOCK_STORE"); value != "" {
|
|
118
|
+
return value
|
|
119
|
+
}
|
|
120
|
+
return filepath.Join(defaultStateDir(), "vault.json")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func defaultStateDir() string {
|
|
124
|
+
if value := os.Getenv("ELDLOCK_STATE_DIR"); value != "" {
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
cwd, err := os.Getwd()
|
|
128
|
+
if err == nil {
|
|
129
|
+
return filepath.Join(cwd, ".eldlock")
|
|
130
|
+
}
|
|
131
|
+
return filepath.Join(os.TempDir(), "eldlock")
|
|
132
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
2
|
+
github.com/keys-pub/go-libfido2 v1.5.3 h1:vtgHxlSB43u6lj0TSuA3VvT6z3E7VI+L1a2hvMFdECk=
|
|
3
|
+
github.com/keys-pub/go-libfido2 v1.5.3/go.mod h1:P0V19qHwJNY0htZwZDe9Ilvs/nokGhdFX7faKFyZ6+U=
|
|
4
|
+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
5
|
+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
6
|
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
7
|
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
8
|
+
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
9
|
+
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
10
|
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
11
|
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# API
|
|
2
|
+
|
|
3
|
+
Local Unix socket API for `eldlock-cli`, shell hooks, and other local integrations.
|
|
4
|
+
|
|
5
|
+
The API is not trusted by default. Every request must be authorized by the server-side policy engine.
|
|
6
|
+
|
|
7
|
+
Current testable commands:
|
|
8
|
+
|
|
9
|
+
- `init` creates the daemon-owned prototype vault.
|
|
10
|
+
- `secret.add` stores env and SSH secret material in the daemon-owned store.
|
|
11
|
+
- `secret.read` releases a secret as either plaintext output or a server-side clipboard write.
|
|
12
|
+
- `secret.list` returns secret names and types without values.
|
|
13
|
+
|
|
14
|
+
The CLI forwards user intent to this API. It must not decide policy, inspect approval state, or write clipboard contents itself.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
package api
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
|
|
8
|
+
"github.com/eldlock/eldlock-server/internal/store"
|
|
9
|
+
"github.com/eldlock/eldlock-server/internal/yubikey"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type SecretStore interface {
|
|
13
|
+
Init() error
|
|
14
|
+
IsInitialized() (bool, error)
|
|
15
|
+
Put(store.Secret) error
|
|
16
|
+
Get(string) (store.Secret, error)
|
|
17
|
+
Delete(string) error
|
|
18
|
+
List() ([]store.SecretSummary, error)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type PINSecretStore interface {
|
|
22
|
+
InitWithPIN(string) error
|
|
23
|
+
PutWithPIN(store.Secret, string) error
|
|
24
|
+
GetWithPIN(string, string) (store.Secret, error)
|
|
25
|
+
DeleteWithPIN(string, string) error
|
|
26
|
+
ListWithPIN(string) ([]store.SecretSummary, error)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type PINBulkSecretStore interface {
|
|
30
|
+
PutManyWithPIN([]store.Secret, string) error
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type EnvSecretStore interface {
|
|
34
|
+
EnvWithPIN(string) (map[string]string, error)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Clipboard interface {
|
|
38
|
+
Write(context.Context, string) error
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type StoreResolver interface {
|
|
42
|
+
StoreForVault(string) (SecretStore, error)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Service struct {
|
|
46
|
+
Store SecretStore
|
|
47
|
+
StoreResolver StoreResolver
|
|
48
|
+
Clipboard Clipboard
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func NewService(secretStore SecretStore) *Service {
|
|
52
|
+
return &Service{
|
|
53
|
+
Store: secretStore,
|
|
54
|
+
Clipboard: LocalClipboard{},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func (s *Service) Handle(ctx context.Context, req Request) Response {
|
|
59
|
+
service, response, ok := s.withRequestStore(req)
|
|
60
|
+
if !ok {
|
|
61
|
+
return response
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
switch req.Action {
|
|
65
|
+
case ActionHealth:
|
|
66
|
+
return Response{OK: true, Message: "eldlock-server ok"}
|
|
67
|
+
case ActionInit:
|
|
68
|
+
return service.initVault(req)
|
|
69
|
+
case ActionAddSecret:
|
|
70
|
+
return service.addSecret(req)
|
|
71
|
+
case ActionReadSecret:
|
|
72
|
+
return service.readSecret(ctx, req)
|
|
73
|
+
case ActionRemoveSecret:
|
|
74
|
+
return service.removeSecret(req)
|
|
75
|
+
case ActionListSecrets:
|
|
76
|
+
return service.listSecrets(req)
|
|
77
|
+
case ActionImportEnv:
|
|
78
|
+
return service.importEnv(req)
|
|
79
|
+
case ActionExec:
|
|
80
|
+
return service.execProcess(ctx, req)
|
|
81
|
+
default:
|
|
82
|
+
return failure("unknown action")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func (s *Service) withRequestStore(req Request) (*Service, Response, bool) {
|
|
87
|
+
if req.Action == ActionHealth || s.StoreResolver == nil {
|
|
88
|
+
return s, Response{}, true
|
|
89
|
+
}
|
|
90
|
+
secretStore, err := s.StoreResolver.StoreForVault(req.Vault)
|
|
91
|
+
if err != nil {
|
|
92
|
+
return nil, failure(fmt.Sprintf("resolve vault: %v", err)), false
|
|
93
|
+
}
|
|
94
|
+
scoped := *s
|
|
95
|
+
scoped.Store = secretStore
|
|
96
|
+
scoped.StoreResolver = nil
|
|
97
|
+
return &scoped, Response{}, true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func (s *Service) requireInitialized() Response {
|
|
101
|
+
initialized, err := s.Store.IsInitialized()
|
|
102
|
+
if err != nil {
|
|
103
|
+
return failure(fmt.Sprintf("check vault: %v", err))
|
|
104
|
+
}
|
|
105
|
+
if !initialized {
|
|
106
|
+
return failure("vault is not initialized; run eldlock init")
|
|
107
|
+
}
|
|
108
|
+
return Response{OK: true}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func failure(message string) Response {
|
|
112
|
+
return Response{OK: false, Error: message}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func pinAwareFailure(message string, err error) Response {
|
|
116
|
+
switch {
|
|
117
|
+
case errors.Is(err, yubikey.ErrPINRequired):
|
|
118
|
+
return Response{OK: false, Error: message, Code: "pin_required"}
|
|
119
|
+
case errors.Is(err, yubikey.ErrPINInvalid):
|
|
120
|
+
return Response{OK: false, Error: message, Code: "pin_invalid"}
|
|
121
|
+
case errors.Is(err, yubikey.ErrPINAuthBlocked):
|
|
122
|
+
return Response{OK: false, Error: message, Code: "pin_auth_blocked"}
|
|
123
|
+
default:
|
|
124
|
+
return failure(message)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
package api
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"os"
|
|
9
|
+
"os/exec"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func (s *Service) execProcess(ctx context.Context, req Request) Response {
|
|
13
|
+
if response := s.requireInitialized(); !response.OK {
|
|
14
|
+
return response
|
|
15
|
+
}
|
|
16
|
+
if !req.Interactive && len(req.Command) == 0 && req.ShellCommand == "" {
|
|
17
|
+
return failure("exec command is required")
|
|
18
|
+
}
|
|
19
|
+
if len(req.Command) > 0 && req.Command[0] == "" {
|
|
20
|
+
return failure("exec command cannot be empty")
|
|
21
|
+
}
|
|
22
|
+
envSecrets, err := s.envWithPIN(req.PIN)
|
|
23
|
+
if err != nil {
|
|
24
|
+
return pinAwareFailure(fmt.Sprintf("load exec environment: %v", err), err)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if req.Interactive {
|
|
28
|
+
return Response{OK: true, Env: envSecrets}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cmd := execCommand(ctx, req)
|
|
32
|
+
if req.CWD != "" {
|
|
33
|
+
cmd.Dir = req.CWD
|
|
34
|
+
}
|
|
35
|
+
cmd.Env = envList(overlayEnv(req.Env, envSecrets))
|
|
36
|
+
|
|
37
|
+
var stdout bytes.Buffer
|
|
38
|
+
var stderr bytes.Buffer
|
|
39
|
+
cmd.Stdout = &stdout
|
|
40
|
+
cmd.Stderr = &stderr
|
|
41
|
+
|
|
42
|
+
err = cmd.Run()
|
|
43
|
+
exitCode := 0
|
|
44
|
+
if err != nil {
|
|
45
|
+
var exitErr *exec.ExitError
|
|
46
|
+
if errors.As(err, &exitErr) {
|
|
47
|
+
exitCode = exitErr.ExitCode()
|
|
48
|
+
} else {
|
|
49
|
+
return failure(fmt.Sprintf("run exec command: %v", err))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Response{
|
|
54
|
+
OK: true,
|
|
55
|
+
Stdout: stdout.String(),
|
|
56
|
+
Stderr: stderr.String(),
|
|
57
|
+
ExitCode: exitCode,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func execCommand(ctx context.Context, req Request) *exec.Cmd {
|
|
62
|
+
if req.ShellCommand == "" {
|
|
63
|
+
return exec.CommandContext(ctx, req.Command[0], req.Command[1:]...)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
shell := req.Shell
|
|
67
|
+
if shell == "" {
|
|
68
|
+
shell = req.Env["SHELL"]
|
|
69
|
+
}
|
|
70
|
+
if shell == "" {
|
|
71
|
+
shell = os.Getenv("SHELL")
|
|
72
|
+
}
|
|
73
|
+
if shell == "" {
|
|
74
|
+
shell = "/bin/sh"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return exec.CommandContext(ctx, shell, "-lc", req.ShellCommand)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func overlayEnv(base map[string]string, values map[string]string) map[string]string {
|
|
81
|
+
env := make(map[string]string, len(base)+len(values))
|
|
82
|
+
for key, value := range base {
|
|
83
|
+
env[key] = value
|
|
84
|
+
}
|
|
85
|
+
for key, value := range values {
|
|
86
|
+
env[key] = value
|
|
87
|
+
}
|
|
88
|
+
return env
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func envList(env map[string]string) []string {
|
|
92
|
+
out := make([]string, 0, len(env))
|
|
93
|
+
for key, value := range env {
|
|
94
|
+
out = append(out, key+"="+value)
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
}
|