blob: bb9ac08e30d5dac0c773951875978a0a0c9bd00f [file] [log] [blame]
lb5tr716ecf62019-08-05 17:33:29 -07001package hkp
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "time"
10)
11
12// TODO(lb5tr): provide as flag
13var keyServers = []string{
14 "http://pool.sks-keyservers.net",
15 "http://keys.gnupg.net",
16}
17
18var (
19 PerServerTimeLimit = 5 * time.Second
20 PerServerRetryCount = 3
21)
22
23var ErrKeyNotFound = errors.New("not found on hkp servers")
24
25const startMarker string = "-----BEGIN PGP PUBLIC KEY BLOCK-----"
26const endMarker string = "-----END PGP PUBLIC KEY BLOCK-----"
27
28type Client interface {
29 GetKeyRing(ctx context.Context, keyID []byte) ([]byte, error)
30}
31
32type transport interface {
33 get(ctx context.Context, path string) ([]byte, error)
34}
35
36type httpTransport struct {
37}
38
39type HKP struct {
40 transport transport
41}
42
43func NewClient() Client {
44 client := HKP{
45 transport: httpTransport{},
46 }
47 return client
48}
49
50func (hkp HKP) GetKeyRing(ctx context.Context, keyID []byte) ([]byte, error) {
51 key := fmt.Sprintf("0x%x", keyID)
52 output := make(chan []byte)
53 errors := make(chan error)
54
55 go func() {
56 var lastError error
57 for _, server := range keyServers {
58 url := server + "/pks/lookup?op=get&search=" + key
59 for i := 0; i < PerServerRetryCount; i++ {
60 localCtx, cancel := context.WithTimeout(context.Background(), PerServerTimeLimit)
61 keyData, err := hkp.transport.get(localCtx, url)
62 cancel()
63
64 // ErrKeyNotFound is retriable. I've seen cases where upon retry
65 // server responds with key just fine
66
67 switch err {
68 case nil:
69 output <- keyData
70 return
71 case ctx.Err():
72 errors <- err
73 return
74 default:
75 lastError = err
76 }
77 }
78 }
79
80 errors <- lastError
81 }()
82
83 select {
84 case <-ctx.Done():
85 return nil, ctx.Err()
86 case finalError := <-errors:
87 return nil, finalError
88 case result := <-output:
89 return result, nil
90 }
91}
92
93func (httpTransport) get(ctx context.Context, url string) ([]byte, error) {
94 localCtx, cancel := context.WithTimeout(ctx, PerServerTimeLimit)
95 defer cancel()
96
97 req, err := http.NewRequest("GET", url, nil)
98 if err != nil {
99 return nil, fmt.Errorf("http.NewRequest(GET, %q): %v", url, err)
100 }
101
102 req = req.WithContext(localCtx)
103 client := http.DefaultClient
104 res, err := client.Do(req)
105
106 if err != nil {
107 return nil, fmt.Errorf("client.Do(%v): %v", req, err)
108 }
109
110 defer res.Body.Close()
111
112 if res.StatusCode != 200 {
113 if res.StatusCode == 404 {
114 return nil, ErrKeyNotFound
115 }
116
117 return nil, fmt.Errorf("got status code %d", res.StatusCode)
118 }
119
120 buf := bytes.NewBuffer([]byte{})
121 buf.ReadFrom(res.Body)
122 response := buf.Bytes()
123
124 start := bytes.Index(response, []byte(startMarker))
125 end := bytes.Index(response, []byte(endMarker))
126
127 if start == -1 || end == -1 {
128 return nil, fmt.Errorf("failed to read")
129 }
130
131 data := response[start : end+len(endMarker)]
132 return data, nil
133}