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

mendersoftware / mender-cli / 1762341830

10 Apr 2025 12:40PM UTC coverage: 31.802%. Remained the same
1762341830

Pull #276

gitlab-ci

alfrunes
chore: Remove deprecated library `io/ioutil`

Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #276: chore: bump the golang-dependencies group with 2 updates

4 of 6 new or added lines in 6 files covered. (66.67%)

20 existing lines in 1 file now uncovered.

810 of 2547 relevant lines covered (31.8%)

1.68 hits per line

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

0.0
/client/deviceconnect/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 deviceconnect
15

16
import (
17
        "bytes"
18
        "context"
19
        "crypto/tls"
20
        "encoding/json"
21
        "fmt"
22
        "io"
23
        "mime/multipart"
24
        "net/http"
25
        "net/http/httputil"
26
        "net/url"
27
        "os"
28
        "strconv"
29
        "strings"
30
        "sync"
31
        "time"
32

33
        "github.com/gorilla/websocket"
34
        "github.com/mendersoftware/go-lib-micro/ws"
35
        "github.com/pkg/errors"
36
        "github.com/vmihailenco/msgpack"
37

38
        "github.com/mendersoftware/mender-cli/client"
39
        "github.com/mendersoftware/mender-cli/log"
40
)
41

42
const (
43
        // protocols
44
        httpProtocol = "http"
45
        wsProtocol   = "ws"
46

47
        // Time allowed to write a message to the peer.
48
        writeWait = 10 * time.Second
49

50
        // Time allowed to read the next pong message from the peer.
51
        pongWait = 1 * time.Minute
52

53
        // deviceconnect API path
54
        devicePath        = "/api/management/v1/deviceconnect/devices/:deviceID"
55
        deviceConnectPath = "/api/management/v1/deviceconnect/devices/:deviceID/connect"
56

57
        // fileUploadURL API path
58
        fileUploadURL = "/api/management/v1/deviceconnect/"
59
)
60

61
type Client struct {
62
        url        string
63
        skipVerify bool
64
        conn       *websocket.Conn
65
        readMutex  *sync.Mutex
66
        writeMutex *sync.Mutex
67
        token      string
68
        client     *http.Client
69
}
70

71
func NewClient(url string, token string, skipVerify bool) *Client {
×
72
        return &Client{
×
73
                url:        url,
×
74
                token:      token,
×
75
                skipVerify: skipVerify,
×
76
                client:     client.NewHttpClient(skipVerify),
×
77
                readMutex:  &sync.Mutex{},
×
78
                writeMutex: &sync.Mutex{},
×
79
        }
×
80
}
×
81

82
// Connect to the websocket
83
func (c *Client) Connect(deviceID string, token string) error {
×
84
        fmt.Fprintf(os.Stderr, "Connecting to the device %s...\n", deviceID)
×
85
        u, err := url.Parse(
×
86
                strings.TrimSuffix(
×
87
                        c.url,
×
88
                        "/",
×
89
                ) + strings.Replace(
×
90
                        deviceConnectPath,
×
91
                        ":deviceID",
×
92
                        deviceID,
×
93
                        1,
×
94
                ),
×
95
        )
×
96
        if err != nil {
×
97
                return errors.Wrap(err, "Unable to parse the server URL")
×
98
        }
×
99
        u.Scheme = strings.Replace(u.Scheme, httpProtocol, wsProtocol, 1)
×
100

×
101
        headers := http.Header{}
×
102
        headers.Set("Authorization", "Bearer "+string(token))
×
103
        websocket.DefaultDialer.TLSClientConfig = &tls.Config{
×
104
                InsecureSkipVerify: c.skipVerify,
×
105
        }
×
106
        conn, rsp, err := websocket.DefaultDialer.Dial(u.String(), headers)
×
107
        if err != nil {
×
108
                return errors.Wrap(err, "Unable to connect to the device")
×
109
        }
×
110
        defer rsp.Body.Close()
×
111

×
112
        err = conn.SetReadDeadline(time.Now().Add(pongWait))
×
113
        if err != nil {
×
114
                return errors.Wrap(err, "Unable to set the read deadline")
×
115
        }
×
116

117
        c.conn = conn
×
118
        return nil
×
119
}
120

121
// GetDevice returns the device
122
func (c *Client) GetDevice(deviceID string) (*Device, error) {
×
123
        path := strings.Replace(devicePath, ":deviceID", deviceID, 1)
×
124
        body, err := client.DoGetRequest(c.token, client.JoinURL(c.url, path), c.client)
×
125
        if err != nil {
×
126
                return nil, err
×
127
        }
×
128

129
        var device Device
×
130
        err = json.Unmarshal(body, &device)
×
131
        if err != nil {
×
132
                return nil, err
×
133
        }
×
134
        return &device, nil
×
135
}
136

137
// PingPong handles the ping-pong connection health check
138
func (c *Client) PingPong(ctx context.Context) {
×
139
        pingPeriod := (pongWait * 9) / 10
×
140
        ticker := time.NewTicker(pingPeriod)
×
141
        defer ticker.Stop()
×
142

×
143
        c.conn.SetPongHandler(func(string) error {
×
144
                ticker.Reset(pingPeriod)
×
145
                return c.conn.SetReadDeadline(time.Now().Add(pongWait))
×
146
        })
×
147

148
        c.conn.SetPingHandler(func(msg string) error {
×
149
                ticker.Reset(pingPeriod)
×
150
                err := c.conn.SetReadDeadline(time.Now().Add(pongWait))
×
151
                if err != nil {
×
152
                        return err
×
153
                }
×
154
                return c.conn.WriteControl(
×
155
                        websocket.PongMessage,
×
156
                        []byte(msg),
×
157
                        time.Now().Add(writeWait),
×
158
                )
×
159
        })
160

161
        for {
×
162
                select {
×
163
                case <-ticker.C:
×
164
                        pongWaitString := strconv.Itoa(int(pongWait.Seconds()))
×
165
                        _ = c.conn.WriteControl(
×
166
                                websocket.PingMessage,
×
167
                                []byte(pongWaitString),
×
168
                                time.Now().Add(writeWait),
×
169
                        )
×
170

171
                case <-ctx.Done():
×
172
                        return
×
173
                }
174
        }
175
}
176

177
// ReadMessage reads a Proto message from the websocket
178
func (c *Client) ReadMessage() (*ws.ProtoMsg, error) {
×
179
        c.readMutex.Lock()
×
180
        defer c.readMutex.Unlock()
×
181
        _, data, err := c.conn.ReadMessage()
×
182
        if err != nil {
×
183
                return nil, err
×
184
        }
×
185

186
        m := &ws.ProtoMsg{}
×
187
        err = msgpack.Unmarshal(data, m)
×
188
        if err != nil {
×
189
                return nil, err
×
190
        }
×
191
        return m, nil
×
192
}
193

194
// WriteMessage writes a Proto message to the websocket
195
func (c *Client) WriteMessage(m *ws.ProtoMsg) error {
×
196
        data, err := msgpack.Marshal(m)
×
197
        if err != nil {
×
198
                return errors.Wrap(err, "Unable to marshal the message from the websocket")
×
199
        }
×
200
        if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
×
201
                return errors.Wrap(err, "Unable to set the write deadline")
×
202
        }
×
203
        c.writeMutex.Lock()
×
204
        defer c.writeMutex.Unlock()
×
205
        if err := c.conn.WriteMessage(websocket.BinaryMessage, data); err != nil {
×
206
                return errors.Wrap(err, "Unable to write the message")
×
207
        }
×
208
        return nil
×
209
}
210

211
// Close closes the connection
212
func (c *Client) Close() {
×
213
        c.conn.Close()
×
214
}
×
215

216
func NewFileTransferClient(url string, token string, skipVerify bool) *Client {
×
217
        return &Client{
×
218
                url:    url,
×
219
                token:  token,
×
220
                client: client.NewHttpClient(skipVerify),
×
221
        }
×
222
}
×
223

224
type DeviceSpec struct {
225
        DeviceID   string
226
        DevicePath string
227
}
228

229
type DeviceConnectError struct {
230
        ErrorStr  string `json:"error"`
231
        RequestID string `json:"request_id"`
232
}
233

234
func (d *DeviceConnectError) Error() string {
×
235
        if d.ErrorStr != "" {
×
236
                if d.RequestID != "" {
×
237
                        return fmt.Sprintf("Error: [%s] %s", d.RequestID, d.ErrorStr)
×
238
                }
×
239
                return fmt.Sprintf("Error: %s", d.ErrorStr)
×
240
        }
241
        return "No Error string returned from the server. This is unexpected behaviour"
×
242
}
243

244
func NewDeviceConnectError(errCode int, r io.Reader) *DeviceConnectError {
×
NEW
245
        body, err := io.ReadAll(r)
×
246
        if err != nil {
×
247
                return &DeviceConnectError{
×
248
                        ErrorStr: fmt.Sprintf("Failed to upload the file. HTTP status code: %d", errCode),
×
249
                }
×
250
        }
×
251
        d := &DeviceConnectError{}
×
252
        if err = json.Unmarshal(body, d); err != nil {
×
253
                d.ErrorStr = string(body) // Just hope there is something sensible in the body
×
254
        }
×
255
        return d
×
256
}
257

258
func (c *Client) Upload(sourcePath string, deviceSpec *DeviceSpec) error {
×
259
        body := &bytes.Buffer{}
×
260
        writer := multipart.NewWriter(body)
×
261
        file, err := os.Open(sourcePath)
×
262
        if err != nil {
×
263
                return err
×
264
        }
×
265
        fi, err := os.Stat(sourcePath)
×
266
        if err != nil {
×
267
                return err
×
268
        }
×
269
        log.Verbf("Uploading the file to %s\n", deviceSpec.DevicePath)
×
270
        if err = writer.WriteField("path", deviceSpec.DevicePath); err != nil {
×
271
                return err
×
272
        }
×
273
        part, err := writer.CreateFormFile("file", sourcePath)
×
274
        if err != nil {
×
275
                return err
×
276
        }
×
277
        if _, err = io.Copy(part, file); err != nil {
×
278
                return err
×
279
        }
×
280
        if err = writer.WriteField("mode", fmt.Sprintf("%o", fi.Mode())); err != nil {
×
281
                return err
×
282
        }
×
283
        if err = writer.Close(); err != nil {
×
284
                return err
×
285
        }
×
286
        req, err := http.NewRequest(http.MethodPut,
×
287
                c.url+fileUploadURL+"devices/"+deviceSpec.DeviceID+"/upload",
×
288
                body)
×
289
        if err != nil {
×
290
                return err
×
291
        }
×
292
        req.Header.Set("Content-Type", writer.FormDataContentType())
×
293
        req.Header.Set("Authorization", "Bearer "+string(c.token))
×
294

×
295
        reqDump, _ := httputil.DumpRequest(req, false)
×
296
        log.Verbf("sending request: \n%v", string(reqDump))
×
297

×
298
        resp, err := c.client.Do(req)
×
299
        if err != nil {
×
300
                return err
×
301
        }
×
302
        defer resp.Body.Close()
×
303

×
304
        switch resp.StatusCode {
×
305
        case http.StatusCreated:
×
306
                return nil
×
307
        case http.StatusBadRequest:
×
308
                log.Err("Error: Bad request\n")
×
309
        case http.StatusForbidden:
×
310
                log.Err("Error: You are not allowed to access the given resource\n")
×
311
        case http.StatusNotFound:
×
312
                log.Err("Error: Resource not found\n")
×
313
        case http.StatusConflict:
×
314
                log.Err("Error: Device not connected\n")
×
315
        case http.StatusInternalServerError:
×
316
                log.Errf("Error: Internal Server Error\n")
×
317
        default:
×
318
                log.Errf("Error: Received unexpected response code: %d\n",
×
319
                        resp.StatusCode)
×
320
        }
321
        return NewDeviceConnectError(resp.StatusCode, resp.Body)
×
322
}
323

324
func (c *Client) Download(deviceSpec *DeviceSpec, sourcePath string) error {
×
325
        req, err := http.NewRequest(http.MethodGet,
×
326
                c.url+fileUploadURL+"devices/"+deviceSpec.DeviceID+"/download",
×
327
                nil,
×
328
        )
×
329
        if err != nil {
×
330
                return nil
×
331
        }
×
332
        req.Header.Set("Authorization", "Bearer "+string(c.token))
×
333
        q := req.URL.Query()
×
334
        q.Add("path", deviceSpec.DevicePath)
×
335
        req.URL.RawQuery = q.Encode()
×
336

×
337
        reqDump, _ := httputil.DumpRequest(req, false)
×
338
        log.Verbf("sending request: \n%v", string(reqDump))
×
339

×
340
        resp, err := c.client.Do(req)
×
341
        if err != nil {
×
342
                return err
×
343
        }
×
344
        defer resp.Body.Close()
×
345

×
346
        rspDump, _ := httputil.DumpResponse(resp, true)
×
347
        log.Verbf("Response: \n%v\n", string(rspDump))
×
348

×
349
        switch resp.StatusCode {
×
350
        case http.StatusOK:
×
351
                return c.downloadFile(sourcePath, resp)
×
352
        case http.StatusBadRequest:
×
353
                log.Err("Bad request\n")
×
354
        case http.StatusForbidden:
×
355
                log.Err("Forbidden")
×
356
        case http.StatusNotFound:
×
357
                log.Err("File not found on the device\n")
×
358
        case http.StatusConflict:
×
359
                log.Err("The device is not connected\n")
×
360
        case http.StatusInternalServerError:
×
361
                log.Err("Internal server error\n")
×
362
        default:
×
363
                log.Errf("Error: Received unexpected response code: %d\n",
×
364
                        resp.StatusCode)
×
365
        }
366
        return NewDeviceConnectError(resp.StatusCode, resp.Body)
×
367
}
368

369
func (c *Client) downloadFile(localFileName string, resp *http.Response) error {
×
370
        path := resp.Header.Get("X-MEN-FILE-PATH")
×
371
        uid := resp.Header.Get("X-MEN-FILE-UID")
×
372
        gid := resp.Header.Get("X-MEN-FILE-GID")
×
373
        mode := resp.Header.Get("X-MEN-FILE-MODE")
×
374
        if mode == "" {
×
375
                return errors.New("Missing X-MEN-FILE-MODE header")
×
376
        }
×
377
        modeo, err := strconv.ParseInt(mode, 8, 32)
×
378
        if err != nil {
×
379
                return err
×
380
        }
×
381
        _size := resp.Header.Get("X-MEN-FILE-SIZE")
×
382
        size, err := strconv.ParseInt(_size, 10, 64)
×
383
        if err != nil {
×
384
                return fmt.Errorf("No proper size given for the file: %s", _size)
×
385
        }
×
386
        var n int64
×
387
        file, err := os.OpenFile(localFileName, os.O_CREATE|os.O_WRONLY, os.FileMode(modeo))
×
388
        if err != nil {
×
389
                log.Errf("Failed to create the file %s locally\n", path)
×
390
                return err
×
391
        }
×
392
        defer file.Close()
×
393

×
394
        if resp.Header.Get("Content-Type") != "application/octet-stream" {
×
395
                return fmt.Errorf("Unexpected Content-Type header: %s", resp.Header.Get("Content-Type"))
×
396
        }
×
397
        if err != nil {
×
398
                log.Err("downloadFile: Failed to parse the Content-Type header")
×
399
                return err
×
400
        }
×
401
        n, err = io.Copy(file, resp.Body)
×
402
        log.Verbf("wrote: %d\n", n)
×
403
        if err != nil {
×
404
                return err
×
405
        }
×
406
        if n != size {
×
407
                return errors.New(
×
408
                        "The downloaded file does not match the expected length in 'X-MEN-FILE-SIZE'",
×
409
                )
×
410
        }
×
411
        // Set the proper permissions and {G,U}ID's if present
412
        if uid != "" && gid != "" {
×
413
                uidi, err := strconv.Atoi(uid)
×
414
                if err != nil {
×
415
                        return err
×
416
                }
×
417
                gidi, err := strconv.Atoi(gid)
×
418
                if err != nil {
×
419
                        return err
×
420
                }
×
421
                err = os.Chown(file.Name(), uidi, gidi)
×
422
                if err != nil {
×
423
                        return err
×
424
                }
×
425
        }
426
        return nil
×
427
}
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