Skip to content

Commit acab3e5

Browse files
authored
plugin: add framework to implement plugins (#580)
Fixes #485
1 parent a8de3de commit acab3e5

File tree

5 files changed

+800
-96
lines changed

5 files changed

+800
-96
lines changed

cmd/age/age_test.go

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
package main
66

77
import (
8-
"bufio"
98
"os"
109
"testing"
1110

1211
"filippo.io/age"
12+
"filippo.io/age/plugin"
1313
"github.com/rogpeppe/go-internal/testscript"
1414
)
1515

@@ -30,51 +30,31 @@ func TestMain(m *testing.M) {
3030
return 0
3131
},
3232
"age-plugin-test": func() (exitCode int) {
33-
// TODO: use plugin server package once it's available.
34-
switch os.Args[1] {
35-
case "--age-plugin=recipient-v1":
36-
scanner := bufio.NewScanner(os.Stdin)
37-
scanner.Scan() // add-recipient
38-
scanner.Scan() // body
39-
scanner.Scan() // grease
40-
scanner.Scan() // body
41-
scanner.Scan() // wrap-file-key
42-
scanner.Scan() // body
43-
fileKey := scanner.Text()
44-
scanner.Scan() // extension-labels
45-
scanner.Scan() // body
46-
scanner.Scan() // done
47-
scanner.Scan() // body
48-
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
49-
os.Stdout.WriteString(fileKey + "\n")
50-
scanner.Scan() // ok
51-
scanner.Scan() // body
52-
os.Stdout.WriteString("-> done\n\n")
53-
return 0
54-
case "--age-plugin=identity-v1":
55-
scanner := bufio.NewScanner(os.Stdin)
56-
scanner.Scan() // add-identity
57-
scanner.Scan() // body
58-
scanner.Scan() // grease
59-
scanner.Scan() // body
60-
scanner.Scan() // recipient-stanza
61-
scanner.Scan() // body
62-
fileKey := scanner.Text()
63-
scanner.Scan() // done
64-
scanner.Scan() // body
65-
os.Stdout.WriteString("-> file-key 0\n")
66-
os.Stdout.WriteString(fileKey + "\n")
67-
scanner.Scan() // ok
68-
scanner.Scan() // body
69-
os.Stdout.WriteString("-> done\n\n")
70-
return 0
71-
default:
72-
return 1
73-
}
33+
p, _ := plugin.New("test")
34+
p.HandleRecipient(func(data []byte) (age.Recipient, error) {
35+
return testPlugin{}, nil
36+
})
37+
p.HandleIdentity(func(data []byte) (age.Identity, error) {
38+
return testPlugin{}, nil
39+
})
40+
return p.Main()
7441
},
7542
}))
7643
}
7744

45+
type testPlugin struct{}
46+
47+
func (testPlugin) Wrap(fileKey []byte) ([]*age.Stanza, error) {
48+
return []*age.Stanza{{Type: "test", Body: fileKey}}, nil
49+
}
50+
51+
func (testPlugin) Unwrap(ss []*age.Stanza) ([]byte, error) {
52+
if len(ss) == 1 && ss[0].Type == "test" {
53+
return ss[0].Body, nil
54+
}
55+
return nil, age.ErrIncorrectIdentity
56+
}
57+
7858
func TestScript(t *testing.T) {
7959
testscript.Run(t, testscript.Params{
8060
Dir: "testdata",

plugin/client.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
// license that can be found in the LICENSE file or at
55
// https://developers.google.com/open-source/licenses/bsd
66

7-
// Package plugin implements the age plugin protocol.
87
package plugin
98

109
import (
@@ -53,6 +52,15 @@ func (r *Recipient) Name() string {
5352
return r.name
5453
}
5554

55+
// String returns the recipient encoding string ("age1name1...") or
56+
// "<identity-based recipient>" if r was created by [Identity.Recipient].
57+
func (r *Recipient) String() string {
58+
if r.identity {
59+
return "<identity-based recipient>"
60+
}
61+
return r.encoding
62+
}
63+
5664
func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
5765
stanzas, _, err = r.WrapWithLabels(fileKey)
5866
return
@@ -79,7 +87,7 @@ func (r *Recipient) WrapWithLabels(fileKey []byte) (stanzas []*age.Stanza, label
7987
if err := writeStanza(conn, addType, r.encoding); err != nil {
8088
return nil, nil, err
8189
}
82-
if err := writeStanza(conn, fmt.Sprintf("grease-%x", rand.Int())); err != nil {
90+
if _, err := writeGrease(conn); err != nil {
8391
return nil, nil, err
8492
}
8593
if err := writeStanzaWithBody(conn, "wrap-file-key", fileKey); err != nil {
@@ -194,6 +202,11 @@ func (i *Identity) Name() string {
194202
return i.name
195203
}
196204

205+
// String returns the identity encoding string ("AGE-PLUGIN-NAME-1...").
206+
func (i *Identity) String() string {
207+
return i.encoding
208+
}
209+
197210
// Recipient returns a Recipient wrapping this identity. When that Recipient is
198211
// used to encrypt a file key, the identity encoding is provided as-is to the
199212
// plugin, which is expected to support encrypting to identities.
@@ -223,7 +236,7 @@ func (i *Identity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
223236
if err := writeStanza(conn, "add-identity", i.encoding); err != nil {
224237
return nil, err
225238
}
226-
if err := writeStanza(conn, fmt.Sprintf("grease-%x", rand.Int())); err != nil {
239+
if _, err := writeGrease(conn); err != nil {
227240
return nil, err
228241
}
229242
for _, rs := range stanzas {
@@ -453,3 +466,18 @@ func writeStanzaWithBody(conn io.Writer, t string, body []byte) error {
453466
s := &format.Stanza{Type: t, Body: body}
454467
return s.Marshal(conn)
455468
}
469+
470+
func writeGrease(conn io.Writer) (sent bool, err error) {
471+
if rand.Intn(3) == 0 {
472+
return false, nil
473+
}
474+
s := &format.Stanza{Type: fmt.Sprintf("grease-%x", rand.Int())}
475+
for i := 0; i < rand.Intn(3); i++ {
476+
s.Args = append(s.Args, fmt.Sprintf("%d", rand.Intn(100)))
477+
}
478+
if rand.Intn(2) == 0 {
479+
s.Body = make([]byte, rand.Intn(100))
480+
rand.Read(s.Body)
481+
}
482+
return true, s.Marshal(conn)
483+
}

plugin/client_test.go

Lines changed: 28 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
package plugin
88

99
import (
10-
"bufio"
1110
"io"
1211
"os"
1312
"path/filepath"
@@ -20,63 +19,41 @@ import (
2019

2120
func TestMain(m *testing.M) {
2221
switch filepath.Base(os.Args[0]) {
23-
// TODO: deduplicate from cmd/age TestMain.
2422
case "age-plugin-test":
25-
switch os.Args[1] {
26-
case "--age-plugin=recipient-v1":
27-
scanner := bufio.NewScanner(os.Stdin)
28-
scanner.Scan() // add-recipient
29-
scanner.Scan() // body
30-
scanner.Scan() // grease
31-
scanner.Scan() // body
32-
scanner.Scan() // wrap-file-key
33-
scanner.Scan() // body
34-
fileKey := scanner.Text()
35-
scanner.Scan() // extension-labels
36-
scanner.Scan() // body
37-
scanner.Scan() // done
38-
scanner.Scan() // body
39-
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
40-
os.Stdout.WriteString(fileKey + "\n")
41-
scanner.Scan() // ok
42-
scanner.Scan() // body
43-
os.Stdout.WriteString("-> done\n\n")
44-
os.Exit(0)
45-
default:
46-
panic(os.Args[1])
47-
}
23+
p, _ := New("test")
24+
p.HandleRecipient(func(data []byte) (age.Recipient, error) {
25+
return testRecipient{}, nil
26+
})
27+
os.Exit(p.Main())
4828
case "age-plugin-testpqc":
49-
switch os.Args[1] {
50-
case "--age-plugin=recipient-v1":
51-
scanner := bufio.NewScanner(os.Stdin)
52-
scanner.Scan() // add-recipient
53-
scanner.Scan() // body
54-
scanner.Scan() // grease
55-
scanner.Scan() // body
56-
scanner.Scan() // wrap-file-key
57-
scanner.Scan() // body
58-
fileKey := scanner.Text()
59-
scanner.Scan() // extension-labels
60-
scanner.Scan() // body
61-
scanner.Scan() // done
62-
scanner.Scan() // body
63-
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
64-
os.Stdout.WriteString(fileKey + "\n")
65-
scanner.Scan() // ok
66-
scanner.Scan() // body
67-
os.Stdout.WriteString("-> labels postquantum\n\n")
68-
scanner.Scan() // ok
69-
scanner.Scan() // body
70-
os.Stdout.WriteString("-> done\n\n")
71-
os.Exit(0)
72-
default:
73-
panic(os.Args[1])
74-
}
29+
p, _ := New("testpqc")
30+
p.HandleRecipient(func(data []byte) (age.Recipient, error) {
31+
return testPQCRecipient{}, nil
32+
})
33+
os.Exit(p.Main())
7534
default:
7635
os.Exit(m.Run())
7736
}
7837
}
7938

39+
type testRecipient struct{}
40+
41+
func (testRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
42+
return []*age.Stanza{{Type: "test", Body: fileKey}}, nil
43+
}
44+
45+
type testPQCRecipient struct{}
46+
47+
var _ age.RecipientWithLabels = testPQCRecipient{}
48+
49+
func (testPQCRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
50+
return []*age.Stanza{{Type: "test", Body: fileKey}}, nil
51+
}
52+
53+
func (testPQCRecipient) WrapWithLabels(fileKey []byte) ([]*age.Stanza, []string, error) {
54+
return []*age.Stanza{{Type: "test", Body: fileKey}}, []string{"postquantum"}, nil
55+
}
56+
8057
func TestLabels(t *testing.T) {
8158
if runtime.GOOS == "windows" {
8259
t.Skip("Windows support is TODO")

plugin/example_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package plugin_test
2+
3+
import (
4+
"log"
5+
"os"
6+
7+
"filippo.io/age"
8+
"filippo.io/age/plugin"
9+
)
10+
11+
type Recipient struct{}
12+
13+
func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
14+
panic("unimplemented")
15+
}
16+
17+
func NewRecipient(data []byte) (*Recipient, error) {
18+
return &Recipient{}, nil
19+
}
20+
21+
type Identity struct{}
22+
23+
func (i *Identity) Unwrap(s []*age.Stanza) ([]byte, error) {
24+
panic("unimplemented")
25+
}
26+
27+
func NewIdentity(data []byte) (*Identity, error) {
28+
return &Identity{}, nil
29+
}
30+
31+
func ExamplePlugin_main() {
32+
p, err := plugin.New("example")
33+
if err != nil {
34+
log.Fatal(err)
35+
}
36+
p.HandleRecipient(func(data []byte) (age.Recipient, error) {
37+
return NewRecipient(data)
38+
})
39+
p.HandleIdentity(func(data []byte) (age.Identity, error) {
40+
return NewIdentity(data)
41+
})
42+
os.Exit(p.Main())
43+
}

0 commit comments

Comments
 (0)