bgpwtf/cccampix/pgpencryptor: implement service
TODO:
* tests
Change-Id: I5d0506542070236a8ee879fcb54bc9518e23b5e3
diff --git a/bgpwtf/cccampix/pgpencryptor/hkp/BUILD.bazel b/bgpwtf/cccampix/pgpencryptor/hkp/BUILD.bazel
new file mode 100644
index 0000000..a2377da
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/hkp/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["hkp.go"],
+ importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor/hkp",
+ visibility = ["//visibility:public"],
+)
diff --git a/bgpwtf/cccampix/pgpencryptor/hkp/hkp.go b/bgpwtf/cccampix/pgpencryptor/hkp/hkp.go
new file mode 100644
index 0000000..bb9ac08
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/hkp/hkp.go
@@ -0,0 +1,133 @@
+package hkp
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "time"
+)
+
+// TODO(lb5tr): provide as flag
+var keyServers = []string{
+ "http://pool.sks-keyservers.net",
+ "http://keys.gnupg.net",
+}
+
+var (
+ PerServerTimeLimit = 5 * time.Second
+ PerServerRetryCount = 3
+)
+
+var ErrKeyNotFound = errors.New("not found on hkp servers")
+
+const startMarker string = "-----BEGIN PGP PUBLIC KEY BLOCK-----"
+const endMarker string = "-----END PGP PUBLIC KEY BLOCK-----"
+
+type Client interface {
+ GetKeyRing(ctx context.Context, keyID []byte) ([]byte, error)
+}
+
+type transport interface {
+ get(ctx context.Context, path string) ([]byte, error)
+}
+
+type httpTransport struct {
+}
+
+type HKP struct {
+ transport transport
+}
+
+func NewClient() Client {
+ client := HKP{
+ transport: httpTransport{},
+ }
+ return client
+}
+
+func (hkp HKP) GetKeyRing(ctx context.Context, keyID []byte) ([]byte, error) {
+ key := fmt.Sprintf("0x%x", keyID)
+ output := make(chan []byte)
+ errors := make(chan error)
+
+ go func() {
+ var lastError error
+ for _, server := range keyServers {
+ url := server + "/pks/lookup?op=get&search=" + key
+ for i := 0; i < PerServerRetryCount; i++ {
+ localCtx, cancel := context.WithTimeout(context.Background(), PerServerTimeLimit)
+ keyData, err := hkp.transport.get(localCtx, url)
+ cancel()
+
+ // ErrKeyNotFound is retriable. I've seen cases where upon retry
+ // server responds with key just fine
+
+ switch err {
+ case nil:
+ output <- keyData
+ return
+ case ctx.Err():
+ errors <- err
+ return
+ default:
+ lastError = err
+ }
+ }
+ }
+
+ errors <- lastError
+ }()
+
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case finalError := <-errors:
+ return nil, finalError
+ case result := <-output:
+ return result, nil
+ }
+}
+
+func (httpTransport) get(ctx context.Context, url string) ([]byte, error) {
+ localCtx, cancel := context.WithTimeout(ctx, PerServerTimeLimit)
+ defer cancel()
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("http.NewRequest(GET, %q): %v", url, err)
+ }
+
+ req = req.WithContext(localCtx)
+ client := http.DefaultClient
+ res, err := client.Do(req)
+
+ if err != nil {
+ return nil, fmt.Errorf("client.Do(%v): %v", req, err)
+ }
+
+ defer res.Body.Close()
+
+ if res.StatusCode != 200 {
+ if res.StatusCode == 404 {
+ return nil, ErrKeyNotFound
+ }
+
+ return nil, fmt.Errorf("got status code %d", res.StatusCode)
+ }
+
+ buf := bytes.NewBuffer([]byte{})
+ buf.ReadFrom(res.Body)
+ response := buf.Bytes()
+
+ start := bytes.Index(response, []byte(startMarker))
+ end := bytes.Index(response, []byte(endMarker))
+
+ if start == -1 || end == -1 {
+ return nil, fmt.Errorf("failed to read")
+ }
+
+ data := response[start : end+len(endMarker)]
+ return data, nil
+}