-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclient.go
157 lines (133 loc) · 4.02 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: MPL-2.0
package ranger
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"strconv"
"github.com/cockroachdb/errors"
"github.com/rs/zerolog/log"
"go.mondoo.com/ranger-rpc/status"
"google.golang.org/protobuf/proto"
"moul.io/http2curl"
)
// ClientPlugin is a plugin that can be used to modify the http headers of a request.
type ClientPlugin interface {
GetName() string
GetHeader(content []byte) http.Header
}
// Client is the client for the Ranger service. It is used as the base client for all service calls.
type Client struct {
plugins []ClientPlugin
}
// AddPlugin adds a client plugin.
// Deprecated: use AddPlugins instead.
func (c *Client) AddPlugin(plugin ClientPlugin) {
c.AddPlugins(plugin)
}
// AddPlugins adds one or many client plugins.
func (c *Client) AddPlugins(plugins ...ClientPlugin) {
for i := range plugins {
c.plugins = append(c.plugins, plugins[i])
}
}
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// DoClientRequest makes a request to the Ranger service.
// It will marshal the proto.Message into the request body, do the request and then parse the response into the
// response proto.Message.
func (c *Client) DoClientRequest(ctx context.Context, client HTTPClient, url string, in, out proto.Message) (err error) {
reqBodyBytes, err := proto.Marshal(in)
if err != nil {
return errors.Wrap(err, "failed to marshal proto request")
}
if err = ctx.Err(); err != nil {
return errors.Wrap(err, "aborted because context was done")
}
header := make(http.Header)
header.Set("Accept", "application/protobuf")
header.Set("Content-Type", "application/protobuf")
for i := range c.plugins {
p := c.plugins[i]
pluginHeader := p.GetHeader(reqBodyBytes)
for k, v := range pluginHeader {
// check if we overwrite an existing header
header[k] = v
}
}
reader := bytes.NewReader(reqBodyBytes)
req, err := http.NewRequest("POST", url, reader)
if err != nil {
return err
}
req = req.WithContext(ctx)
req.Header = header
// trace curl request
if log.Trace().Enabled() {
c.PrintTraceCurlCommand(url, in)
}
// do http call
resp, err := client.Do(req)
if err != nil {
return errors.Wrap(err, "failed to do request")
}
defer func() {
cerr := resp.Body.Close()
if err == nil && cerr != nil {
err = errors.Wrap(cerr, "failed to close response body")
}
}()
if err = ctx.Err(); err != nil {
return errors.Wrap(err, "aborted because context was done")
}
if resp.StatusCode != 200 {
// TODO wrap body in error
spb, err := parseStatus(resp.Body)
if err == nil {
log.Debug().Str("body", spb.Message).Int("status", resp.StatusCode).Msg("non-ok http request")
return status.FromProto(spb).Err()
} else {
payload, err := io.ReadAll(reader)
if err != nil {
log.Error().Err(err).Msg("could not parse http body")
}
return status.New(status.CodeFromHTTPStatus(resp.StatusCode), string(payload)).Err()
}
}
if err = ctx.Err(); err != nil {
return errors.Wrap(err, "aborted because context was done")
}
respBodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "failed to read response body")
}
if err = proto.Unmarshal(respBodyBytes, out); err != nil {
return errors.Wrap(err, "failed to unmarshal proto response")
}
return nil
}
func (c *Client) PrintTraceCurlCommand(url string, in proto.Message) {
// for better debuggability we try to construct an equivalent json request
jsonBytes, err := json.Marshal(in)
if err != nil {
log.Error().Err(err).Msg("could not generate trace http log")
}
header := make(http.Header)
header.Set("Accept", "application/json")
header.Set("Content-Type", "application/json")
header.Set("Content-Length", strconv.Itoa(len(jsonBytes)))
// create http request
reader := bytes.NewReader(jsonBytes)
req, err := http.NewRequest("POST", url, reader)
if err != nil {
return
}
req.Header = header
// convert request to curl command
command, _ := http2curl.GetCurlCommand(req)
log.Trace().Msg(command.String())
}