@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
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
package api
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
"unicode"
|
|
11
|
+
|
|
12
|
+
"github.com/eldlock/eldlock-server/internal/store"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func (s *Service) addSecret(req Request) Response {
|
|
16
|
+
if response := s.requireInitialized(); !response.OK {
|
|
17
|
+
return response
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
name := strings.TrimSpace(req.Name)
|
|
21
|
+
if name == "" {
|
|
22
|
+
return failure("secret name is required")
|
|
23
|
+
}
|
|
24
|
+
if req.Type != store.SecretTypeEnv && req.Type != store.SecretTypeSSH {
|
|
25
|
+
return failure("secret type must be env or ssh")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
value := req.Value
|
|
29
|
+
if req.SourcePath != "" {
|
|
30
|
+
raw, err := os.ReadFile(req.SourcePath)
|
|
31
|
+
if err != nil {
|
|
32
|
+
return failure(fmt.Sprintf("read source file: %v", err))
|
|
33
|
+
}
|
|
34
|
+
value = string(raw)
|
|
35
|
+
}
|
|
36
|
+
if value == "" {
|
|
37
|
+
return failure("secret value is required")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if err := s.putWithPIN(store.Secret{Name: name, Type: req.Type, Value: value}, req.PIN); err != nil {
|
|
41
|
+
return pinAwareFailure(fmt.Sprintf("store secret: %v", err), err)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Response{OK: true, Message: fmt.Sprintf("stored %s secret %q", req.Type, name)}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func (s *Service) readSecret(ctx context.Context, req Request) Response {
|
|
48
|
+
if response := s.requireInitialized(); !response.OK {
|
|
49
|
+
return response
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
name := strings.TrimSpace(req.Name)
|
|
53
|
+
if name == "" {
|
|
54
|
+
return failure("secret name is required")
|
|
55
|
+
}
|
|
56
|
+
if req.Output != OutputPlain && req.Output != OutputClipboard {
|
|
57
|
+
return failure("read output must be plain or clipboard")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
secret, err := s.getWithPIN(name, req.PIN)
|
|
61
|
+
if errors.Is(err, store.ErrNotFound) {
|
|
62
|
+
return failure("secret not found")
|
|
63
|
+
}
|
|
64
|
+
if err != nil {
|
|
65
|
+
return pinAwareFailure(fmt.Sprintf("read secret: %v", err), err)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
switch req.Output {
|
|
69
|
+
case OutputPlain:
|
|
70
|
+
return Response{OK: true, Value: secret.Value}
|
|
71
|
+
case OutputClipboard:
|
|
72
|
+
if s.Clipboard == nil {
|
|
73
|
+
return failure("clipboard is not configured")
|
|
74
|
+
}
|
|
75
|
+
if err := s.Clipboard.Write(ctx, secret.Value); err != nil {
|
|
76
|
+
return failure(fmt.Sprintf("write clipboard: %v", err))
|
|
77
|
+
}
|
|
78
|
+
return Response{OK: true, Message: fmt.Sprintf("copied %q to clipboard", name)}
|
|
79
|
+
default:
|
|
80
|
+
return failure("read output must be plain or clipboard")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func (s *Service) removeSecret(req Request) Response {
|
|
85
|
+
if response := s.requireInitialized(); !response.OK {
|
|
86
|
+
return response
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
name := strings.TrimSpace(req.Name)
|
|
90
|
+
if name == "" {
|
|
91
|
+
return failure("secret name is required")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if err := s.deleteWithPIN(name, req.PIN); errors.Is(err, store.ErrNotFound) {
|
|
95
|
+
return failure("secret not found")
|
|
96
|
+
} else if err != nil {
|
|
97
|
+
return pinAwareFailure(fmt.Sprintf("remove secret: %v", err), err)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Response{OK: true, Message: fmt.Sprintf("removed secret %q", name)}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func (s *Service) listSecrets(req Request) Response {
|
|
104
|
+
if response := s.requireInitialized(); !response.OK {
|
|
105
|
+
return response
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
secrets, err := s.listWithPIN(req.PIN)
|
|
109
|
+
if err != nil {
|
|
110
|
+
return pinAwareFailure(fmt.Sprintf("list secrets: %v", err), err)
|
|
111
|
+
}
|
|
112
|
+
return Response{OK: true, Secrets: secrets}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func (s *Service) importEnv(req Request) Response {
|
|
116
|
+
if response := s.requireInitialized(); !response.OK {
|
|
117
|
+
return response
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
sourcePath := strings.TrimSpace(req.SourcePath)
|
|
121
|
+
if sourcePath == "" {
|
|
122
|
+
sourcePath = ".env"
|
|
123
|
+
}
|
|
124
|
+
if !filepath.IsAbs(sourcePath) && req.CWD != "" {
|
|
125
|
+
sourcePath = filepath.Join(req.CWD, sourcePath)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
raw, err := os.ReadFile(sourcePath)
|
|
129
|
+
if err != nil {
|
|
130
|
+
return failure(fmt.Sprintf("read env file: %v", err))
|
|
131
|
+
}
|
|
132
|
+
values, err := parseEnvFile(string(raw))
|
|
133
|
+
if err != nil {
|
|
134
|
+
return failure(fmt.Sprintf("parse env file: %v", err))
|
|
135
|
+
}
|
|
136
|
+
if len(values) == 0 {
|
|
137
|
+
return failure("env file did not contain any variables")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
secrets := make([]store.Secret, 0, len(values))
|
|
141
|
+
for name, value := range values {
|
|
142
|
+
secrets = append(secrets, store.Secret{Name: name, Type: store.SecretTypeEnv, Value: value})
|
|
143
|
+
}
|
|
144
|
+
if err := s.putManyWithPIN(secrets, req.PIN); err != nil {
|
|
145
|
+
return pinAwareFailure(fmt.Sprintf("import env file: %v", err), err)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return Response{OK: true, Imported: len(secrets), Message: fmt.Sprintf("imported %d env secrets", len(secrets))}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func (s *Service) putWithPIN(secret store.Secret, pin string) error {
|
|
152
|
+
if store, ok := s.Store.(PINSecretStore); ok {
|
|
153
|
+
return store.PutWithPIN(secret, pin)
|
|
154
|
+
}
|
|
155
|
+
return s.Store.Put(secret)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func (s *Service) putManyWithPIN(secrets []store.Secret, pin string) error {
|
|
159
|
+
if store, ok := s.Store.(PINBulkSecretStore); ok {
|
|
160
|
+
return store.PutManyWithPIN(secrets, pin)
|
|
161
|
+
}
|
|
162
|
+
for _, secret := range secrets {
|
|
163
|
+
if err := s.putWithPIN(secret, pin); err != nil {
|
|
164
|
+
return err
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return nil
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func (s *Service) getWithPIN(name string, pin string) (store.Secret, error) {
|
|
171
|
+
if store, ok := s.Store.(PINSecretStore); ok {
|
|
172
|
+
return store.GetWithPIN(name, pin)
|
|
173
|
+
}
|
|
174
|
+
return s.Store.Get(name)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func (s *Service) deleteWithPIN(name string, pin string) error {
|
|
178
|
+
if store, ok := s.Store.(PINSecretStore); ok {
|
|
179
|
+
return store.DeleteWithPIN(name, pin)
|
|
180
|
+
}
|
|
181
|
+
return s.Store.Delete(name)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func (s *Service) listWithPIN(pin string) ([]store.SecretSummary, error) {
|
|
185
|
+
if store, ok := s.Store.(PINSecretStore); ok {
|
|
186
|
+
return store.ListWithPIN(pin)
|
|
187
|
+
}
|
|
188
|
+
return s.Store.List()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
func (s *Service) envWithPIN(pin string) (map[string]string, error) {
|
|
192
|
+
if store, ok := s.Store.(EnvSecretStore); ok {
|
|
193
|
+
return store.EnvWithPIN(pin)
|
|
194
|
+
}
|
|
195
|
+
secrets, err := s.Store.List()
|
|
196
|
+
if err != nil {
|
|
197
|
+
return nil, err
|
|
198
|
+
}
|
|
199
|
+
env := make(map[string]string)
|
|
200
|
+
for _, summary := range secrets {
|
|
201
|
+
if summary.Type == store.SecretTypeEnv {
|
|
202
|
+
secret, err := s.getWithPIN(summary.Name, pin)
|
|
203
|
+
if err != nil {
|
|
204
|
+
return nil, err
|
|
205
|
+
}
|
|
206
|
+
env[summary.Name] = secret.Value
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return env, nil
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
func parseEnvFile(contents string) (map[string]string, error) {
|
|
213
|
+
out := map[string]string{}
|
|
214
|
+
for index, rawLine := range strings.Split(contents, "\n") {
|
|
215
|
+
lineNumber := index + 1
|
|
216
|
+
line := strings.TrimSpace(strings.TrimSuffix(rawLine, "\r"))
|
|
217
|
+
if line == "" || strings.HasPrefix(line, "#") {
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
if strings.HasPrefix(line, "export ") {
|
|
221
|
+
line = strings.TrimSpace(strings.TrimPrefix(line, "export "))
|
|
222
|
+
}
|
|
223
|
+
key, value, ok := strings.Cut(line, "=")
|
|
224
|
+
if !ok {
|
|
225
|
+
return nil, fmt.Errorf("line %d: expected KEY=value", lineNumber)
|
|
226
|
+
}
|
|
227
|
+
key = strings.TrimSpace(key)
|
|
228
|
+
if !validEnvKey(key) {
|
|
229
|
+
return nil, fmt.Errorf("line %d: invalid env key", lineNumber)
|
|
230
|
+
}
|
|
231
|
+
parsed, err := parseEnvValue(value)
|
|
232
|
+
if err != nil {
|
|
233
|
+
return nil, fmt.Errorf("line %d: %w", lineNumber, err)
|
|
234
|
+
}
|
|
235
|
+
out[key] = parsed
|
|
236
|
+
}
|
|
237
|
+
return out, nil
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
func parseEnvValue(value string) (string, error) {
|
|
241
|
+
value = strings.TrimSpace(value)
|
|
242
|
+
if value == "" {
|
|
243
|
+
return "", nil
|
|
244
|
+
}
|
|
245
|
+
switch value[0] {
|
|
246
|
+
case '\'':
|
|
247
|
+
return parseSingleQuotedEnv(value)
|
|
248
|
+
case '"':
|
|
249
|
+
inner, err := parseDoubleQuotedEnv(value)
|
|
250
|
+
if err != nil {
|
|
251
|
+
return "", err
|
|
252
|
+
}
|
|
253
|
+
return unescapeDoubleQuotedEnv(inner)
|
|
254
|
+
default:
|
|
255
|
+
return strings.TrimSpace(stripInlineEnvComment(value)), nil
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func parseSingleQuotedEnv(value string) (string, error) {
|
|
260
|
+
closing := strings.IndexRune(value[1:], '\'')
|
|
261
|
+
if closing < 0 {
|
|
262
|
+
return "", errors.New("unterminated single-quoted value")
|
|
263
|
+
}
|
|
264
|
+
closing++
|
|
265
|
+
if err := validateQuotedEnvTail(value[closing+1:]); err != nil {
|
|
266
|
+
return "", err
|
|
267
|
+
}
|
|
268
|
+
return value[1:closing], nil
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
func parseDoubleQuotedEnv(value string) (string, error) {
|
|
272
|
+
escaped := false
|
|
273
|
+
for index, char := range value[1:] {
|
|
274
|
+
actualIndex := index + 1
|
|
275
|
+
if escaped {
|
|
276
|
+
escaped = false
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
if char == '\\' {
|
|
280
|
+
escaped = true
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
if char == '"' {
|
|
284
|
+
if err := validateQuotedEnvTail(value[actualIndex+1:]); err != nil {
|
|
285
|
+
return "", err
|
|
286
|
+
}
|
|
287
|
+
return value[1:actualIndex], nil
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return "", errors.New("unterminated double-quoted value")
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func validateQuotedEnvTail(tail string) error {
|
|
294
|
+
tail = strings.TrimSpace(tail)
|
|
295
|
+
if tail == "" || strings.HasPrefix(tail, "#") {
|
|
296
|
+
return nil
|
|
297
|
+
}
|
|
298
|
+
return errors.New("unexpected characters after quoted value")
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
func stripInlineEnvComment(value string) string {
|
|
302
|
+
for index, char := range value {
|
|
303
|
+
if char == '#' && (index == 0 || unicode.IsSpace(rune(value[index-1]))) {
|
|
304
|
+
return value[:index]
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return value
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
func unescapeDoubleQuotedEnv(value string) (string, error) {
|
|
311
|
+
var builder strings.Builder
|
|
312
|
+
escaped := false
|
|
313
|
+
for _, char := range value {
|
|
314
|
+
if escaped {
|
|
315
|
+
switch char {
|
|
316
|
+
case 'n':
|
|
317
|
+
builder.WriteByte('\n')
|
|
318
|
+
case 'r':
|
|
319
|
+
builder.WriteByte('\r')
|
|
320
|
+
case 't':
|
|
321
|
+
builder.WriteByte('\t')
|
|
322
|
+
case '"', '\\', '$':
|
|
323
|
+
builder.WriteRune(char)
|
|
324
|
+
default:
|
|
325
|
+
builder.WriteRune(char)
|
|
326
|
+
}
|
|
327
|
+
escaped = false
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
330
|
+
if char == '\\' {
|
|
331
|
+
escaped = true
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
builder.WriteRune(char)
|
|
335
|
+
}
|
|
336
|
+
if escaped {
|
|
337
|
+
builder.WriteByte('\\')
|
|
338
|
+
}
|
|
339
|
+
return builder.String(), nil
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
func validEnvKey(key string) bool {
|
|
343
|
+
if key == "" {
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
for index, char := range key {
|
|
347
|
+
if index == 0 {
|
|
348
|
+
if char != '_' && !unicode.IsLetter(char) {
|
|
349
|
+
return false
|
|
350
|
+
}
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
if char != '_' && !unicode.IsLetter(char) && !unicode.IsDigit(char) {
|
|
354
|
+
return false
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return true
|
|
358
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package api
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"fmt"
|
|
8
|
+
"net"
|
|
9
|
+
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type Server struct {
|
|
14
|
+
SocketPath string
|
|
15
|
+
Service *Service
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func (s *Server) ListenAndServe(ctx context.Context) error {
|
|
19
|
+
if s.SocketPath == "" {
|
|
20
|
+
return fmt.Errorf("socket path is required")
|
|
21
|
+
}
|
|
22
|
+
if s.Service == nil {
|
|
23
|
+
return fmt.Errorf("service is required")
|
|
24
|
+
}
|
|
25
|
+
if err := os.MkdirAll(filepath.Dir(s.SocketPath), 0o700); err != nil {
|
|
26
|
+
return fmt.Errorf("create socket directory: %w", err)
|
|
27
|
+
}
|
|
28
|
+
if err := os.Remove(s.SocketPath); err != nil && !os.IsNotExist(err) {
|
|
29
|
+
return fmt.Errorf("remove old socket: %w", err)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
listener, err := net.Listen("unix", s.SocketPath)
|
|
33
|
+
if err != nil {
|
|
34
|
+
return fmt.Errorf("listen on socket: %w", err)
|
|
35
|
+
}
|
|
36
|
+
defer listener.Close()
|
|
37
|
+
defer os.Remove(s.SocketPath)
|
|
38
|
+
|
|
39
|
+
if unixListener, ok := listener.(*net.UnixListener); ok {
|
|
40
|
+
go func() {
|
|
41
|
+
<-ctx.Done()
|
|
42
|
+
_ = unixListener.Close()
|
|
43
|
+
}()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for {
|
|
47
|
+
conn, err := listener.Accept()
|
|
48
|
+
if err != nil {
|
|
49
|
+
if ctx.Err() != nil {
|
|
50
|
+
return nil
|
|
51
|
+
}
|
|
52
|
+
return fmt.Errorf("accept connection: %w", err)
|
|
53
|
+
}
|
|
54
|
+
go s.handleConn(ctx, conn)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func (s *Server) handleConn(ctx context.Context, conn net.Conn) {
|
|
59
|
+
defer conn.Close()
|
|
60
|
+
|
|
61
|
+
scanner := bufio.NewScanner(conn)
|
|
62
|
+
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
|
63
|
+
encoder := json.NewEncoder(conn)
|
|
64
|
+
for scanner.Scan() {
|
|
65
|
+
var req Request
|
|
66
|
+
if err := json.Unmarshal(scanner.Bytes(), &req); err != nil {
|
|
67
|
+
_ = encoder.Encode(failure("invalid request json"))
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
_ = encoder.Encode(s.Service.Handle(ctx, req))
|
|
71
|
+
}
|
|
72
|
+
}
|