-
Notifications
You must be signed in to change notification settings - Fork 38
/
Copy pathrpc.go
368 lines (326 loc) · 10.8 KB
/
rpc.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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
package rpc
import (
"bytes"
"context"
"errors"
"fmt"
"hash"
"io"
"net/http"
"net/url"
"reflect"
"strings"
"time"
"github.com/bmc-toolbox/bmclib/v2/internal/httpclient"
"github.com/bmc-toolbox/bmclib/v2/providers"
"github.com/go-logr/logr"
"github.com/jacobweinstock/registrar"
)
const (
// ProviderName for the RPC implementation.
ProviderName = "rpc"
// ProviderProtocol for the rpc implementation.
ProviderProtocol = "http"
// defaults
timestampHeader = "X-BMCLIB-Timestamp"
signatureHeader = "X-BMCLIB-Signature"
contentType = "application/json"
maxContentLenAllowed = 512 << (10 * 1) // 512KB
// SHA256 is the SHA256 algorithm.
SHA256 Algorithm = "sha256"
// SHA256Short is the short version of the SHA256 algorithm.
SHA256Short Algorithm = "256"
// SHA512 is the SHA512 algorithm.
SHA512 Algorithm = "sha512"
// SHA512Short is the short version of the SHA512 algorithm.
SHA512Short Algorithm = "512"
)
// Features implemented by the RPC provider.
var Features = registrar.Features{
providers.FeaturePowerSet,
providers.FeaturePowerState,
providers.FeatureBootDeviceSet,
}
// Algorithm is the type for HMAC algorithms.
type Algorithm string
// Secrets hold per algorithm slice secrets.
// These secrets will be used to create HMAC signatures.
type Secrets map[Algorithm][]string
// Signatures hold per algorithm slice of signatures.
type Signatures map[Algorithm][]string
// Provider defines the configuration for sending rpc notifications.
type Provider struct {
// ConsumerURL is the URL where an rpc consumer/listener is running
// and to which we will send and receive all notifications.
ConsumerURL string
// Host is the BMC ip address or hostname or identifier.
Host string
// HTTPClient is the http client used for all HTTP calls.
HTTPClient *http.Client
// Logger is the logger to use for logging.
Logger logr.Logger
// LogNotificationsDisabled determines whether responses from rpc consumer/listeners will be logged or not.
LogNotificationsDisabled bool
// Opts are the options for the rpc provider.
Opts Opts
// listenerURL is the URL of the rpc consumer/listener.
listenerURL *url.URL
}
type Opts struct {
// Request is the options used to create the rpc HTTP request.
Request RequestOpts
// Signature is the options used for adding an HMAC signature to an HTTP request.
Signature SignatureOpts
// HMAC is the options used to create a HMAC signature.
HMAC HMACOpts
// Experimental options.
Experimental Experimental
}
type RequestOpts struct {
// HTTPContentType is the content type to use for the rpc request notification.
HTTPContentType string
// HTTPMethod is the HTTP method to use for the rpc request notification.
HTTPMethod string
// StaticHeaders are predefined headers that will be added to every request.
StaticHeaders http.Header
// TimestampFormat is the time format for the timestamp header.
TimestampFormat string
// TimestampHeader is the header name that should contain the timestamp. Example: X-BMCLIB-Timestamp
TimestampHeader string
}
type SignatureOpts struct {
// HeaderName is the header name that should contain the signature(s). Example: X-BMCLIB-Signature
HeaderName string
// AppendAlgoToHeaderDisabled decides whether to append the algorithm to the signature header or not.
// Example: X-BMCLIB-Signature becomes X-BMCLIB-Signature-256
// When set to true, a header will be added for each algorithm. Example: X-BMCLIB-Signature-256 and X-BMCLIB-Signature-512
AppendAlgoToHeaderDisabled bool
// IncludedPayloadHeaders are headers whose values will be included in the signature payload. Example: given these headers in a request:
// X-My-Header=123,X-Another=456, and IncludedPayloadHeaders := []string{"X-Another"}, the value of "X-Another" will be included in the signature payload.
// All headers will be deduplicated.
IncludedPayloadHeaders []string
}
type HMACOpts struct {
// Hashes is a map of algorithms to a slice of hash.Hash (these are the hashed secrets). The slice is used to support multiple secrets.
Hashes map[Algorithm][]hash.Hash
// PrefixSigDisabled determines whether the algorithm will be prefixed to the signature. Example: sha256=abc123
PrefixSigDisabled bool
// Secrets are a map of algorithms to secrets used for signing.
Secrets Secrets
}
type Experimental struct {
// CustomRequestPayload must be in json.
CustomRequestPayload []byte
// DotPath is the path to where the bmclib RequestPayload{} will be embedded. For example: object.data.body
DotPath string
}
// New returns a new Config containing all the defaults for the rpc provider.
func New(consumerURL string, host string, secrets Secrets) *Provider {
// defaults
c := &Provider{
Host: host,
ConsumerURL: consumerURL,
HTTPClient: httpclient.Build(),
Logger: logr.Discard(),
Opts: Opts{
Request: RequestOpts{
HTTPContentType: contentType,
HTTPMethod: http.MethodPost,
TimestampFormat: time.RFC3339,
TimestampHeader: timestampHeader,
},
Signature: SignatureOpts{
HeaderName: signatureHeader,
IncludedPayloadHeaders: []string{},
},
HMAC: HMACOpts{
Hashes: map[Algorithm][]hash.Hash{},
Secrets: secrets,
},
Experimental: Experimental{},
},
}
if len(secrets) > 0 {
c.Opts.HMAC.Hashes = CreateHashes(secrets)
}
return c
}
// Name returns the name of this rpc provider.
// Implements bmc.Provider interface
func (p *Provider) Name() string {
return ProviderName
}
// Open a connection to the rpc consumer.
// For the rpc provider, Open means validating the Config and
// that communication with the rpc consumer can be established.
func (p *Provider) Open(ctx context.Context) error {
// 1. validate consumerURL is a properly formatted URL.
// 2. validate that we can communicate with the rpc consumer.
u, err := url.Parse(p.ConsumerURL)
if err != nil {
return err
}
p.listenerURL = u
if _, err = p.process(ctx, RequestPayload{
ID: time.Now().UnixNano(),
Host: p.Host,
Method: PingMethod,
}); err != nil {
return err
}
return nil
}
// Close a connection to the rpc consumer.
func (p *Provider) Close(_ context.Context) (err error) {
return nil
}
// BootDeviceSet sends a next boot device rpc notification.
func (p *Provider) BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) {
rp := RequestPayload{
ID: time.Now().UnixNano(),
Host: p.Host,
Method: BootDeviceMethod,
Params: BootDeviceParams{
Device: bootDevice,
Persistent: setPersistent,
EFIBoot: efiBoot,
},
}
resp, err := p.process(ctx, rp)
if err != nil {
return false, err
}
if resp.Error != nil && resp.Error.Code != 0 {
return false, fmt.Errorf("error from rpc consumer: %v", resp.Error)
}
return true, nil
}
// PowerSet sets the power state of a BMC machine.
func (p *Provider) PowerSet(ctx context.Context, state string) (ok bool, err error) {
switch strings.ToLower(state) {
case "on", "off", "cycle":
rp := RequestPayload{
ID: time.Now().UnixNano(),
Host: p.Host,
Method: PowerSetMethod,
Params: PowerSetParams{
State: strings.ToLower(state),
},
}
resp, err := p.process(ctx, rp)
if err != nil {
return ok, err
}
if resp.Error != nil && resp.Error.Code != 0 {
return ok, fmt.Errorf("error from rpc consumer: %v", resp.Error)
}
return true, nil
}
return false, errors.New("requested power state is not supported")
}
// PowerStateGet gets the power state of a BMC machine.
func (p *Provider) PowerStateGet(ctx context.Context) (state string, err error) {
rp := RequestPayload{
ID: time.Now().UnixNano(),
Host: p.Host,
Method: PowerGetMethod,
}
resp, err := p.process(ctx, rp)
if err != nil {
return "", err
}
if resp.Error != nil && resp.Error.Code != 0 {
return "", fmt.Errorf("error from rpc consumer: %v", resp.Error)
}
s, ok := resp.Result.(string)
if !ok {
return "", fmt.Errorf("expected result equal to type string, got: %T", resp.Result)
}
return s, nil
}
// process is the main function for the roundtrip of rpc calls to the ConsumerURL.
func (p *Provider) process(ctx context.Context, rp RequestPayload) (ResponsePayload, error) {
// 1. create the HTTP request.
// 2. create the signature payload.
// 3. sign the signature payload.
// 4. add signatures to the request as headers.
// 5. request/response round trip.
// 6. handle the response.
req, err := p.createRequest(ctx, rp)
if err != nil {
return ResponsePayload{}, err
}
// create the signature payload
reqBuf := new(bytes.Buffer)
reqBody, err := req.GetBody()
if err != nil {
return ResponsePayload{}, fmt.Errorf("failed to get request body: %w", err)
}
if _, err := io.Copy(reqBuf, reqBody); err != nil {
return ResponsePayload{}, fmt.Errorf("failed to read request body: %w", err)
}
headersForSig := http.Header{}
for _, h := range p.Opts.Signature.IncludedPayloadHeaders {
if val := req.Header.Get(h); val != "" {
headersForSig.Add(h, val)
}
}
sigPay := createSignaturePayload(reqBuf.Bytes(), headersForSig)
// sign the signature payload
sigs, err := sign(sigPay, p.Opts.HMAC.Hashes, p.Opts.HMAC.PrefixSigDisabled)
if err != nil {
return ResponsePayload{}, err
}
// add signatures to the request as headers.
for algo, keys := range sigs {
if len(sigs) > 0 {
h := p.Opts.Signature.HeaderName
if !p.Opts.Signature.AppendAlgoToHeaderDisabled {
h = fmt.Sprintf("%s-%s", h, algo.ToShort())
}
req.Header.Add(h, strings.Join(keys, ","))
}
}
// request/response round trip.
kvs := requestKVS(req.Method, req.URL.String(), req.Header, reqBuf)
kvs = append(kvs, []interface{}{"host", p.Host, "method", rp.Method, "consumerURL", p.ConsumerURL}...)
if rp.Params != nil {
kvs = append(kvs, []interface{}{"params", rp.Params}...)
}
resp, err := p.HTTPClient.Do(req)
if err != nil {
p.Logger.Error(err, "failed to send rpc notification", kvs...)
return ResponsePayload{}, err
}
defer resp.Body.Close()
// handle the response
if resp.ContentLength > maxContentLenAllowed || resp.ContentLength < 0 {
return ResponsePayload{}, fmt.Errorf("response body is too large: %d bytes, max allowed: %d bytes", resp.ContentLength, maxContentLenAllowed)
}
respBuf := new(bytes.Buffer)
if _, err := io.CopyN(respBuf, resp.Body, resp.ContentLength); err != nil {
return ResponsePayload{}, fmt.Errorf("failed to read response body: %w", err)
}
respPayload, err := p.handleResponse(resp.StatusCode, resp.Header, respBuf, kvs)
if err != nil {
return ResponsePayload{}, err
}
return respPayload, nil
}
// Transformer implements the mergo interfaces for merging custom types.
func (p *Provider) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
switch typ {
case reflect.TypeOf(logr.Logger{}):
return func(dst, src reflect.Value) error {
if dst.CanSet() {
isZero := dst.MethodByName("GetSink")
result := isZero.Call(nil)
if result[0].IsNil() {
dst.Set(src)
}
}
return nil
}
}
return nil
}