lb5tr | 716ecf6 | 2019-08-05 17:33:29 -0700 | [diff] [blame] | 1 | package hkp |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "net/http" |
| 9 | "time" |
| 10 | ) |
| 11 | |
| 12 | // TODO(lb5tr): provide as flag |
| 13 | var keyServers = []string{ |
| 14 | "http://pool.sks-keyservers.net", |
| 15 | "http://keys.gnupg.net", |
| 16 | } |
| 17 | |
| 18 | var ( |
| 19 | PerServerTimeLimit = 5 * time.Second |
| 20 | PerServerRetryCount = 3 |
| 21 | ) |
| 22 | |
| 23 | var ErrKeyNotFound = errors.New("not found on hkp servers") |
| 24 | |
| 25 | const startMarker string = "-----BEGIN PGP PUBLIC KEY BLOCK-----" |
| 26 | const endMarker string = "-----END PGP PUBLIC KEY BLOCK-----" |
| 27 | |
| 28 | type Client interface { |
| 29 | GetKeyRing(ctx context.Context, keyID []byte) ([]byte, error) |
| 30 | } |
| 31 | |
| 32 | type transport interface { |
| 33 | get(ctx context.Context, path string) ([]byte, error) |
| 34 | } |
| 35 | |
| 36 | type httpTransport struct { |
| 37 | } |
| 38 | |
| 39 | type HKP struct { |
| 40 | transport transport |
| 41 | } |
| 42 | |
| 43 | func NewClient() Client { |
| 44 | client := HKP{ |
| 45 | transport: httpTransport{}, |
| 46 | } |
| 47 | return client |
| 48 | } |
| 49 | |
| 50 | func (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 | |
| 93 | func (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 | } |