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

mendersoftware / iot-manager / 1401702106

05 Aug 2024 08:32PM UTC coverage: 87.577%. Remained the same
1401702106

push

gitlab-ci

web-flow
Merge pull request #295 from mendersoftware/dependabot/docker/docker-dependencies-03b04ac819

chore: bump golang from 1.22.4-alpine3.19 to 1.22.5-alpine3.19 in the docker-dependencies group

3264 of 3727 relevant lines covered (87.58%)

11.44 hits per line

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

83.43
/model/connection_string.go
1
// Copyright 2022 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

15
package model
16

17
import (
18
        "crypto/hmac"
19
        "crypto/sha256"
20
        "encoding/base64"
21
        "fmt"
22
        "net"
23
        "net/url"
24
        "reflect"
25
        "strings"
26
        "time"
27

28
        validation "github.com/go-ozzo/ozzo-validation/v4"
29
        "github.com/pkg/errors"
30

31
        "github.com/mendersoftware/iot-manager/crypto"
32
)
33

34
const (
35
        csDelimiter    = ";"
36
        csVarSeparator = "="
37
        omitted        = "...<omitted>"
38

39
        csKeyHostName              = "HostName"
40
        csKeySharedAccessKey       = "SharedAccessKey"
41
        csKeySharedAccessKeyName   = "SharedAccessKeyName"
42
        csKeySharedAccessSignature = "SharedAccessSignature"
43
        csKeyDeviceId              = "DeviceId"
44
        csKeyModuleId              = "ModuleId"
45
)
46

47
var (
48
        ErrConnectionStringTooLong = errors.New(
49
                "connection string can be no longer than 4096 characters",
50
        )
51
        ErrHostnameTrust = errors.New(
52
                "hostname does not refer to a trusted domain",
53
        )
54

55
        trustedHostnames hostnameValidator
56
)
57

58
type ResolveError string
59
type lookupHost func(hostname string) error
60

61
var lookupHostFunc lookupHost = func(hostname string) error {
17✔
62
        _, err := net.LookupHost(hostname)
17✔
63
        return err
17✔
64
}
17✔
65

66
func (err ResolveError) Error() string {
×
67
        return fmt.Sprintf("failed to lookup host with name '%s'", string(err))
×
68
}
×
69

70
func SetTrustedHostnames(hostnames []string) {
4✔
71
        trustedHostnames = newHostnameValidator(hostnames)
4✔
72
}
4✔
73

74
// ConnectionString implements the Azure connection string format and the
75
// SharedAccessSignature authz algorithm.
76
// The implementation is based on the official python SDK.
77
// https://github.com/Azure/azure-iot-sdk-python
78
type ConnectionString struct {
79
        HostName        string        `cs:"HostName" bson:"hostname"`
80
        GatewayHostName string        `cs:"GatewayHostName" bson:"gateway_hostname,omitempty"`
81
        Name            string        `cs:"SharedAccessKeyName" bson:"name,omitempty"`
82
        DeviceID        string        `cs:"DeviceId" bson:"device_id,omitempty"`
83
        ModuleID        string        `cs:"ModuleId" bson:"module_id,omitempty"`
84
        Key             crypto.String `cs:"SharedAccessKey" bson:"access_key"`
85
        Signature       string        `cs:"SharedAccessSignature" bson:"-"`
86
}
87

88
func ParseConnectionString(connection string) (*ConnectionString, error) {
9✔
89
        cs := new(ConnectionString)
9✔
90
        csArgs := strings.Split(connection, csDelimiter)
9✔
91
        for _, arg := range csArgs {
34✔
92
                kv := strings.SplitN(arg, csVarSeparator, 2)
25✔
93
                if len(kv) != 2 {
26✔
94
                        return nil, errors.New("invalid connectionstring format")
1✔
95
                }
1✔
96
                switch kv[0] {
24✔
97
                case csKeyHostName:
8✔
98
                        cs.HostName = kv[1]
8✔
99
                case csKeySharedAccessKey:
8✔
100
                        key, err := base64.StdEncoding.DecodeString(kv[1])
8✔
101
                        if err != nil {
8✔
102
                                return nil, errors.Wrap(err, "shared access key format")
×
103
                        }
×
104
                        cs.Key = crypto.String(key)
8✔
105
                case csKeySharedAccessKeyName:
2✔
106
                        cs.Name = kv[1]
2✔
107
                case csKeySharedAccessSignature:
×
108
                        cs.Signature = kv[1]
×
109
                case csKeyDeviceId:
6✔
110
                        cs.DeviceID = kv[1]
6✔
111
                case csKeyModuleId:
×
112
                        cs.ModuleID = kv[1]
×
113
                default:
×
114
                        return nil, fmt.Errorf("invalid connection string key: %s", kv[0])
×
115
                }
116
        }
117
        return cs, errors.Wrap(cs.Validate(), "connection string invalid")
8✔
118
}
119

120
func (cs ConnectionString) IsZero() bool {
31✔
121
        rVal := reflect.ValueOf(cs)
31✔
122
        n := rVal.NumField()
31✔
123
        for i := 0; i < n; i++ {
62✔
124
                if !rVal.Field(i).IsZero() {
62✔
125
                        return false
31✔
126
                }
31✔
127
        }
128
        return true
×
129
}
130

131
func (cs ConnectionString) Validate() error {
27✔
132
        if cs.IsZero() {
27✔
133
                return nil
×
134
        }
×
135
        err := validation.ValidateStruct(&cs,
27✔
136
                validation.Field(&cs.HostName, validation.Required, trustedHostnames),
27✔
137
                validation.Field(&cs.Key, validation.Required),
27✔
138
                validation.Field(&cs.GatewayHostName, validation.When(
27✔
139
                        cs.GatewayHostName != "", trustedHostnames,
27✔
140
                )),
27✔
141
        )
27✔
142
        if err != nil {
28✔
143
                return err
1✔
144
        }
1✔
145
        if cs.DeviceID == "" && cs.Name == "" {
26✔
146
                return errors.New("one of 'DeviceId' or 'SharedAccessKeyName' must be set")
×
147
        }
×
148
        if len(cs.String()) > 4096 {
26✔
149
                return ErrConnectionStringTooLong
×
150
        }
×
151
        return nil
26✔
152
}
153

154
func (cs ConnectionString) Authorization(expireAt time.Time) string {
12✔
155
        qURI := url.QueryEscape(cs.HostName)
12✔
156
        msg := fmt.Sprintf("%s\n%d", qURI, expireAt.Unix())
12✔
157
        signer := hmac.New(sha256.New, []byte(cs.Key))
12✔
158
        _, _ = signer.Write([]byte(msg))
12✔
159
        sign := signer.Sum(nil)
12✔
160
        sign64 := base64.StdEncoding.EncodeToString(sign)
12✔
161
        token := fmt.Sprintf("SharedAccessSignature sr=%s&sig=%s&se=%d",
12✔
162
                qURI,
12✔
163
                url.QueryEscape(sign64),
12✔
164
                expireAt.Unix(),
12✔
165
        )
12✔
166
        if cs.Name != "" {
23✔
167
                token += "&skn=" + cs.Name
11✔
168
        }
11✔
169
        return token
12✔
170
}
171

172
func (cs ConnectionString) string(omit bool) string {
28✔
173
        val := reflect.ValueOf(cs)
28✔
174
        typ := val.Type()
28✔
175
        n := typ.NumField()
28✔
176
        var res = make([]string, 0, n)
28✔
177
        for i := 0; i < n; i++ {
224✔
178
                field := typ.Field(i)
196✔
179
                tag := field.Tag.Get("cs")
196✔
180
                if tag == "" {
196✔
181
                        continue
×
182
                }
183
                fieldVal := val.Field(i)
196✔
184
                if fieldVal.Len() == 0 {
308✔
185
                        continue
112✔
186
                }
187
                switch typ := fieldVal.Interface().(type) {
84✔
188
                case []byte:
×
189
                        res = append(res, tag+"="+base64.StdEncoding.EncodeToString(typ))
×
190
                case crypto.String:
28✔
191
                        var value string
28✔
192
                        if omit {
29✔
193
                                value = base64.StdEncoding.EncodeToString([]byte(typ[:3])) + omitted
1✔
194
                        } else {
28✔
195
                                value = base64.StdEncoding.EncodeToString([]byte(typ))
27✔
196
                        }
27✔
197
                        res = append(res, tag+"="+value)
28✔
198
                case string:
56✔
199
                        res = append(res, tag+"="+typ)
56✔
200
                default:
×
201
                        continue
×
202
                }
203
        }
204
        txt := strings.Join(res, csDelimiter)
28✔
205
        return txt
28✔
206
}
207

208
func (cs ConnectionString) String() string {
27✔
209
        return cs.string(false)
27✔
210
}
27✔
211

212
func (cs ConnectionString) MarshalText() ([]byte, error) {
1✔
213
        return []byte(cs.string(true)), nil
1✔
214
}
1✔
215

216
func (cs *ConnectionString) UnmarshalText(b []byte) error {
3✔
217
        if len(b) == 0 {
3✔
218
                return nil
×
219
        }
×
220
        connStr, err := ParseConnectionString(string(b))
3✔
221
        if err != nil {
3✔
222
                return err
×
223
        }
×
224
        *cs = *connStr
3✔
225
        return nil
3✔
226
}
227

228
type hostnameValidator [][]string
229

230
func newHostnameValidator(hostnames []string) hostnameValidator {
6✔
231
        // Compile the list of hostnames into a set of hostnames split by separator '.'
6✔
232
        var ret = make(hostnameValidator, len(hostnames))
6✔
233
        var j int
6✔
234
        for i := range hostnames {
18✔
235
                if hostnames[i] == "" {
13✔
236
                        continue
1✔
237
                }
238
                ret[i] = strings.Split(hostnames[i], ".")
11✔
239
                j++
11✔
240
        }
241
        ret = ret[:j]
6✔
242
        return ret
6✔
243
}
244

245
func (patterns hostnameValidator) matchHostname(hostname string) bool {
28✔
246
        hostname = strings.ToLower(strings.TrimSuffix(hostname, "."))
28✔
247
        if len(hostname) == 0 {
29✔
248
                return false
1✔
249
        }
1✔
250
        hostParts := strings.Split(hostname, ".")
27✔
251
        for _, patternParts := range patterns {
75✔
252
                if len(patternParts) != len(hostParts) {
69✔
253
                        continue
21✔
254
                }
255
                allMatch := true
27✔
256
                for i, patternPart := range patternParts {
107✔
257
                        if patternPart == "*" {
107✔
258
                                continue
27✔
259
                        }
260
                        if patternPart != hostParts[i] {
54✔
261
                                allMatch = false
1✔
262
                                break
1✔
263
                        }
264
                }
265
                if allMatch {
53✔
266
                        return true
26✔
267
                }
26✔
268
        }
269
        return false
1✔
270
}
271

272
func (patterns hostnameValidator) Validate(v interface{}) error {
30✔
273
        if len(patterns) == 0 {
31✔
274
                return errors.New("[PROG ERR(hostnameValidator)] no trusted hostnames configured")
1✔
275
        }
1✔
276
        hostname, ok := v.(string)
29✔
277
        if !ok {
30✔
278
                return errors.New("[PROG ERR(hostnameValidator)] validating non-string hostname")
1✔
279
        }
1✔
280
        hostname = strings.SplitN(hostname, ":", 2)[0]
28✔
281
        if !patterns.matchHostname(hostname) {
30✔
282
                return ErrHostnameTrust
2✔
283
        } else if err := lookupHostFunc(hostname); err != nil {
28✔
284
                return ResolveError(hostname)
×
285
        }
×
286
        return nil
26✔
287
}
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