• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender / 989668079

31 Aug 2023 08:01AM UTC coverage: 58.158% (-0.02%) from 58.176%
989668079

push

gitlab-ci

web-flow
Merge pull request #1373 from tranchitella/qa-614

feat: build and test using the latest version of golang (1.21 today)

0 of 1 new or added line in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

6352 of 10922 relevant lines covered (58.16%)

30.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

43.19
/client/client.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//        Licensed under the Apache License, Version 2.0 (the "License");
4
//        you may not use this file except in compliance with the License.
5
//        You may obtain a copy of the License at
6
//
7
//            http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//        Unless required by applicable law or agreed to in writing, software
10
//        distributed under the License is distributed on an "AS IS" BASIS,
11
//        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//        See the License for the specific language governing permissions and
13
//        limitations under the License.
14
package client
15

16
import (
17
        "bufio"
18
        "context"
19
        "encoding/base64"
20
        "encoding/json"
21
        "fmt"
22
        "io"
23
        "net"
24
        "net/http"
25
        "net/url"
26
        "os"
27
        "path"
28
        "strings"
29
        "time"
30

31
        "io/ioutil"
32

33
        "github.com/gorilla/websocket"
34
        "github.com/pkg/errors"
35
        log "github.com/sirupsen/logrus"
36

37
        "github.com/mendersoftware/openssl"
38
)
39

40
const (
41
        apiPrefix = "/api/devices/"
42

43
        errMissingServerCertF = "IGNORING ERROR: The client server-certificate can not be " +
44
                "loaded: (%s). The client will continue running, but may not be able to " +
45
                "communicate with the server. If this is not your intention please add a valid " +
46
                "server certificate"
47
        errMissingCerts = "No trusted certificates. The client will continue running, but will " +
48
                "not be able to communicate with the server. Either specify ServerCertificate in " +
49
                "mender.conf, or make sure that CA certificates are installed on the system"
50
        pkcs11URIPrefix = "pkcs11:"
51
)
52

53
var (
54
        //                           http.Client.Timeout
55
        // +--------------------------------------------------------+
56
        // +--------+  +---------+  +-------+  +--------+  +--------+
57
        // |  Dial  |  |   TLS   |  |Request|  |Response|  |Response|
58
        // |        |  |handshake|  |       |  |headers |  |body    |
59
        // +--------+  +---------+  +-------+  +--------+  +--------+
60
        // +--------+  +---------+             +--------+
61
        //  Dial        TLS                     Response
62
        //  timeout     handshake               header
63
        //  timeout                             timeout
64
        //
65
        //  It covers the entire exchange, from Dial (if a connection is not reused)
66
        // to reading the body. This is to timeout long lasting connections.
67
        //
68
        // 4 hours should be enough to download a 2GB image file with the
69
        // average download speed ~1 mbps
70
        defaultClientReadingTimeout = 4 * time.Hour
71

72
        // connection keepalive options
73
        connectionKeepaliveTime = 10 * time.Second
74

75
        ErrClientUnauthorized = errors.New("Client is unauthorized")
76
)
77

78
// Mender API Client wrapper. A standard http.Client is compatible with this
79
// interface and can be used without further configuration where ApiRequester is
80
// expected. Instead of instantiating the client by yourself, one can also use a
81
// wrapper call NewApiClient() that sets up TLS handling according to passed
82
// configuration.
83
type ApiRequester interface {
84
        Do(req *http.Request) (*http.Response, error)
85
}
86

87
// An ApiRequester which internally authorizes automatically. It's possible to
88
// call ClearAuthorization() in order to force reauthorization.
89
type AuthorizedApiRequester interface {
90
        ApiRequester
91
        ClearAuthorization()
92
}
93

94
// MenderServer is a placeholder for a full server definition used when
95
// multiple servers are given. The fields corresponds to the definitions
96
// given in MenderConfig.
97
type MenderServer struct {
98
        ServerURL string
99
        // TODO: Move all possible server specific configurations in
100
        //       MenderConfig over to this struct. (e.g. TenantToken?)
101
}
102

103
// APIError is an error type returned after receiving an error message from the
104
// server. It wraps a regular error with the request_id - and if
105
// the server returns an error message, this is also returned.
106
type APIError struct {
107
        error
108
        reqID        string
109
        serverErrMsg string
110
}
111

112
func NewAPIError(err error, resp *http.Response) *APIError {
26✔
113
        a := APIError{
26✔
114
                error: err,
26✔
115
                reqID: resp.Header.Get("request_id"),
26✔
116
        }
26✔
117

26✔
118
        if resp.StatusCode >= 400 && resp.StatusCode < 600 {
26✔
119
                a.serverErrMsg = unmarshalErrorMessage(resp.Body)
×
120
        }
×
121
        return &a
26✔
122
}
123

124
func (a *APIError) Error() string {
×
125

×
126
        err := a.error.Error()
×
127

×
128
        if a.reqID != "" {
×
129
                err = fmt.Sprintf("(request_id: %s): %s", a.reqID, err)
×
130
        }
×
131

132
        if a.serverErrMsg != "" {
×
133
                return err + fmt.Sprintf(" server error message: %s", a.serverErrMsg)
×
134
        }
×
135

136
        return err
×
137

138
}
139

140
// Cause returns the underlying error, as
141
// an APIError is merely an error wrapper.
142
func (a *APIError) Cause() error {
×
143
        return a.error
×
144
}
×
145

146
func (a *APIError) Unwrap() error {
26✔
147
        return a.error
26✔
148
}
26✔
149

150
type ApiClient struct {
151
        http.Client
152
}
153

154
type ReauthorizingClient struct {
155
        ApiClient
156
        // authorization code to use for requests
157
        auth AuthToken
158
        // server to use for requests
159
        serverURL ServerURL
160
        // anonymous function to initiate reauthorization
161
        revoke ClientReauthorizeFunc
162
}
163

164
// function type for reauthorization closure (see func reauthorize@mender.go)
165
type ClientReauthorizeFunc func() (AuthToken, ServerURL, error)
166

167
// Reconstruct the given request, taking JWT token and serverURL into account.
168
func (c *ReauthorizingClient) reconstructRequest(req *http.Request) (*http.Request, error) {
28✔
169
        if c.serverURL == "" {
56✔
170
                return nil, ErrClientUnauthorized
28✔
171
        }
28✔
172

173
        serverURL, err := url.Parse(string(c.serverURL))
27✔
174
        if err != nil {
27✔
175
                return nil, errors.Wrap(err, "Could not parse ServerURL from auth manager")
×
176
        }
×
177

178
        // First prefill newURL with existing URL.
179
        newURL, _ := url.Parse(req.URL.String())
27✔
180

27✔
181
        // Then selectively fill in the prefix from ServerURL.
27✔
182
        newURL.Scheme = serverURL.Scheme
27✔
183
        newURL.Host = serverURL.Host
27✔
184
        newURL.Path = strings.TrimRight(serverURL.Path, "/") +
27✔
185
                "/" +
27✔
186
                strings.TrimLeft(req.URL.Path, "/")
27✔
187

27✔
188
        log.Debugf("Connecting to server %s", serverURL.String())
27✔
189

27✔
190
        var body io.ReadCloser
27✔
191
        if req.GetBody != nil {
54✔
192
                body, err = req.GetBody()
27✔
193
                if err != nil {
27✔
194
                        return nil, errors.Wrap(err, "Unable to reconstruct HTTP request body")
×
195
                }
×
196
        } else {
2✔
197
                body = nil
2✔
198
        }
2✔
199

200
        // create a new request object to avoid issues when consuming
201
        // the request body multiple times when failing over a different
202
        // server. It is not safe to reuse the same request multiple times
203
        // when request.Body is not nil
204
        //
205
        // see: https://github.com/golang/go/issues/19653
206
        // Error message: http: ContentLength=52 with Body length 0
207
        newReq, _ := http.NewRequest(req.Method, newURL.String(), body)
27✔
208
        newReq.Header = req.Header
27✔
209
        newReq.GetBody = req.GetBody
27✔
210
        newReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.auth))
27✔
211

27✔
212
        return newReq, nil
27✔
213
}
214

215
// Do is a wrapper for http.Do function for ApiRequests. This function in
216
// addition to calling http.Do handles client-server authorization header /
217
// reauthorization, as well as attempting failover servers (if given) whenever
218
// the server "refuse" to serve the request.
219
func (c *ReauthorizingClient) Do(req *http.Request) (*http.Response, error) {
28✔
220
        fetchedNewToken := false
28✔
221
        for {
56✔
222
                var r *http.Response
28✔
223
                newReq, err := c.reconstructRequest(req)
28✔
224
                if err == nil {
55✔
225
                        r, err = c.ApiClient.Do(newReq)
27✔
226
                } else if err != ErrClientUnauthorized {
55✔
227
                        return nil, err
×
228
                }
×
229

230
                // If we haven't yet fetched a new token, and the call results
231
                // in connection error or Unauthorized status, try to get
232
                // one. Otherwise return the result as is.
233
                if !fetchedNewToken && (err != nil || r.StatusCode == http.StatusUnauthorized) {
56✔
234
                        // Try to fetch a new JWT token from one of the servers in the server
28✔
235
                        // list.
28✔
236
                        log.Info("Device unauthorized; attempting reauthorization")
28✔
237
                        jwt, serverURL, e := c.revoke()
28✔
238
                        if e == nil {
55✔
239
                                // retry API request with new JWT token
27✔
240
                                c.auth = jwt
27✔
241
                                c.serverURL = serverURL
27✔
242
                                log.Info("Reauthorization successful")
27✔
243
                        } else {
28✔
244
                                log.Warnf("Reauthorization failed with error: %s", e.Error())
1✔
245
                                return nil, e
1✔
246
                        }
1✔
247
                        fetchedNewToken = true
27✔
248
                } else {
27✔
249
                        return r, err
27✔
250
                }
27✔
251
        }
252
}
253

254
func (c *ReauthorizingClient) ClearAuthorization() {
13✔
255
        c.auth = ""
13✔
256
        c.serverURL = ""
13✔
257
}
13✔
258

259
func NewReauthorizingClient(
260
        conf Config,
261
        reauth ClientReauthorizeFunc,
262
) (*ReauthorizingClient, error) {
192✔
263
        client, err := NewApiClient(conf)
192✔
264
        if err != nil {
192✔
265
                return nil, err
×
266
        }
×
267
        return &ReauthorizingClient{
191✔
268
                ApiClient: *client,
191✔
269
                revoke:    reauth,
191✔
270
        }, nil
191✔
271
}
272

273
func NewApiClient(conf Config) (*ApiClient, error) {
192✔
274

192✔
275
        var client *http.Client
192✔
276
        if conf == (Config{}) {
206✔
277
                client = newHttpClient()
14✔
278
        } else {
206✔
279
                var err error
192✔
280
                client, err = newHttpsClient(conf)
192✔
281
                if err != nil {
192✔
282
                        return nil, err
×
283
                }
×
284
        }
285

286
        if client.Transport == nil {
205✔
287
                client.Transport = &http.Transport{
14✔
288
                        Proxy: http.ProxyFromEnvironment,
14✔
289
                }
14✔
290
        }
14✔
291
        // set connection timeout
292
        client.Timeout = defaultClientReadingTimeout
191✔
293

191✔
294
        transport := client.Transport.(*http.Transport)
191✔
295
        //set keepalive options
191✔
296
        transport.DialContext = (&net.Dialer{
191✔
297
                KeepAlive: connectionKeepaliveTime,
191✔
298
        }).DialContext
191✔
299

191✔
300
        return &ApiClient{*client}, nil
191✔
301
}
302

303
func newHttpClient() *http.Client {
191✔
304
        return &http.Client{}
191✔
305
}
191✔
306

307
// nrOfSystemCertsFound simply returns the number of certificates found in
308
// 'certDir'. The only reason this is needed, is so that the user can be
309
// notified if the system certificate directory is empty, since this is not done
310
// by the OpenSSL wrapper, in the same manner it was done by the Go standard
311
// library. OpenSSL loads the trust chain on a dial. This system cert, and
312
// server cert path are set by the 'SetDefaultVerifyPaths' and
313
// 'LoadVerifyLocations' respectively.
314
func nrOfSystemCertsFound(certDir string) (int, error) {
192✔
315
        sysCertsFound := 0
192✔
316
        files, err := ioutil.ReadDir(certDir)
192✔
317
        if err != nil {
192✔
318
                return 0, fmt.Errorf(
×
319
                        "Failed to read the OpenSSL default directory (%s). Err %v",
×
320
                        certDir,
×
321
                        err.Error(),
×
322
                )
×
323
        }
×
324
        for _, certFile := range files {
384✔
325
                // Need to re-stat here because ReadDir does not resolve
192✔
326
                // symlinks.
192✔
327
                info, err := os.Stat(path.Join(certDir, certFile.Name()))
192✔
328
                if err != nil {
192✔
329
                        log.Debugf("Failed to stat file %s: %s", certFile.Name(), err.Error())
×
330
                        continue
×
331
                } else if !info.Mode().IsRegular() {
192✔
332
                        log.Debugf("Not a regular file, skipping: %s", info.Name())
×
333
                        continue
×
334
                }
335

336
                certBytes, err := ioutil.ReadFile(path.Join(certDir, certFile.Name()))
192✔
337
                if err != nil {
192✔
338
                        log.Debugf(
×
339
                                "Failed to read the certificate file for the HttpsClient. Err %v",
×
340
                                err.Error(),
×
341
                        )
×
342
                        continue
×
343
                }
344

345
                certs := openssl.SplitPEM(certBytes)
192✔
346
                if len(certs) == 0 {
192✔
347
                        log.Debugf("No PEM certificate found in '%s'", certFile)
×
348
                        continue
×
349
                }
350
                for _, cert := range certs {
384✔
351
                        _, err = openssl.LoadCertificateFromPEM(cert)
192✔
352
                        if err != nil {
192✔
353
                                log.Debug(err.Error())
×
354
                        } else {
192✔
355
                                sysCertsFound += 1
192✔
356
                        }
192✔
357
                }
358
        }
359
        return sysCertsFound, nil
191✔
360
}
361

362
func loadServerTrust(ctx *openssl.Ctx, conf *Config) (*openssl.Ctx, error) {
192✔
363
        defaultCertDir, err := openssl.GetDefaultCertificateDirectory()
192✔
364
        if err != nil {
192✔
365
                return ctx, errors.Wrap(
×
366
                        err,
×
367
                        "Failed to get the default OpenSSL certificate directory. Please verify the"+
×
368
                                " OpenSSL setup",
×
369
                )
×
370
        }
×
371
        sysCertsFound, err := nrOfSystemCertsFound(defaultCertDir)
192✔
372
        if err != nil {
192✔
373
                log.Warnf("Failed to list the system certificates with error: %s", err.Error())
×
374
        }
×
375

376
        // Set the default system certificate path for this OpenSSL context
377
        err = ctx.SetDefaultVerifyPaths()
191✔
378
        if err != nil {
191✔
379
                return ctx, fmt.Errorf(
×
380
                        "Failed to set the default OpenSSL directory. OpenSSL error code: %s",
×
381
                        err.Error(),
×
382
                )
×
383
        }
×
384
        if conf.ServerCert != "" {
191✔
385
                // Load the server certificate into the OpenSSL context
×
386
                err = ctx.LoadVerifyLocations(conf.ServerCert, "")
×
387
                if err != nil {
×
NEW
388
                        if strings.Contains(strings.ToLower(err.Error()), "no such file") {
×
389
                                log.Warnf(errMissingServerCertF, conf.ServerCert)
×
390
                        } else {
×
391
                                log.Errorf("Failed to Load the Server certificate. Err %s", err.Error())
×
392
                        }
×
393
                        // If no system certificates, nor a server certificate is found,
394
                        // warn the user, as this is a pretty common error.
395
                        if sysCertsFound == 0 {
×
396
                                log.Error(errMissingCerts)
×
397
                        }
×
398
                }
399
        } else if sysCertsFound == 0 {
191✔
400
                log.Warn(errMissingCerts)
×
401
        }
×
402
        return ctx, err
191✔
403
}
404

405
func loadPrivateKey(keyFile string, engineId string) (key openssl.PrivateKey, err error) {
×
406
        if strings.HasPrefix(keyFile, pkcs11URIPrefix) {
×
407
                engine, err := openssl.EngineById(engineId)
×
408
                if err != nil {
×
409
                        log.Errorf("Failed to Load '%s' engine. Err %s",
×
410
                                engineId, err.Error())
×
411
                        return nil, err
×
412
                }
×
413

414
                key, err = openssl.EngineLoadPrivateKey(engine, keyFile)
×
415
                if err != nil {
×
416
                        log.Errorf("Failed to Load private key from engine '%s'. Err %s",
×
417
                                engineId, err.Error())
×
418
                        return nil, err
×
419
                }
×
420
                log.Infof("loaded private key: '%s...' from '%s'.", pkcs11URIPrefix, engineId)
×
421
        } else {
×
422
                keyBytes, err := ioutil.ReadFile(keyFile)
×
423
                if err != nil {
×
424
                        return nil, errors.Wrap(err, "Private key file from the HttpsClient configuration"+
×
425
                                " not found")
×
426
                }
×
427

428
                key, err = openssl.LoadPrivateKeyFromPEM(keyBytes)
×
429
                if err != nil {
×
430
                        return nil, err
×
431
                }
×
432
        }
433

434
        return key, nil
×
435
}
436

437
func loadClientTrust(ctx *openssl.Ctx, conf *Config) (*openssl.Ctx, error) {
×
438

×
439
        if conf.HttpsClient == nil {
×
440
                return ctx, errors.New("Empty HttpsClient config given")
×
441

×
442
        }
×
443
        certFile := conf.HttpsClient.Certificate
×
444

×
445
        certBytes, err := ioutil.ReadFile(certFile)
×
446
        if err != nil {
×
447
                return ctx, errors.Wrap(err, "Failed to read the certificate file for the HttpsClient")
×
448
        }
×
449

450
        certs := openssl.SplitPEM(certBytes)
×
451
        if len(certs) == 0 {
×
452
                return ctx, fmt.Errorf("No PEM certificate found in '%s'", certFile)
×
453
        }
×
454
        first, certs := certs[0], certs[1:]
×
455
        cert, err := openssl.LoadCertificateFromPEM(first)
×
456
        if err != nil {
×
457
                return ctx, err
×
458
        }
×
459

460
        err = ctx.UseCertificate(cert)
×
461
        if err != nil {
×
462
                return ctx, err
×
463
        }
×
464

465
        for _, pem := range certs {
×
466
                cert, err := openssl.LoadCertificateFromPEM(pem)
×
467
                if err != nil {
×
468
                        return ctx, err
×
469
                }
×
470
                err = ctx.AddChainCertificate(cert)
×
471
                if err != nil {
×
472
                        return ctx, err
×
473
                }
×
474
        }
475

476
        key, err := loadPrivateKey(conf.HttpsClient.Key, conf.HttpsClient.SSLEngine)
×
477
        if err != nil {
×
478
                return ctx, err
×
479
        }
×
480

481
        err = ctx.UsePrivateKey(key)
×
482
        if err != nil {
×
483
                return ctx, err
×
484
        }
×
485

486
        return ctx, nil
×
487
}
488

489
func establishSSLConnection(
490
        addr string,
491
        proxyURL *url.URL,
492
        ctx *openssl.Ctx,
493
        flags openssl.DialFlags,
494
) (*openssl.Conn, error) {
31✔
495
        if proxyURL != nil {
31✔
496
                proxyConn, err := dialProxy("tcp", addr, proxyURL)
×
497
                if err != nil {
×
498
                        return nil, errors.Wrap(err, "Failed to connect to proxy")
×
499
                }
×
500
                return openssl.DialUpgrade(addr, proxyConn, ctx, flags)
×
501
        }
502
        return openssl.Dial("tcp", addr, ctx, flags)
31✔
503
}
504

505
func dialOpenSSL(
506
        ctx *openssl.Ctx,
507
        conf *Config,
508
        _, addr string,
509
) (net.Conn, error) {
31✔
510
        proxyURL, err := ProxyURLFromHostPortGetter(addr)
31✔
511
        if err != nil {
31✔
512
                return nil, errors.Wrapf(err,
×
513
                        "Failed to get http-proxy configurations during dial")
×
514
        }
×
515
        flags := openssl.DialFlags(0)
31✔
516

31✔
517
        if conf.NoVerify {
31✔
518
                flags = openssl.InsecureSkipHostVerification
×
519
        }
×
520

521
        conn, err := establishSSLConnection(addr, proxyURL, ctx, flags)
31✔
522
        if err != nil {
33✔
523
                return nil, err
2✔
524
        }
2✔
525

526
        if conf.NoVerify {
29✔
527
                return conn, nil
×
528
        }
×
529
        v := conn.VerifyResult()
29✔
530
        if v != openssl.Ok {
29✔
531
                if v == openssl.CertHasExpired {
×
532
                        return nil, errors.Errorf("certificate has expired, "+
×
533
                                "openssl verify rc: %d server cert file: %s", v, conf.ServerCert)
×
534
                }
×
535
                if v == openssl.DepthZeroSelfSignedCert {
×
536
                        return nil, errors.Errorf("depth zero self-signed certificate, "+
×
537
                                "openssl verify rc: %d server cert file: %s", v, conf.ServerCert)
×
538
                }
×
539
                if v == openssl.EndEntityKeyTooSmall {
×
540
                        return nil, errors.Errorf("end entity key too short, "+
×
541
                                "openssl verify rc: %d server cert file: %s", v, conf.ServerCert)
×
542
                }
×
543
                if v == openssl.UnableToGetIssuerCertLocally {
×
544
                        return nil, errors.Errorf("certificate signed by unknown authority, "+
×
545
                                "openssl verify rc: %d server cert file: %s", v, conf.ServerCert)
×
546
                }
×
547
                return nil, errors.Errorf("not a valid certificate, "+
×
548
                        "openssl verify rc: %d server cert file: %s", v, conf.ServerCert)
×
549
        }
550
        return conn, err
29✔
551
}
552

553
func newOpenSSLCtx(conf Config) (*openssl.Ctx, error) {
192✔
554
        ctx, err := openssl.NewCtx()
192✔
555
        if err != nil {
192✔
556
                return nil, err
×
557
        }
×
558

559
        ctx, err = loadServerTrust(ctx, &conf)
192✔
560
        if err != nil {
192✔
561
                log.Warn(errors.Wrap(err, "Failed to load the server TLS certificate settings"))
×
562
        }
×
563

564
        if conf.HttpsClient != nil {
191✔
565
                ctx, err = loadClientTrust(ctx, &conf)
×
566
                if err != nil {
×
567
                        log.Warn(errors.Wrap(err, "Failed to load the client TLS certificate settings"))
×
568
                }
×
569
        }
570

571
        if conf.NoVerify {
191✔
572
                log.Warn("certificate verification skipped..")
×
573
        }
×
574

575
        return ctx, nil
191✔
576
}
577

578
func newHttpsClient(conf Config) (*http.Client, error) {
192✔
579
        ctx, err := newOpenSSLCtx(conf)
192✔
580
        if err != nil {
192✔
581
                return nil, err
×
582
        }
×
583

584
        disableKeepAlive := false
191✔
585
        idleConnTimeoutSeconds := 0
191✔
586
        if conf.Connectivity != nil {
382✔
587
                disableKeepAlive = conf.Connectivity.DisableKeepAlive
191✔
588
                idleConnTimeoutSeconds = conf.Connectivity.IdleConnTimeoutSeconds
191✔
589
        }
191✔
590
        transport := http.Transport{
191✔
591
                DisableKeepAlives: disableKeepAlive,
191✔
592
                IdleConnTimeout:   time.Duration(idleConnTimeoutSeconds) * time.Second,
191✔
593
                DialTLS: func(network string, addr string) (net.Conn, error) {
222✔
594
                        return dialOpenSSL(ctx, &conf, network, addr)
31✔
595
                },
31✔
596
        }
597

598
        client := newHttpClient()
191✔
599
        client.Transport = &transport
191✔
600
        return client, nil
191✔
601
}
602

603
// Client configuration
604

605
// HttpsClient holds the configuration for the client side mTLS configuration
606
// NOTE: Careful when changing this, the struct is exposed directly in the
607
// 'mender.conf' file.
608
type HttpsClient struct {
609
        Certificate string `json:",omitempty"`
610
        Key         string `json:",omitempty"`
611
        SSLEngine   string `json:",omitempty"`
612
}
613

614
// Security structure holds the configuration for the client
615
// Added for MEN-3924 in order to provide a way to specify PKI params
616
// outside HttpsClient.
617
// NOTE: Careful when changing this, the struct is exposed directly in the
618
// 'mender.conf' file.
619
type Security struct {
620
        AuthPrivateKey string `json:",omitempty"`
621
        SSLEngine      string `json:",omitempty"`
622
}
623

624
// Connectivity instructs the client how we want to treat the keep alive connections
625
// and when a connection is considered idle and therefore closed
626
// NOTE: Careful when changing this, the struct is exposed directly in the
627
// 'mender.conf' file.
628
type Connectivity struct {
629
        // If set to true, there will be no persistent connections, and every
630
        // HTTP transaction will try to establish a new connection
631
        DisableKeepAlive bool `json:",omitempty"`
632
        // A number of seconds after which a connection is considered idle and closed.
633
        // The longer this is the longer connections are up after the first call over HTTP
634
        IdleConnTimeoutSeconds int `json:",omitempty"`
635
}
636

637
func (h *HttpsClient) Validate() {
206✔
638
        if h == nil {
206✔
639
                return
×
640
        }
×
641
        if h.Certificate != "" || h.Key != "" {
206✔
642
                if h.Certificate == "" {
×
643
                        log.Error(
×
644
                                "The 'Key' field is set in the mTLS configuration, but no 'Certificate' is given." +
×
645
                                        " Both need to be present in order for mTLS to function",
×
646
                        )
×
647
                }
×
648
                if h.Key == "" {
×
649
                        log.Error(
×
650
                                "The 'Certificate' field is set in the mTLS configuration, but no 'Key' is given." +
×
651
                                        " Both need to be present in order for mTLS to function",
×
652
                        )
×
653
                } else if strings.HasPrefix(h.Key, pkcs11URIPrefix) && len(h.SSLEngine) == 0 {
×
654
                        log.Errorf("The 'Key' field is set to be loaded from %s, but no 'SSLEngine' is given."+
×
655
                                " Both need to be present in order for loading of the key to function",
×
656
                                pkcs11URIPrefix)
×
657
                }
×
658
        }
659
}
660

661
type Config struct {
662
        ServerCert string
663
        *HttpsClient
664
        *Connectivity
665
        NoVerify bool
666
}
667

668
func buildURL(server string) string {
31✔
669
        if strings.HasPrefix(server, "https://") || strings.HasPrefix(server, "http://") {
62✔
670
                return server
31✔
671
        }
31✔
672
        return "https://" + server
×
673
}
674

675
func buildApiURL(server, url string) string {
31✔
676
        return buildURL(server) + apiPrefix + strings.TrimPrefix(url, "/")
31✔
677
}
31✔
678

679
// Normally one minute, but used in tests to lower the interval to avoid
680
// waiting.
681
var ExponentialBackoffSmallestUnit time.Duration = time.Minute
682

683
var MaxRetriesExceededError = errors.New("Tried maximum amount of times")
684

685
// Simple algorithm: Start with one minute, and try three times, then double
686
// interval (maxInterval is maximum) and try again. Repeat until we tried
687
// three times with maxInterval.
688
func GetExponentialBackoffTime(tried int,
689
        maxInterval time.Duration,
690
        maxAttempts int) (time.Duration, error) {
1✔
691
        const perIntervalAttempts = 3
1✔
692

1✔
693
        interval := 1 * ExponentialBackoffSmallestUnit
1✔
694
        nextInterval := interval
1✔
695

1✔
696
        if maxAttempts > 0 && tried >= maxAttempts {
1✔
697
                return 0, MaxRetriesExceededError
×
698
        }
×
699

700
        for c := 0; c <= tried; c += perIntervalAttempts {
2✔
701
                interval = nextInterval
1✔
702
                nextInterval *= 2
1✔
703
                if interval >= maxInterval {
2✔
704
                        if maxAttempts <= 0 && tried-c >= perIntervalAttempts {
1✔
705
                                // At max interval and already tried three
×
706
                                // times. Give up.
×
707
                                return 0, MaxRetriesExceededError
×
708
                        }
×
709

710
                        // Don't use less than the smallest unit, usually one
711
                        // minute.
712
                        if maxInterval < ExponentialBackoffSmallestUnit {
2✔
713
                                return ExponentialBackoffSmallestUnit, nil
1✔
714
                        }
1✔
715
                        return maxInterval, nil
×
716
                }
717
        }
718

719
        return interval, nil
×
720
}
721

722
// unmarshalErrorMessage unmarshals the error message contained in an
723
// error request from the server.
724
func unmarshalErrorMessage(r io.Reader) string {
×
725
        e := new(struct {
×
726
                Error string `json:"error"`
×
727
        })
×
728
        resp, err := ioutil.ReadAll(r)
×
729
        if err != nil {
×
730
                return fmt.Sprintf("Failed to read the response body %s", err)
×
731
        }
×
732
        if err = json.Unmarshal(resp, e); err != nil {
×
733
                return string(resp)
×
734
        }
×
735
        return e.Error
×
736
}
737

738
func newWebsocketDialerTLS(conf Config) (*websocket.Dialer, error) {
31✔
739
        ctx, err := newOpenSSLCtx(conf)
31✔
740
        if err != nil {
31✔
741
                return nil, err
×
742
        }
×
743

744
        dialer := websocket.Dialer{
31✔
745
                NetDialTLSContext: func(_ context.Context, network string, addr string) (net.Conn, error) {
31✔
746
                        return dialOpenSSL(ctx, &conf, network, addr)
×
747
                },
×
748
        }
749

750
        return &dialer, nil
31✔
751
}
752

753
// http.ProxyFromEnvironment returns nil if parameter req.URL is localhost.
754
// This function variable is public so it can be overriden in tests.
755
var ProxyURLFromHostPortGetter = func(addr string) (*url.URL, error) {
31✔
756
        u, err := url.Parse("https://" + addr)
31✔
757
        if err != nil {
31✔
758
                return u, err
×
759
        }
×
760
        return http.ProxyFromEnvironment(&http.Request{URL: u})
31✔
761
}
762

763
func getHostPort(u *url.URL) (hostPort string) {
×
764
        hostPort = u.Host
×
765
        if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
×
766
                return u.Host
×
767
        } else {
×
768
                switch u.Scheme {
×
769
                case "https":
×
770
                        hostPort += ":443"
×
771
                default:
×
772
                        hostPort += ":80"
×
773
                }
774
        }
775
        return hostPort
×
776
}
777

778
func dialProxy(network string, addr string, proxyURL *url.URL) (net.Conn, error) {
×
779
        var (
×
780
                resp *http.Response
×
781
                err  error
×
782
        )
×
783
        hostPort := getHostPort(proxyURL)
×
784
        conn, err := net.Dial(network, hostPort)
×
785
        if err != nil {
×
786
                return nil, err
×
787
        }
×
788

789
        connectHeader := make(http.Header)
×
790
        if user := proxyURL.User; user != nil {
×
791
                proxyUser := user.Username()
×
792
                if proxyPassword, passwordSet := user.Password(); passwordSet {
×
793
                        credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
×
794
                        connectHeader.Set("Proxy-Authorization", "Basic "+credential)
×
795
                }
×
796
        }
797

798
        connectReq := &http.Request{
×
799
                Method: http.MethodConnect,
×
800
                URL:    &url.URL{Opaque: addr},
×
801
                Host:   addr,
×
802
                Header: connectHeader,
×
803
        }
×
804
        connectCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
×
805
        defer cancel()
×
806

×
807
        didReadResponse := make(chan struct{})
×
808

×
809
        go func() {
×
810
                defer close(didReadResponse)
×
811
                if err := connectReq.Write(conn); err != nil {
×
812
                        return
×
813
                }
×
814
                br := bufio.NewReader(conn)
×
815
                resp, err = http.ReadResponse(br, connectReq) //nolint:bodyclose
×
816
        }()
817
        select {
×
818
        case <-connectCtx.Done():
×
819
                conn.Close()
×
820
                <-didReadResponse
×
821
                return nil, connectCtx.Err()
×
822
        case <-didReadResponse:
×
823
        }
824

825
        if err != nil {
×
826
                conn.Close()
×
827
                return nil, err
×
828
        }
×
829
        defer resp.Body.Close()
×
830

×
831
        if resp.StatusCode != http.StatusOK {
×
832
                conn.Close()
×
833
                return nil, errors.Errorf("Response line received from proxy: %s", resp.Status)
×
834
        }
×
835
        return conn, nil
×
836
}
837

838
func NewWebsocketDialer(conf Config) (*websocket.Dialer, error) {
31✔
839
        var dialer *websocket.Dialer
31✔
840
        var err error
31✔
841
        if conf == (Config{}) {
31✔
842
                dialer = websocket.DefaultDialer
×
843
        } else {
31✔
844
                dialer, err = newWebsocketDialerTLS(conf)
31✔
845
        }
31✔
846
        if err != nil {
31✔
847
                return nil, err
×
848
        }
×
849

850
        return dialer, nil
31✔
851
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc