"
35 |
36 | var keyCmd = &cobra.Command{
37 | Use: "key",
38 | Short: "Generate an install key and corresponding verifier",
39 | RunE: keyCmdRun,
40 | }
41 |
42 | func keyCmdRun(cmd *cobra.Command, args []string) error {
43 |
44 | var groupUUID uuid.UUID
45 | var err error
46 | if groupArg != defaultGroupArg {
47 | groupUUID, err = uuid.Parse(groupArg)
48 | if err != nil {
49 | return fmt.Errorf(`Could not parse "%s" as a UUID: %w`, groupArg, err)
50 | }
51 | } else {
52 | groupUUID, err = uuid.NewRandom()
53 | if err != nil {
54 | return fmt.Errorf("Failed to generate random group UUID: %w", err)
55 | }
56 | }
57 |
58 | key, err := auth.NewKey(groupUUID)
59 | if err != nil {
60 | return fmt.Errorf("Failed to generate install key: %w", err)
61 | }
62 |
63 | verifier, err := auth.NewVerifier(key)
64 | if err != nil {
65 | return fmt.Errorf("Failed to create verifier from install key: %w", err)
66 | }
67 |
68 | if !auth.Verify(key, verifier) {
69 | return fmt.Errorf("Generated key and verifier do not match")
70 | }
71 |
72 | keyJSONBytes, err := json.MarshalIndent(key, " ", " ")
73 | if err != nil {
74 | return fmt.Errorf("Failed to serialize generated key to JSON: %w", err)
75 | }
76 |
77 | verifierJSONBytes, err := json.MarshalIndent(verifier, " ", " ")
78 | if err != nil {
79 | return fmt.Errorf("Failed to serialize generated verifier to JSON: %w", err)
80 | }
81 |
82 | printString := fmt.Sprintf(`{
83 | "key": %s,
84 | "verifier": %s
85 | }`, string(keyJSONBytes), string(verifierJSONBytes))
86 |
87 | printedBytes, err := fmt.Println(printString)
88 | if err != nil {
89 | return fmt.Errorf("Failed to print generated key and verifier: %w", err)
90 | }
91 |
92 | if printedBytes < len(printString) {
93 | return fmt.Errorf("Failed to print entire key and verifier")
94 | }
95 |
96 | return nil
97 | }
98 |
99 | func init() {
100 | keyCmd.Flags().StringVar(&groupArg, "group", defaultGroupArg, "Valid UUID to indentify the sensor group")
101 | rootCmd.AddCommand(keyCmd)
102 | }
103 |
--------------------------------------------------------------------------------
/cmd/key_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package cmd
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/spf13/cobra"
26 | )
27 |
28 | var kC = &cobra.Command{
29 | Use: "key",
30 | Short: "test",
31 | }
32 |
33 | func TestGoodUUID(t *testing.T) {
34 | groupArg = "0c805482-41b5-4ed3-a425-ff2694498293"
35 | err := keyCmdRun(kC, make([]string, 0))
36 | if err != nil {
37 | t.Fatalf("Key command execution failed with supplied UUID: %v", err)
38 | }
39 | }
40 |
41 | func TestBadUUID(t *testing.T) {
42 | groupArg = "0c805482-41b5-4ed3-a425-f94498293"
43 | err := keyCmdRun(kC, make([]string, 0))
44 | if err == nil {
45 | t.Fatalf("Key command execution succeed with bad UUID")
46 | }
47 | }
48 |
49 | func TestNoUUID(t *testing.T) {
50 | groupArg = defaultGroupArg
51 | err := keyCmdRun(kC, make([]string, 0))
52 | if err != nil {
53 | t.Fatalf("Key command execution failed with no supplied UUID: %v", err)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/cmd/license.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package cmd
21 |
22 | import (
23 | "fmt"
24 |
25 | "github.com/spf13/cobra"
26 | )
27 |
28 | var licenseCmd = &cobra.Command{
29 | Use: "license",
30 | Short: "Display license information for this program",
31 | RunE: licenseCmdRun,
32 | }
33 |
34 | func licenseCmdRun(cmd *cobra.Command, args []string) error {
35 |
36 | licenseString := `
37 | This is Weaklayer Gateway.
38 |
39 | Weaklayer Gateway is free software: you can redistribute it and/or modify
40 | it under the terms of the GNU Affero General Public License as published by
41 | the Free Software Foundation, either version 3 of the License, or
42 | (at your option) any later version.
43 |
44 | This program is distributed in the hope that it will be useful,
45 | but WITHOUT ANY WARRANTY; without even the implied warranty of
46 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47 | GNU Affero General Public License for more details.
48 |
49 | You should have received a copy of the GNU Affero General Public License
50 | along with this program. If not, see .
51 | `
52 |
53 | printedBytes, err := fmt.Println(licenseString)
54 | if err != nil {
55 | return fmt.Errorf("Failed to display license information: %w", err)
56 | }
57 |
58 | if printedBytes < len(licenseString) {
59 | return fmt.Errorf("Failed to display entire license")
60 | }
61 |
62 | return nil
63 | }
64 |
65 | func init() {
66 | rootCmd.AddCommand(licenseCmd)
67 | }
68 |
--------------------------------------------------------------------------------
/cmd/license_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package cmd
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/spf13/cobra"
26 | )
27 |
28 | var lC = &cobra.Command{
29 | Use: "license",
30 | Short: "test",
31 | }
32 |
33 | func TestLicenseRun(t *testing.T) {
34 | err := licenseCmdRun(lC, make([]string, 0))
35 | if err != nil {
36 | t.Fatalf("License command execution failed: %v", err)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package cmd
21 |
22 | import (
23 | "fmt"
24 |
25 | "github.com/spf13/cobra"
26 | )
27 |
28 | var rootCmd = &cobra.Command{
29 | Use: "weaklayer-gateway",
30 | Short: "Weaklayer is a software system for browser detection and response",
31 | Long: `
32 | Welcome to Weaklayer Gateway
33 | This program contains the Weaklayer Gateway Server and associated admin utilities
34 | More information and documentation at https://weaklayer.com
35 | `,
36 | RunE: rootCmdRun,
37 | }
38 |
39 | func rootCmdRun(cmd *cobra.Command, args []string) error {
40 |
41 | message := `
42 | Use "weaklayer-gateway help" to display usage information
43 | `
44 |
45 | printedBytes, err := fmt.Println(message)
46 | if err != nil {
47 | return fmt.Errorf("Failed to display message: %w", err)
48 | }
49 |
50 | if printedBytes < len(message) {
51 | return fmt.Errorf("Failed to display entire message")
52 | }
53 |
54 | return nil
55 | }
56 |
57 | // Execute is the main entry point for this program
58 | func Execute() error {
59 | return rootCmd.Execute()
60 | }
61 |
--------------------------------------------------------------------------------
/cmd/root_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package cmd
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/spf13/cobra"
26 | )
27 |
28 | var rC = &cobra.Command{
29 | Use: "root",
30 | Short: "test",
31 | }
32 |
33 | func TestRootRun(t *testing.T) {
34 | err := rootCmdRun(rC, make([]string, 0))
35 | if err != nil {
36 | t.Fatalf("Root command execution failed: %v", err)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/secret.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package cmd
21 |
22 | import (
23 | "encoding/json"
24 | "fmt"
25 |
26 | "github.com/spf13/cobra"
27 | "github.com/weaklayer/gateway/common/auth"
28 | )
29 |
30 | var secretCmd = &cobra.Command{
31 | Use: "secret",
32 | Short: "Generate a random 512-bit secret",
33 | RunE: secretCmdRun,
34 | }
35 |
36 | type secretContainer struct {
37 | Secret []byte `json:"secret"`
38 | }
39 |
40 | func secretCmdRun(cmd *cobra.Command, args []string) error {
41 |
42 | secret, err := auth.NewRandomBytes(512 / 8)
43 | if err != nil {
44 | return fmt.Errorf("Failed to generate token signing secret: %w", err)
45 | }
46 |
47 | sc := secretContainer{
48 | Secret: secret,
49 | }
50 |
51 | secretContainerJSONBytes, err := json.MarshalIndent(sc, "", " ")
52 | if err != nil {
53 | return fmt.Errorf("Failed to serialize generated secret to JSON: %w", err)
54 | }
55 |
56 | secretContainerJSON := string(secretContainerJSONBytes)
57 |
58 | printedBytes, err := fmt.Println(secretContainerJSON)
59 | if err != nil {
60 | return fmt.Errorf("Failed to print generated secret: %w", err)
61 | }
62 |
63 | if printedBytes < len(secretContainerJSON) {
64 | return fmt.Errorf("Failed to print entire secret")
65 | }
66 |
67 | return nil
68 | }
69 |
70 | func init() {
71 | rootCmd.AddCommand(secretCmd)
72 | }
73 |
--------------------------------------------------------------------------------
/cmd/secret_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package cmd
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/spf13/cobra"
26 | )
27 |
28 | var sC = &cobra.Command{
29 | Use: "secret",
30 | Short: "test",
31 | }
32 |
33 | func TestSecretRun(t *testing.T) {
34 | err := secretCmdRun(sC, make([]string, 0))
35 | if err != nil {
36 | t.Fatalf("Secret command execution failed: %v", err)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/server.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package cmd
21 |
22 | import (
23 | "fmt"
24 | "strings"
25 |
26 | "encoding/json"
27 |
28 | "github.com/spf13/cobra"
29 | "github.com/spf13/viper"
30 | "github.com/weaklayer/gateway/common/auth"
31 | "github.com/weaklayer/gateway/server"
32 | "github.com/xeipuuv/gojsonschema"
33 | )
34 |
35 | type stringsConfig struct {
36 | Sensor struct {
37 | API struct {
38 | Host string `json:"host,omitempty"`
39 | Port int32 `json:"port,omitempty"`
40 | HTTPS struct {
41 | Certificate string `json:"certificate,omitempty"`
42 | Key string `json:"key,omitempty"`
43 | Password string `json:"password,omitempty"`
44 | } `json:"https,omitempty"`
45 | } `json:"api,omitempty"`
46 | Token struct {
47 | Duration int64 `json:"duration,omitempty"`
48 | Secrets struct {
49 | Current string `json:"current,omitempty"`
50 | Past []string `json:"past,omitempty"`
51 | } `json:"secrets,omitempty"`
52 | } `json:"token,omitempty"`
53 | Install struct {
54 | Verifiers []struct {
55 | Group string `json:"group,omitempty"`
56 | Salt string `json:"salt,omitempty"`
57 | Hash string `json:"hash,omitempty"`
58 | Checksum string `json:"checksum,omitempty"`
59 | } `json:"verifiers,omitempty"`
60 | } `json:"install,omitempty"`
61 | } `json:"sensor,omitempty"`
62 | Outputs []struct {
63 | Type string `json:"type,omitempty"`
64 | Directory string `json:"directory,omitempty"`
65 | Age int64 `json:"age,omitempty"`
66 | Size int `json:"size,omitempty"`
67 | } `json:"outputs,omitempty"`
68 | }
69 |
70 | var configJSONSchema = fmt.Sprintf(`
71 | {
72 | "$schema": "http://json-schema.org/draft-07/schema#",
73 | "title": "Config",
74 | "type": "object",
75 | "required": ["sensor", "outputs"],
76 | "properties": {
77 | "sensor": {
78 | "type": "object",
79 | "required": ["token", "api"],
80 | "properties": {
81 |
82 | "token": {
83 | "type": "object",
84 | "required": ["secrets", "duration"],
85 | "properties": {
86 | "duration": {
87 | "type": "integer",
88 | "minimum": 1,
89 | "description": "The number of microseconds that issued tokens are valid for."
90 | },
91 | "secrets": {
92 | "type": "object",
93 | "required": ["current"],
94 | "properties": {
95 | "current": {
96 | "type": "string",
97 | "pattern": "^[a-zA-z0-9+\/]{86}==$",
98 | "example": "4QIuEulkopN+QbyMXOMVCKZBps0JjutFpI7U1OI6FgUFY6587Be9xLRPUSOK7fajM+RokZSwv3F31t5XAbTXnQ==",
99 | "description": "A 512-bit secret value that is used to sign and verify sensor auth tokens."
100 | },
101 | "past": {
102 | "type": "array",
103 | "items": {
104 | "type": "string",
105 | "pattern": "^[a-zA-z0-9+\/]{86}==$",
106 | "example": "4QIuEulkopN+QbyMXOMVCKZBps0JjutFpI7U1OI6FgUFY6587Be9xLRPUSOK7fajM+RokZSwv3F31t5XAbTXnQ==",
107 | "description": "A 512-bit secret value that is used to sign and verify sensor auth tokens."
108 | }
109 | }
110 | }
111 | }
112 | }
113 | },
114 |
115 | "install": {
116 | "type": "object",
117 | "properties": {
118 | "verifiers": {
119 | "type": "array",
120 | "items": %s
121 | }
122 | }
123 | },
124 |
125 | "api": {
126 | "type": "object",
127 | "required": ["host", "port"],
128 | "properties": {
129 | "host": {
130 | "type": "string",
131 | "format": "hostname",
132 | "example": "localhost",
133 | "description": "The host that the sensor API listens on."
134 | },
135 | "port": {
136 | "type": "integer",
137 | "minimum": 0,
138 | "maximum": 65535,
139 | "example": 8080,
140 | "description": "The port that the sensor API listens on."
141 | },
142 | "https": {
143 | "type": "object",
144 | "description": "Config values for enabling https on the sensor api.",
145 | "properties": {
146 | "certificate": {
147 | "type": "string",
148 | "example": "/home/weaklayer/certificate.pem",
149 | "description": "Path to a TLS certificate (PEM format)"
150 | },
151 | "key": {
152 | "type": "string",
153 | "example": "/home/weaklayer/key.pem",
154 | "description": "Path to the TLS certificate private key (PEM format)"
155 | },
156 | "password": {
157 | "type": "string",
158 | "example": "examplekeypassword",
159 | "description": "Password for decrypting the private key (if applicable)."
160 | }
161 | }
162 | }
163 | }
164 | }
165 | }
166 | },
167 | "outputs": {
168 | "type": "array",
169 | "items": {
170 | "type": "object",
171 | "required": ["type"],
172 | "properties": {
173 | "type": {
174 | "type": "string",
175 | "enum": ["stdout", "filesystem"],
176 | "example": "stdout",
177 | "description": "The type of output to configure"
178 | },
179 | "directory": {
180 | "type": "string",
181 | "example": "./weaklayer-events",
182 | "description": "Directory that the gateway will write events to"
183 | },
184 | "age": {
185 | "type": "integer",
186 | "minimum": 1,
187 | "example": 3600,
188 | "description": "The file age, in microseconds, that the filesystem output will close files at"
189 | },
190 | "size": {
191 | "type": "integer",
192 | "minimum": 1,
193 | "example": 250000000,
194 | "description": "The file size, in bytes, that the filesystem output will close files at"
195 | }
196 | }
197 | }
198 | }
199 | }
200 | }
201 | `, auth.VerifierJSONSchema)
202 |
203 | var configFilePath = ""
204 |
205 | var serverCmd = &cobra.Command{
206 | Use: "server",
207 | Short: "Run the Weaklayer Gateway Server",
208 | RunE: serverCmdRun,
209 | }
210 |
211 | func serverCmdRun(cmd *cobra.Command, args []string) error {
212 |
213 | // Configs where a default makes sense
214 | viper.SetDefault("sensor.api.host", "localhost")
215 | viper.SetDefault("sensor.api.port", 8080)
216 | viper.SetDefault("sensor.token.duration", 2419200000000)
217 |
218 | if configFilePath != "" {
219 | viper.SetConfigFile(configFilePath)
220 | } else {
221 | return fmt.Errorf("Must specify a config file with --config")
222 | }
223 |
224 | viper.SetEnvPrefix("WEAKLAYER")
225 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
226 | viper.AutomaticEnv()
227 |
228 | err := viper.ReadInConfig()
229 | if err != nil {
230 | return fmt.Errorf("Failed to read config: %w", err)
231 | }
232 |
233 | var mergedConfig stringsConfig
234 |
235 | err = viper.Unmarshal(&mergedConfig)
236 | if err != nil {
237 | return fmt.Errorf("Failed to unmarshall config into struct: %w", err)
238 | }
239 |
240 | // We serialize the config from viper to json and back
241 | // This is primarily done because viper wont unmarshal a base64 string into a byte slice
242 | // json unmarshal does so we can marshal to json and back for the desired effect
243 | // we get the added bonus now of being able to use json schema validation
244 | mergedConfigBytes, err := json.Marshal(mergedConfig)
245 | if err != nil {
246 | return fmt.Errorf("Failed to convert config into normalized format: %w", err)
247 | }
248 |
249 | err = validateConfigJSON(mergedConfigBytes)
250 | if err != nil {
251 | return fmt.Errorf("Config validation failed: %w", err)
252 | }
253 |
254 | var finalConfig server.Config
255 | err = json.Unmarshal(mergedConfigBytes, &finalConfig)
256 | if err != nil {
257 | return fmt.Errorf("Failed to convert config into normalized format: %w", err)
258 | }
259 |
260 | err = validateConfigStruct(finalConfig)
261 | if err != nil {
262 | return fmt.Errorf("Config validation failed: %w", err)
263 | }
264 |
265 | return server.Run(finalConfig)
266 | }
267 |
268 | func validateConfigJSON(jsonBytes []byte) error {
269 |
270 | schemaLoader := gojsonschema.NewStringLoader(configJSONSchema)
271 | schema, err := gojsonschema.NewSchema(schemaLoader)
272 | if err != nil {
273 | return fmt.Errorf("Failed to load key JSON schema: %w", err)
274 | }
275 |
276 | documentLoader := gojsonschema.NewBytesLoader(jsonBytes)
277 | result, err := schema.Validate(documentLoader)
278 | if err != nil {
279 | return fmt.Errorf("Failed to validate against config JSON schema: %w", err)
280 | }
281 |
282 | if !result.Valid() {
283 | return fmt.Errorf("Config JSON schema validation returned not valid")
284 | }
285 |
286 | return nil
287 | }
288 |
289 | func validateConfigStruct(config server.Config) error {
290 | // The assumption here is that the json version of the config was already validated
291 | // This is for some extra stuff that can't be done with the json schema.
292 | // for example, verifier checksums
293 |
294 | for i, verifier := range config.Sensor.Install.Verifiers {
295 | if !auth.IsVerifierValid(verifier) {
296 | return fmt.Errorf("The checksum of install verfier at index %d did not match", i)
297 | }
298 | }
299 |
300 | // check that either both or neither of certificate and private key are specified for https
301 | if (config.Sensor.API.HTTPS.Certificate != "" && config.Sensor.API.HTTPS.Key == "") ||
302 | (config.Sensor.API.HTTPS.Certificate == "" && config.Sensor.API.HTTPS.Key != "") {
303 | return fmt.Errorf("Both a certificate and key must be specified to enable https")
304 | }
305 |
306 | if len(config.Outputs) == 0 {
307 | return fmt.Errorf("Must configure at least one output")
308 | }
309 | for i, configOutput := range config.Outputs {
310 | if configOutput.Type == "stdout" {
311 | // no fields for stdoutput
312 | } else if configOutput.Type == "filesystem" {
313 | directory := configOutput.Directory
314 | if directory == "" {
315 | return fmt.Errorf("Must specify a directory for filesystem output at location %d in outputs array", i)
316 | }
317 | age := configOutput.Age
318 | if age <= 0 {
319 | return fmt.Errorf("Must specify a strictly positive age for filesystem output at location %d in outputs array", i)
320 | }
321 | size := configOutput.Size
322 | if size <= 0 {
323 | return fmt.Errorf("Must specify a strictly positive size for filesystem output at location %d in outputs array", i)
324 | }
325 | } else {
326 | return fmt.Errorf("Unknown output type %s at at location %d in outputs array", configOutput.Type, i)
327 | }
328 | }
329 |
330 | return nil
331 | }
332 |
333 | func init() {
334 |
335 | serverCmd.Flags().StringVar(&configFilePath, "config", "", `Path to the desired config file
336 | Permitted formats are YAML, JSON, TOML, HCL, envfile and Java properties config files`)
337 |
338 | rootCmd.AddCommand(serverCmd)
339 | }
340 |
--------------------------------------------------------------------------------
/cmd/server_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package cmd
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/spf13/cobra"
26 | )
27 |
28 | var serverC = &cobra.Command{
29 | Use: "server",
30 | Short: "test",
31 | }
32 |
33 | func TestServerNoConfig(t *testing.T) {
34 | configFilePath = ""
35 | err := serverCmdRun(serverC, make([]string, 0))
36 | if err == nil {
37 | t.Fatalf("Server command did not produce error")
38 | }
39 | t.Logf("%v", err)
40 | }
41 |
42 | func TestServerBadConfigPath(t *testing.T) {
43 | configFilePath = "test-configs/doesnt_exist.yaml"
44 | err := serverCmdRun(serverC, make([]string, 0))
45 | if err == nil {
46 | t.Fatalf("Server command did not produce error")
47 | }
48 | t.Logf("%v", err)
49 | }
50 |
51 | func TestServerBadYamlConfig(t *testing.T) {
52 | configFilePath = "test-configs/invalid_yaml.yaml"
53 | err := serverCmdRun(serverC, make([]string, 0))
54 | if err == nil {
55 | t.Fatalf("Server command did not produce error")
56 | }
57 | t.Logf("%v", err)
58 | }
59 |
60 | func TestServerMissingTokenSecretConfig(t *testing.T) {
61 | configFilePath = "test-configs/missing_current_secret.yaml"
62 | err := serverCmdRun(serverC, make([]string, 0))
63 | if err == nil {
64 | t.Fatalf("Server command did not produce error")
65 | }
66 | t.Logf("%v", err)
67 | }
68 |
69 | func TestServerBadTokenSecretConfig(t *testing.T) {
70 | configFilePath = "test-configs/bad_current_secret.yaml"
71 | err := serverCmdRun(serverC, make([]string, 0))
72 | if err == nil {
73 | t.Fatalf("Server command did not produce error")
74 | }
75 | t.Logf("%v", err)
76 | }
77 |
78 | func TestServerBadInstallKeyChecksumsConfig(t *testing.T) {
79 | configFilePath = "test-configs/bad_checksum.yaml"
80 | err := serverCmdRun(serverC, make([]string, 0))
81 | if err == nil {
82 | t.Fatalf("Server command did not produce error")
83 | }
84 | t.Logf("%v", err)
85 | }
86 |
87 | func TestServerBadHttpsConfig(t *testing.T) {
88 | configFilePath = "test-configs/bad_https.yaml"
89 | err := serverCmdRun(serverC, make([]string, 0))
90 | if err == nil {
91 | t.Fatalf("Server command did not produce error")
92 | }
93 | t.Logf("%v", err)
94 | }
95 |
96 | func TestServerMissingOutputConfig(t *testing.T) {
97 | configFilePath = "test-configs/missing_outputs.yaml"
98 | err := serverCmdRun(serverC, make([]string, 0))
99 | if err == nil {
100 | t.Fatalf("Server command did not produce error")
101 | }
102 | t.Logf("%v", err)
103 | }
104 |
105 | func TestServerMissingFilesystemOutputDirectoryConfig(t *testing.T) {
106 | configFilePath = "test-configs/missing_output_filesystem_directory.yaml"
107 | err := serverCmdRun(serverC, make([]string, 0))
108 | if err == nil {
109 | t.Fatalf("Server command did not produce error")
110 | }
111 | t.Logf("%v", err)
112 | }
113 |
--------------------------------------------------------------------------------
/cmd/test-configs/bad_checksum.yaml:
--------------------------------------------------------------------------------
1 | sensor:
2 | api:
3 | host: localhost
4 | port: 8080
5 | token:
6 | duration: 2419200000000
7 | secrets:
8 | current: jZAcBYO3EussMYp/GtfAx+luMZd17BQX3lgLxCGTLBY47pR2glLuC4XdUlIe70O19Uu4Yy5zqzH7aIbVQbl7cw==
9 | past:
10 | - sQBc60569Z4mcf0Idf6OIu+u+Ht+vjz9rqQDx7/mdHUKoMWcFpvfcHRA3Ta5yB1HlHsIRkRajLtdLFmRgpwtiQ==
11 | - zAVlcn/qje8jAjL6HWyMZrKiKuYgLIWfVn4uHZA2KaPLbQWCII9wymJe+WPmAph2Qbau5pYDPXAzjVfYo2pU6Q==
12 | install:
13 | verifiers:
14 | -
15 | group: 68886d61-572b-41a5-8edd-93a564fb5ba3
16 | salt: oWrccA==
17 | hash: bAW4dciXby0vAAhqHDebxsLioy6H3eYzGqvL8CMRhh0=
18 | # modified checksum slightly from good value
19 | checksum: fhpiskuymKjvsyRKJ/U9ohdKRDpgft4x/Exn/zHM1zd=
20 | outputs:
21 | - type: stdout
22 |
--------------------------------------------------------------------------------
/cmd/test-configs/bad_current_secrety.yaml:
--------------------------------------------------------------------------------
1 | sensor:
2 | api:
3 | host: localhost
4 | port: 8080
5 | token:
6 | duration: 2419200000000
7 | secrets:
8 | # current secret is too short
9 | current: jZAcBYO3EussMYpGTLBY47pR2glLuC4XdUlIe70O19Uu4Yy5zqzH7aIbVQbl7cw==
10 | past:
11 | - sQBc60569Z4mcf0Idf6OIu+u+Ht+vjz9rqQDx7/mdHUKoMWcFpvfcHRA3Ta5yB1HlHsIRkRajLtdLFmRgpwtiQ==
12 | - zAVlcn/qje8jAjL6HWyMZrKiKuYgLIWfVn4uHZA2KaPLbQWCII9wymJe+WPmAph2Qbau5pYDPXAzjVfYo2pU6Q==
13 | install:
14 | verifiers:
15 | -
16 | group: 68886d61-572b-41a5-8edd-93a564fb5ba3
17 | salt: oWrccA==
18 | hash: bAW4dciXby0vAAhqHDebxsLioy6H3eYzGqvL8CMRhh0=
19 | checksum: fhpiskuymKjvsyRKJ/U9ohdKRDpgft4x/Exn/zHM1zs=
20 | outputs:
21 | - type: stdout
22 |
--------------------------------------------------------------------------------
/cmd/test-configs/bad_https.yaml:
--------------------------------------------------------------------------------
1 | sensor:
2 | api:
3 | host: localhost
4 | port: 8080
5 | https:
6 | certificate: cert_example.pem
7 | # we specify the cert but not a key
8 | token:
9 | duration: 2419200000000
10 | secrets:
11 | current: jZAcBYO3EussMYp/GtfAx+luMZd17BQX3lgLxCGTLBY47pR2glLuC4XdUlIe70O19Uu4Yy5zqzH7aIbVQbl7cw==
12 | past:
13 | - sQBc60569Z4mcf0Idf6OIu+u+Ht+vjz9rqQDx7/mdHUKoMWcFpvfcHRA3Ta5yB1HlHsIRkRajLtdLFmRgpwtiQ==
14 | - zAVlcn/qje8jAjL6HWyMZrKiKuYgLIWfVn4uHZA2KaPLbQWCII9wymJe+WPmAph2Qbau5pYDPXAzjVfYo2pU6Q==
15 | install:
16 | verifiers:
17 | -
18 | group: 68886d61-572b-41a5-8edd-93a564fb5ba3
19 | salt: oWrccA==
20 | hash: bAW4dciXby0vAAhqHDebxsLioy6H3eYzGqvL8CMRhh0=
21 | checksum: fhpiskuymKjvsyRKJ/U9ohdKRDpgft4x/Exn/zHM1zs=
22 | outputs:
23 | - type: stdout
24 |
--------------------------------------------------------------------------------
/cmd/test-configs/invalid_yaml.yaml:
--------------------------------------------------------------------------------
1 | wfdkhgwbwjfbjkhgfbj
2 | {}wdg
3 |
4 | dfgsdfgsdb
5 |
6 | bcvbxcv
7 |
--------------------------------------------------------------------------------
/cmd/test-configs/missing_current_secret.yaml:
--------------------------------------------------------------------------------
1 | sensor:
2 | api:
3 | host: localhost
4 | port: 8080
5 | token:
6 | duration: 2419200000000
7 | secrets:
8 | past:
9 | - sQBc60569Z4mcf0Idf6OIu+u+Ht+vjz9rqQDx7/mdHUKoMWcFpvfcHRA3Ta5yB1HlHsIRkRajLtdLFmRgpwtiQ==
10 | - zAVlcn/qje8jAjL6HWyMZrKiKuYgLIWfVn4uHZA2KaPLbQWCII9wymJe+WPmAph2Qbau5pYDPXAzjVfYo2pU6Q==
11 | install:
12 | verifiers:
13 | -
14 | group: 68886d61-572b-41a5-8edd-93a564fb5ba3
15 | salt: oWrccA==
16 | hash: bAW4dciXby0vAAhqHDebxsLioy6H3eYzGqvL8CMRhh0=
17 | checksum: fhpiskuymKjvsyRKJ/U9ohdKRDpgft4x/Exn/zHM1zs=
18 | outputs:
19 | - type: stdout
20 |
--------------------------------------------------------------------------------
/cmd/test-configs/missing_output_filesystem_directory.yaml:
--------------------------------------------------------------------------------
1 | sensor:
2 | api:
3 | host: localhost
4 | port: 8080
5 | token:
6 | duration: 2419200000000
7 | secrets:
8 | current: jZAcBYO3EussMYp/GtfAx+luMZd17BQX3lgLxCGTLBY47pR2glLuC4XdUlIe70O19Uu4Yy5zqzH7aIbVQbl7cw==
9 | past:
10 | - sQBc60569Z4mcf0Idf6OIu+u+Ht+vjz9rqQDx7/mdHUKoMWcFpvfcHRA3Ta5yB1HlHsIRkRajLtdLFmRgpwtiQ==
11 | - zAVlcn/qje8jAjL6HWyMZrKiKuYgLIWfVn4uHZA2KaPLbQWCII9wymJe+WPmAph2Qbau5pYDPXAzjVfYo2pU6Q==
12 | install:
13 | verifiers:
14 | -
15 | group: 68886d61-572b-41a5-8edd-93a564fb5ba3
16 | salt: oWrccA==
17 | hash: bAW4dciXby0vAAhqHDebxsLioy6H3eYzGqvL8CMRhh0=
18 | checksum: fhpiskuymKjvsyRKJ/U9ohdKRDpgft4x/Exn/zHM1zs=
19 | outputs:
20 | - type: filesystem
21 | age: 1
22 | size: 1
23 |
--------------------------------------------------------------------------------
/cmd/test-configs/missing_outputs.yaml:
--------------------------------------------------------------------------------
1 | sensor:
2 | api:
3 | host: localhost
4 | port: 8080
5 | token:
6 | duration: 2419200000000
7 | secrets:
8 | current: jZAcBYO3EussMYp/GtfAx+luMZd17BQX3lgLxCGTLBY47pR2glLuC4XdUlIe70O19Uu4Yy5zqzH7aIbVQbl7cw==
9 | past:
10 | - sQBc60569Z4mcf0Idf6OIu+u+Ht+vjz9rqQDx7/mdHUKoMWcFpvfcHRA3Ta5yB1HlHsIRkRajLtdLFmRgpwtiQ==
11 | - zAVlcn/qje8jAjL6HWyMZrKiKuYgLIWfVn4uHZA2KaPLbQWCII9wymJe+WPmAph2Qbau5pYDPXAzjVfYo2pU6Q==
12 | install:
13 | verifiers:
14 | -
15 | group: 68886d61-572b-41a5-8edd-93a564fb5ba3
16 | salt: oWrccA==
17 | hash: bAW4dciXby0vAAhqHDebxsLioy6H3eYzGqvL8CMRhh0=
18 | checksum: fhpiskuymKjvsyRKJ/U9ohdKRDpgft4x/Exn/zHM1zs=
19 |
--------------------------------------------------------------------------------
/common/auth/auth.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package auth
21 |
22 | import (
23 | "bytes"
24 | "crypto/rand"
25 | "crypto/sha256"
26 | "fmt"
27 |
28 | "github.com/google/uuid"
29 | "github.com/rs/zerolog/log"
30 | "golang.org/x/crypto/pbkdf2"
31 | )
32 |
33 | // KeyJSONSchema should be used to validate keys in JSON form
34 | const KeyJSONSchema = `
35 | {
36 | "$schema": "http://json-schema.org/draft-07/schema#",
37 | "title": "Key",
38 | "type": "object",
39 | "properties": {
40 | "group": {
41 | "type": "string",
42 | "format": "uuid",
43 | "example": "73d0710f-c4a4-468a-9087-a06073bebe8c",
44 | "description": "The sensor group that this key enables installation into."
45 | },
46 | "secret": {
47 | "type": "string",
48 | "pattern": "^[a-zA-z0-9+\/]{86}==$",
49 | "example": "4QIuEulkopN+QbyMXOMVCKZBps0JjutFpI7U1OI6FgUFY6587Be9xLRPUSOK7fajM+RokZSwv3F31t5XAbTXnQ==",
50 | "description": "A 512-bit secret value that acts as a password for installing a sensor."
51 | },
52 | "checksum": {
53 | "type": "string",
54 | "pattern": "^[a-zA-z0-9+\/]{43}=$",
55 | "example": "iNg+ovQngoycxTRsb3byxjuE26E5eUQChVmmh54e2To=",
56 | "description": "A 256-bit hash of the other fields to determine if mistakes were made when entering a key."
57 | }
58 | }
59 | }
60 | `
61 |
62 | // Key is a value that administrators will configure the Weaklayer Sensor with.
63 | // Its purpose is to authenticate the sensor to the gateway so we know it
64 | // is allowed to send data.
65 | type Key struct {
66 | Group uuid.UUID `json:"group"`
67 | Secret []byte `json:"secret"`
68 | Checksum []byte `json:"checksum"`
69 | }
70 |
71 | // Verifier is data provided to the Weaklayer Gateway so it can verify the key
72 | // that a Weaklayer Sensor Provides.
73 | type Verifier struct {
74 | Group uuid.UUID `json:"group"`
75 | Salt []byte `json:"salt"`
76 | Hash []byte `json:"hash"`
77 | Checksum []byte `json:"checksum"`
78 | }
79 |
80 | // VerifierJSONSchema should be used to validate verifiers in JSON form
81 | const VerifierJSONSchema = `
82 | {
83 | "$schema": "http://json-schema.org/draft-07/schema#",
84 | "title": "Verifier",
85 | "type": "object",
86 | "properties": {
87 | "group": {
88 | "type": "string",
89 | "format": "uuid",
90 | "example": "73d0710f-c4a4-468a-9087-a06073bebe8c",
91 | "description": "The sensor group that this verifier enables installation into."
92 | },
93 | "salt": {
94 | "type": "string",
95 | "pattern": "^[a-zA-z0-9+\/]{6}==$",
96 | "example": "SGQ7Fw==",
97 | "description": "A 32-bit random value used in the calculation of the install key secret hash."
98 | },
99 | "hash": {
100 | "type": "string",
101 | "pattern": "^[a-zA-z0-9+\/]{43}=$",
102 | "example": "BioOJy7op91gsmlO+L9PMAsRrUWNKP5JJQZ6JwCjlk4=",
103 | "description": "A 256-bit hash of the install key secret and the validator salt. Note this is a much more expensive hash is used here than for the checksum."
104 | },
105 | "checksum": {
106 | "type": "string",
107 | "pattern": "^[a-zA-z0-9+\/]{43}=$",
108 | "example": "jDmzy23i1Db7on0hTLZTsnATlGg79QOwwHZprlmSJ18=",
109 | "description": "A hash of the other fields to determine if mistakes were made when entering a validator."
110 | }
111 | }
112 | }
113 | `
114 |
115 | // NewKey creates a new install key with a random secret for the provided group
116 | // On error, the default Key is returned with error set
117 | func NewKey(group uuid.UUID) (Key, error) {
118 | var key Key
119 |
120 | secret, err := NewRandomBytes(64)
121 | if err != nil {
122 | return key, fmt.Errorf("Install key secret generation failed: %w", err)
123 | }
124 |
125 | checksum, err := calculateKeyChecksum(group, secret)
126 | if err != nil {
127 | return key, fmt.Errorf("Install key checksum calculation failed: %w", err)
128 | }
129 |
130 | key = Key{
131 | Group: group,
132 | Secret: secret,
133 | Checksum: checksum,
134 | }
135 |
136 | return key, nil
137 | }
138 |
139 | // NewVerifier creates a new install verifier from an existing key and a random salt
140 | // On error, the default Verifier is returned with error set
141 | func NewVerifier(key Key) (Verifier, error) {
142 | var verifier Verifier
143 |
144 | // 32-bit salt according to NIST reccomendations
145 | salt, err := NewRandomBytes(4)
146 | if err != nil {
147 | return verifier, fmt.Errorf("Install verifier salt generation failed: %w", err)
148 | }
149 |
150 | hash := calculateHash(key, salt)
151 |
152 | sum, err := calculateVerifierChecksum(key.Group, salt, hash)
153 | if err != nil {
154 | return verifier, fmt.Errorf("Install verifier checksum calculation failed: %w", err)
155 | }
156 |
157 | verifier = Verifier{
158 | Group: key.Group,
159 | Salt: salt,
160 | Hash: hash,
161 | Checksum: sum,
162 | }
163 |
164 | return verifier, nil
165 | }
166 |
167 | // Verify ensures the following:
168 | // - GroupIDs match
169 | // - Key Checksum valid
170 | // - Verifier Checksum valid
171 | // - Key secret matches Verifier hash
172 | //
173 | // Returns true if all these conditions are met and no errors. False otherwise.
174 | func Verify(key Key, verifier Verifier) bool {
175 | return UUIDEquals(key.Group, verifier.Group) && isKeyChecksumValid(key) && IsVerifierValid(verifier) && isHashValid(key, verifier)
176 | }
177 |
178 | // IsVerifierValid ensures the verifier's checksum matches
179 | func IsVerifierValid(verifier Verifier) bool {
180 | calculatedChecksum, err := calculateVerifierChecksum(verifier.Group, verifier.Salt, verifier.Hash)
181 | if err != nil {
182 | log.Info().Err(err).Msg("Install verifier checksum calculation failed")
183 | return false
184 | }
185 |
186 | return bytes.Equal(calculatedChecksum, verifier.Checksum)
187 | }
188 |
189 | // UUIDEquals returns true if the two UUID contents are equal. False otherwise.
190 | func UUIDEquals(u1 uuid.UUID, u2 uuid.UUID) bool {
191 | u1Bytes, err := u1.MarshalBinary()
192 | if err != nil {
193 | log.Warn().Err(err).Msg("Failed to marshal UUID to bytes")
194 | return false
195 | }
196 |
197 | u2Bytes, err := u2.MarshalBinary()
198 | if err != nil {
199 | log.Warn().Err(err).Msg("Failed to marshal UUID to bytes")
200 | return false
201 | }
202 |
203 | return bytes.Equal(u1Bytes, u2Bytes)
204 | }
205 |
206 | // CalculateHash produces an install key hash given the salt and install key
207 | func calculateHash(key Key, Salt []byte) []byte {
208 | // 10000 iterations according to NIST reccomendations
209 | return pbkdf2.Key(key.Secret, Salt, 10000, 32, sha256.New)
210 | }
211 |
212 | func isHashValid(key Key, verifier Verifier) bool {
213 | calculatedHash := calculateHash(key, verifier.Salt)
214 | return bytes.Equal(verifier.Hash, calculatedHash)
215 | }
216 |
217 | func calculateChecksum(input []byte) ([]byte, error) {
218 | digest := sha256.New()
219 | b, err := digest.Write(input)
220 | if err != nil || b != len(input) {
221 | return nil, fmt.Errorf("Error calculating sha256 digest: %w", err)
222 | }
223 |
224 | return digest.Sum(nil), nil
225 | }
226 |
227 | func calculateVerifierChecksum(group uuid.UUID, salt []byte, hash []byte) ([]byte, error) {
228 | uuidBytes, err := group.MarshalBinary()
229 | if err != nil {
230 | return nil, fmt.Errorf("Failed to marshal UUID to bytes: %w", err)
231 | }
232 |
233 | sumInput := append(uuidBytes, salt...)
234 | sumInput = append(sumInput, hash...)
235 |
236 | sum, err := calculateChecksum(sumInput)
237 | if err != nil {
238 | return nil, fmt.Errorf("Failed to calculate verifier checksum: %w", err)
239 | }
240 |
241 | return sum, nil
242 | }
243 |
244 | func calculateKeyChecksum(group uuid.UUID, secret []byte) ([]byte, error) {
245 | uuidBytes, err := group.MarshalBinary()
246 | if err != nil {
247 | return nil, fmt.Errorf("Failed to marshal UUID to bytes: %w", err)
248 | }
249 |
250 | sumInput := append(uuidBytes, secret...)
251 |
252 | sum, err := calculateChecksum(sumInput)
253 | if err != nil {
254 | return nil, fmt.Errorf("Failed to calculate key checksum: %w", err)
255 | }
256 |
257 | return sum, nil
258 | }
259 |
260 | func isKeyChecksumValid(key Key) bool {
261 |
262 | calculatedChecksum, err := calculateKeyChecksum(key.Group, key.Secret)
263 | if err != nil {
264 | log.Warn().Err(err).Msg("Key checksum validation failed")
265 | return false
266 | }
267 |
268 | return bytes.Equal(calculatedChecksum, key.Checksum)
269 | }
270 |
271 | // NewRandomBytes generates a byte array full of cryptographic strength random data
272 | func NewRandomBytes(length int) ([]byte, error) {
273 | retVal := make([]byte, length)
274 | numBytes, err := rand.Read(retVal)
275 |
276 | if err != nil {
277 | return nil, fmt.Errorf("Random byte generation failed: %w", err)
278 | }
279 | if numBytes != length {
280 | return nil, fmt.Errorf("Failed to generate enough random bytes: %w", err)
281 | }
282 |
283 | return retVal, nil
284 | }
285 |
--------------------------------------------------------------------------------
/common/auth/auth_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package auth
21 |
22 | import (
23 | "bytes"
24 |
25 | "encoding/json"
26 | "testing"
27 |
28 | "github.com/google/uuid"
29 | "github.com/xeipuuv/gojsonschema"
30 | )
31 |
32 | func TestKeySerialization(t *testing.T) {
33 | jsonKey := `{"group":"73d0710f-c4a4-468a-9087-a06073bebe8c","secret":"4QIuEulkopN+QbyMXOMVCKZBps0JjutFpI7U1OI6FgUFY6587Be9xLRPUSOK7fajM+RokZSwv3F31t5XAbTXnQ==","checksum":"iNg+ovQngoycxTRsb3byxjuE26E5eUQChVmmh54e2To="}`
34 | jsonKeyBytes := []byte(jsonKey)
35 |
36 | schemaLoader := gojsonschema.NewStringLoader(KeyJSONSchema)
37 | schema, err := gojsonschema.NewSchema(schemaLoader)
38 | if err != nil {
39 | t.Fatal("Failed to load key JSON schema.", err)
40 | }
41 |
42 | documentLoader := gojsonschema.NewBytesLoader(jsonKeyBytes)
43 | result, err := schema.Validate(documentLoader)
44 | if err != nil {
45 | t.Fatal("Failed to validate against key JSON schema.", err)
46 | }
47 |
48 | if !result.Valid() {
49 | t.Fatal("Key JSON schema validation returned not valid.")
50 | }
51 |
52 | var key Key
53 |
54 | err = json.Unmarshal(jsonKeyBytes, &key)
55 | if err != nil {
56 | t.Fatal("Failed to deserialize key.", err)
57 | }
58 |
59 | serializedBytes, err := json.Marshal(key)
60 | if err != nil {
61 | t.Fatal("Failed to serialize key.", err)
62 | }
63 |
64 | if !bytes.Equal(jsonKeyBytes, serializedBytes) {
65 | t.Fatal("Processed key not equal to original.", jsonKey, string(serializedBytes))
66 | }
67 | }
68 |
69 | func TestVerifierSerialization(t *testing.T) {
70 | jsonVerifier := `{"group":"73d0710f-c4a4-468a-9087-a06073bebe8c","salt":"SGQ7Fw==","hash":"BioOJy7op91gsmlO+L9PMAsRrUWNKP5JJQZ6JwCjlk4=","checksum":"jDmzy23i1Db7on0hTLZTsnATlGg79QOwwHZprlmSJ18="}`
71 | jsonVerifierBytes := []byte(jsonVerifier)
72 |
73 | schemaLoader := gojsonschema.NewStringLoader(VerifierJSONSchema)
74 | schema, err := gojsonschema.NewSchema(schemaLoader)
75 | if err != nil {
76 | t.Fatal("Failed to load verifier JSON schema.", err)
77 | }
78 |
79 | documentLoader := gojsonschema.NewBytesLoader(jsonVerifierBytes)
80 | result, err := schema.Validate(documentLoader)
81 | if err != nil {
82 | t.Fatal("Failed to validate against verifier JSON schema.", err)
83 | }
84 |
85 | if !result.Valid() {
86 | t.Fatal("Verifier JSON schema validation returned not valid.")
87 | }
88 |
89 | var verifier Verifier
90 |
91 | err = json.Unmarshal(jsonVerifierBytes, &verifier)
92 | if err != nil {
93 | t.Fatal("Failed to deserialize verifier.", err)
94 | }
95 |
96 | serializedBytes, err := json.Marshal(verifier)
97 | if err != nil {
98 | t.Fatal("Failed to serialize verifier.", err)
99 | }
100 |
101 | if !bytes.Equal(jsonVerifierBytes, serializedBytes) {
102 | t.Fatal("Processed verifier not equal to original.", jsonVerifier, string(serializedBytes))
103 | }
104 | }
105 |
106 | func TestKnownKeyVerification(t *testing.T) {
107 | jsonKey := `{"group":"73d0710f-c4a4-468a-9087-a06073bebe8c","secret":"4QIuEulkopN+QbyMXOMVCKZBps0JjutFpI7U1OI6FgUFY6587Be9xLRPUSOK7fajM+RokZSwv3F31t5XAbTXnQ==","checksum":"iNg+ovQngoycxTRsb3byxjuE26E5eUQChVmmh54e2To="}`
108 | jsonKeyBytes := []byte(jsonKey)
109 |
110 | var key Key
111 |
112 | err := json.Unmarshal(jsonKeyBytes, &key)
113 | if err != nil {
114 | t.Fatal("Failed to deserialize key.", err)
115 | }
116 |
117 | jsonVerifier := `{"group":"73d0710f-c4a4-468a-9087-a06073bebe8c","salt":"SGQ7Fw==","hash":"BioOJy7op91gsmlO+L9PMAsRrUWNKP5JJQZ6JwCjlk4=","checksum":"jDmzy23i1Db7on0hTLZTsnATlGg79QOwwHZprlmSJ18="}`
118 | jsonVerifierBytes := []byte(jsonVerifier)
119 |
120 | var verifier Verifier
121 |
122 | err = json.Unmarshal(jsonVerifierBytes, &verifier)
123 | if err != nil {
124 | t.Fatal("Failed to deserialize verifier.", err)
125 | }
126 |
127 | if !Verify(key, verifier) {
128 | t.Fatal("Install verification failed.")
129 | }
130 | }
131 |
132 | func TestNewKeyVerification(t *testing.T) {
133 | groupID := uuid.New()
134 |
135 | key, err := NewKey(groupID)
136 | if err != nil {
137 | t.Fatal("Failed to create new key.", err)
138 | }
139 |
140 | verifier, err := NewVerifier(key)
141 | if err != nil {
142 | t.Fatal("Failed to create new verifier.", err)
143 | }
144 |
145 | if !Verify(key, verifier) {
146 | t.Fatal("Install verification failed.")
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/example_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "sensor": {
3 | "api": {
4 | "host": "localhost",
5 | "port": 8080
6 | },
7 | "token": {
8 | "duration": 2419200000000,
9 | "secrets": {
10 | "current": "jZAcBYO3EussMYp/GtfAx+luMZd17BQX3lgLxCGTLBY47pR2glLuC4XdUlIe70O19Uu4Yy5zqzH7aIbVQbl7cw==",
11 | "past": [
12 | "sQBc60569Z4mcf0Idf6OIu+u+Ht+vjz9rqQDx7/mdHUKoMWcFpvfcHRA3Ta5yB1HlHsIRkRajLtdLFmRgpwtiQ==",
13 | "zAVlcn/qje8jAjL6HWyMZrKiKuYgLIWfVn4uHZA2KaPLbQWCII9wymJe+WPmAph2Qbau5pYDPXAzjVfYo2pU6Q=="
14 | ]
15 | }
16 | },
17 | "install": {
18 | "verifiers": [
19 | {
20 | "group": "68886d61-572b-41a5-8edd-93a564fb5ba3",
21 | "salt": "oWrccA==",
22 | "hash": "bAW4dciXby0vAAhqHDebxsLioy6H3eYzGqvL8CMRhh0=",
23 | "checksum": "fhpiskuymKjvsyRKJ/U9ohdKRDpgft4x/Exn/zHM1zs="
24 | }
25 | ]
26 | }
27 | },
28 | "outputs": [
29 | {
30 | "type": "stdout"
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/example_config.yaml:
--------------------------------------------------------------------------------
1 | sensor:
2 | api:
3 | # host that the sensor api will bind to
4 | host: localhost
5 | # port that the sensor api will bind to
6 | port: 8080
7 | # uncomment this section to enable https with example self signed crypt material
8 | # https:
9 | # certificate: server/test-crypt/example_cert.pem
10 | # key: server/test-crypt/example_key_encrypted.pem
11 | # password: examplekeypassword
12 | token:
13 | # How long issued tokens will be valid for in microseconds (28 days here)
14 | duration: 2419200000000
15 | # token signing secrets. generate them with "weaklayer-gateway secret"
16 | secrets:
17 | # REQUIRED: the current secret is used to issue and verify tokens
18 | current: jZAcBYO3EussMYp/GtfAx+luMZd17BQX3lgLxCGTLBY47pR2glLuC4XdUlIe70O19Uu4Yy5zqzH7aIbVQbl7cw==
19 | # past secrets are not used for issuing tokens but the server will accept tokens created with them
20 | past:
21 | - sQBc60569Z4mcf0Idf6OIu+u+Ht+vjz9rqQDx7/mdHUKoMWcFpvfcHRA3Ta5yB1HlHsIRkRajLtdLFmRgpwtiQ==
22 | - zAVlcn/qje8jAjL6HWyMZrKiKuYgLIWfVn4uHZA2KaPLbQWCII9wymJe+WPmAph2Qbau5pYDPXAzjVfYo2pU6Q==
23 | install:
24 | # verfifiers are used to verify the keys that sensors provide on installation requests
25 | # you can specify more than one verifier for a given group
26 | verifiers:
27 | -
28 | group: 68886d61-572b-41a5-8edd-93a564fb5ba3
29 | salt: oWrccA==
30 | hash: bAW4dciXby0vAAhqHDebxsLioy6H3eYzGqvL8CMRhh0=
31 | checksum: fhpiskuymKjvsyRKJ/U9ohdKRDpgft4x/Exn/zHM1zs=
32 | # Destinations for the gateway to write events to
33 | # At least one output must be present
34 | outputs:
35 | - type: stdout
36 | #- type: filesystem
37 | # the top directory where the gateway will write events to the filesystem
38 | # directory: ./weaklayer-events
39 | # the time, in microseconds, that a file will be closed after
40 | # age: 300000000
41 | # the size, in bytes, that a file will be closed after
42 | # size: 100000000
43 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/weaklayer/gateway
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible
7 | github.com/google/uuid v1.1.2
8 | github.com/rs/zerolog v1.20.0
9 | github.com/spf13/cobra v1.0.0
10 | github.com/spf13/viper v1.7.1
11 | github.com/xeipuuv/gojsonschema v1.2.0
12 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
13 | )
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
14 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
15 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
16 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
17 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
18 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
19 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
20 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
21 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
22 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
23 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
24 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
25 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
26 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
27 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
28 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
29 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
30 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
31 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
32 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
33 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
34 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
35 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
36 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
37 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
38 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
39 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
40 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
41 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
42 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
43 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
44 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
45 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
46 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
47 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
48 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
49 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
50 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
51 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
52 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
53 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
54 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
55 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
56 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
57 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
58 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
59 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
60 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
61 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
62 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
63 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
64 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
65 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
66 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
67 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
68 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
69 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
70 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
71 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
72 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
73 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
74 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
75 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
76 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
77 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
78 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
79 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
80 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
81 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
82 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
83 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
84 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
85 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
86 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
87 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
88 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
89 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
90 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
91 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
92 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
93 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
94 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
95 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
96 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
97 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
98 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
99 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
100 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
101 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
102 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
103 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
104 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
105 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
106 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
107 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
108 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
109 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
110 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
111 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
112 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
113 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
114 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
115 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
116 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
117 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
118 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
119 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
120 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
121 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
122 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
123 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
124 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
125 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
126 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
127 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
128 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
129 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
130 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
131 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
132 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
133 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
134 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
135 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
136 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
137 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
138 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
139 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
140 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
141 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
142 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
143 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
144 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
145 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
146 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
147 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
148 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
149 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
150 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
151 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
152 | github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
153 | github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
154 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
155 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
156 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
157 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
158 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
159 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
160 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
161 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
162 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
163 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
164 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
165 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
166 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
167 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
168 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
169 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
170 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
171 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
172 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
173 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
174 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
175 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
176 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
177 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
178 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
179 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
180 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
181 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
182 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
183 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
184 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
185 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
186 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
187 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
188 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
189 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
190 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
191 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
192 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
193 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
194 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
195 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
196 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
197 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
198 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
199 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
200 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
201 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
202 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
203 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
204 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
205 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
206 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
207 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
208 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
209 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
210 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
211 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
212 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
213 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
214 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
215 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
216 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
217 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
218 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
219 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
220 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
221 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
222 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
223 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
224 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
225 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
226 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
227 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
228 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
229 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
230 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
231 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
232 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
233 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
234 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
235 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
236 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
237 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
238 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
239 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
240 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
241 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
242 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
243 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
244 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
245 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
246 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
247 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
248 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
249 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
250 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
251 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
252 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
253 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
254 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
255 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
256 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
257 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
258 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
259 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
260 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
261 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
262 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
263 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
264 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
265 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
266 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
267 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
268 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
269 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
270 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
271 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
272 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
273 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
274 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
275 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
276 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
277 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
278 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
279 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
280 | golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
281 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
282 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
283 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
284 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
285 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
286 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
287 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
288 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
289 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
290 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
291 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
292 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
293 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
294 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
295 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
296 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
297 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
298 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
299 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
300 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
301 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
302 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
303 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
304 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
305 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
306 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
307 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
308 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
309 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
310 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
311 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
312 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
313 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
314 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
315 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
316 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
317 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
318 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
319 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
320 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
321 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
322 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
323 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
324 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package main
21 |
22 | import (
23 | "os"
24 |
25 | "github.com/rs/zerolog"
26 |
27 | "github.com/weaklayer/gateway/cmd"
28 | )
29 |
30 | func main() {
31 | // zerolog is only really used in the server command
32 | // however, it is present in the common modules
33 | // Unix micros is what we use across weaklayer
34 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMicro
35 |
36 | err := cmd.Execute()
37 | if err != nil {
38 | // cobra already prints the error
39 | os.Exit(1)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/package.sh:
--------------------------------------------------------------------------------
1 | # !/bin/sh
2 |
3 | set -e
4 |
5 | go test ./... > /dev/null
6 |
7 | rm -rf dist
8 | rm -rf .licenses
9 |
10 | mkdir -p dist
11 |
12 | licensed cache
13 | licensed status
14 | licensed notices
15 |
16 | cp .licenses/NOTICE dist/
17 | cp LICENSE dist/
18 | cp example_config.yaml dist/
19 | cp example_config.json dist/
20 |
21 | GOOS=linux GOARCH=amd64 go build -o dist/weaklayer-gateway-linux-amd64
22 | GOOS=windows GOARCH=amd64 go build -o dist/weaklayer-gateway-windows-amd64.exe
23 | GOOS=darwin GOARCH=amd64 go build -o dist/weaklayer-gateway-macOS-amd64
24 |
25 | cd dist
26 | zip -r weaklayer-gateway-binary-release.zip ./
27 |
--------------------------------------------------------------------------------
/server/api/events.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package api
21 |
22 | import (
23 | "compress/gzip"
24 | "encoding/json"
25 | "net/http"
26 | "strings"
27 |
28 | "github.com/rs/zerolog/log"
29 |
30 | "github.com/weaklayer/gateway/server/events"
31 | "github.com/weaklayer/gateway/server/output"
32 | "github.com/weaklayer/gateway/server/token"
33 | )
34 |
35 | // EventsAPI handles requests to the /events path
36 | type EventsAPI struct {
37 | tokenProcessor token.Processor
38 | eventOutput output.Output
39 | }
40 |
41 | // NewEventsAPI provisions an events API with its required resources
42 | func NewEventsAPI(tokenProcessor token.Processor, eventOutput output.Output) (EventsAPI, error) {
43 | return EventsAPI{
44 | tokenProcessor: tokenProcessor,
45 | eventOutput: eventOutput,
46 | }, nil
47 | }
48 |
49 | // Handle does nothing right now
50 | func (eventsAPI EventsAPI) Handle(responseWriter http.ResponseWriter, request *http.Request) {
51 |
52 | encodingHeader := request.Header.Get("Content-Encoding")
53 | if encodingHeader != "gzip" {
54 | responseWriter.WriteHeader(http.StatusUnsupportedMediaType)
55 | return
56 | }
57 |
58 | // Authenticate the request
59 | var authToken string
60 | authHeader := request.Header.Get("Authorization")
61 | if authHeader != "" {
62 | if strings.HasPrefix(authHeader, "Bearer ") {
63 | authToken = strings.TrimPrefix(authHeader, "Bearer ")
64 | } else {
65 | log.Info().Msg("Provided Authorization header does not match Bearer schema")
66 | responseWriter.WriteHeader(http.StatusBadRequest)
67 | return
68 | }
69 | } else {
70 | log.Info().Msg("No Authorization header provided")
71 | responseWriter.WriteHeader(http.StatusBadRequest)
72 | return
73 | }
74 |
75 | isTokenValid, claims := eventsAPI.tokenProcessor.VerifyToken(authToken)
76 | if !isTokenValid {
77 | log.Info().Msg("Invalid token provided")
78 | responseWriter.WriteHeader(http.StatusUnauthorized)
79 | return
80 | }
81 |
82 | // These are the outputs from authentication
83 | sensor := claims.Sensor
84 | group := claims.Group
85 |
86 | body := request.Body
87 | defer func() {
88 | err := body.Close()
89 | if err != nil {
90 | log.Info().Err(err).Msg("Failed to close request body")
91 | }
92 | }()
93 |
94 | gzipReader, err := gzip.NewReader(request.Body)
95 | if err != nil {
96 | log.Info().Err(err).Msg("Failed to parse gzip content")
97 | responseWriter.WriteHeader(http.StatusBadRequest)
98 | return
99 | }
100 | defer func() {
101 | err := gzipReader.Close()
102 | if err != nil {
103 | log.Info().Err(err).Msg("Failed to close gzip content reader")
104 | }
105 | }()
106 |
107 | // Start parsing the request body
108 | // The request body is expected to be a (potentially large) JSON array of events
109 | // Different event types can be mixed in the array
110 | decoder := json.NewDecoder(gzipReader)
111 |
112 | openingToken, err := decoder.Token()
113 | if err != nil {
114 | log.Info().Err(err).Msg("Could not parse request body as gzip compressed json")
115 | responseWriter.WriteHeader(http.StatusBadRequest)
116 | return
117 | }
118 |
119 | delimiter, ok := openingToken.(json.Delim)
120 | if !ok || delimiter != '[' {
121 | log.Info().Msg("Request body is not a JSON array")
122 | responseWriter.WriteHeader(http.StatusBadRequest)
123 | return
124 | }
125 |
126 | var parsedEvents []events.SensorEvent
127 |
128 | for decoder.More() {
129 |
130 | // get the bytes for the next event
131 | var eventData json.RawMessage
132 | err := decoder.Decode(&eventData)
133 | if err != nil {
134 | log.Info().Err(err).Msg("Could not parse request body JSON entry")
135 | responseWriter.WriteHeader(http.StatusBadRequest)
136 | return
137 | }
138 |
139 | // parse the bytes as a sensor event
140 | event, err := events.ParseEvent(eventData, sensor, group)
141 | if err != nil {
142 | log.Info().Err(err).Msg("Could not parse JSON entry as sensor event")
143 | responseWriter.WriteHeader(http.StatusBadRequest)
144 | return
145 | }
146 |
147 | parsedEvents = append(parsedEvents, event)
148 | }
149 |
150 | err = eventsAPI.eventOutput.Consume(parsedEvents)
151 | if err != nil {
152 | log.Info().Err(err).Msg("Event processing failed")
153 | responseWriter.WriteHeader(http.StatusInternalServerError)
154 | return
155 | }
156 |
157 | return
158 | }
159 |
--------------------------------------------------------------------------------
/server/api/install.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package api
21 |
22 | import (
23 | "encoding/json"
24 | "fmt"
25 | "io/ioutil"
26 | "net/http"
27 | "strings"
28 | "time"
29 |
30 | "github.com/google/uuid"
31 | "github.com/rs/zerolog/log"
32 |
33 | "github.com/weaklayer/gateway/common/auth"
34 | "github.com/weaklayer/gateway/server/events"
35 | "github.com/weaklayer/gateway/server/output"
36 | "github.com/weaklayer/gateway/server/token"
37 | "github.com/xeipuuv/gojsonschema"
38 | )
39 |
40 | // InstallResponse forms the response body that the sensor will receive on a successful install request
41 | type InstallResponse struct {
42 | Token string `json:"token"`
43 | Sensor uuid.UUID `json:"sensor"`
44 | ExpiresAt int64 `json:"expiresAt"`
45 | IssuedAt int64 `json:"issuedAt"`
46 | }
47 |
48 | // InstallRequest is what the sensor sends in the HTTP body to request installation
49 | type InstallRequest struct {
50 | Key auth.Key `json:"key"`
51 | Label string `json:"label"`
52 | }
53 |
54 | var installRequestJSONSchema = fmt.Sprintf(`
55 | {
56 | "$schema": "http://json-schema.org/draft-07/schema#",
57 | "title": "InstallRequest",
58 | "type": "object",
59 | "properties": {
60 | "label": {
61 | "type": "string"
62 | },
63 | "key": %s
64 | }
65 | }
66 | `, auth.KeyJSONSchema)
67 |
68 | // InstallAPI handles requests to the /install path for sensor installation
69 | type InstallAPI struct {
70 | tokenProcessor token.Processor
71 | installRequestSchema *gojsonschema.Schema
72 | eventOutput output.Output
73 | verifiers []auth.Verifier
74 | }
75 |
76 | // NewInstallAPI provisions a sensor API with its required resources
77 | func NewInstallAPI(tokenProcessor token.Processor, eventOutput output.Output, verifiers []auth.Verifier) (InstallAPI, error) {
78 | var installAPI InstallAPI
79 |
80 | schemaLoader := gojsonschema.NewStringLoader(installRequestJSONSchema)
81 | schemaVerifier, err := gojsonschema.NewSchema(schemaLoader)
82 | if err != nil {
83 | return installAPI, fmt.Errorf("Failed to load install request JSON schema: %w", err)
84 | }
85 |
86 | return InstallAPI{
87 | tokenProcessor: tokenProcessor,
88 | installRequestSchema: schemaVerifier,
89 | eventOutput: eventOutput,
90 | verifiers: verifiers,
91 | }, nil
92 | }
93 |
94 | func (installAPI InstallAPI) parseInstallRequest(data []byte) (InstallRequest, error) {
95 | var installRequest InstallRequest
96 |
97 | documentLoader := gojsonschema.NewBytesLoader(data)
98 | result, err := installAPI.installRequestSchema.Validate(documentLoader)
99 | if err != nil {
100 | return installRequest, fmt.Errorf("Failed to validate install request against json schema: %w", err)
101 | }
102 |
103 | if !result.Valid() {
104 | return installRequest, fmt.Errorf("Install request did not match json schema")
105 | }
106 |
107 | err = json.Unmarshal(data, &installRequest)
108 | if err != nil {
109 | return installRequest, fmt.Errorf("Failed to unmarshal install request: %w", err)
110 | }
111 |
112 | if !installAPI.isInstallRequestValid(installRequest) {
113 | return installRequest, fmt.Errorf("Install request verification unsuccessful")
114 | }
115 |
116 | return installRequest, nil
117 | }
118 |
119 | // Handle validates and processes install requests
120 | func (installAPI InstallAPI) Handle(responseWriter http.ResponseWriter, request *http.Request) {
121 |
122 | // Don't want any responses cached
123 | responseWriter.Header().Add("Cache-Control", "no-store")
124 | responseWriter.Header().Add("Pragma", "no-cache")
125 |
126 | bodyContents, err := ioutil.ReadAll(request.Body)
127 | if err != nil {
128 | log.Warn().Err(err).Msg("Failed to read request body contents")
129 | responseWriter.WriteHeader(http.StatusInternalServerError)
130 | return
131 | }
132 |
133 | installRequest, err := installAPI.parseInstallRequest(bodyContents)
134 | if err != nil {
135 | log.Info().Err(err).Msg("Failed to parse install request")
136 | responseWriter.WriteHeader(http.StatusBadRequest)
137 | return
138 | }
139 |
140 | var tokenProvided = false
141 | var providedToken string
142 | authHeader := request.Header.Get("Authorization")
143 | if authHeader != "" {
144 | if strings.HasPrefix(authHeader, "Bearer ") {
145 | providedToken = strings.TrimPrefix(authHeader, "Bearer ")
146 | tokenProvided = true
147 | } else {
148 | log.Info().Msg("Provided Authorization header does not match Bearer schema")
149 | responseWriter.WriteHeader(http.StatusBadRequest)
150 | return
151 | }
152 | }
153 |
154 | isInstallationRenewal := false
155 | group := installRequest.Key.Group
156 | var sensor uuid.UUID
157 |
158 | if tokenProvided {
159 | isTokenValid, claims := installAPI.tokenProcessor.VerifyToken(providedToken)
160 | if isTokenValid {
161 | if auth.UUIDEquals(installRequest.Key.Group, claims.Group) {
162 | sensor = claims.Sensor
163 | isInstallationRenewal = true
164 | } else {
165 | log.Info().Msgf("Token group %s differs from the install key group %s. Proceeding as new install.", claims.Group.String(), installRequest.Key.Group.String())
166 | }
167 | } else {
168 | log.Info().Msg("Received an invalid JWT for install renewal. Proceeding as new install.")
169 | }
170 | }
171 |
172 | if !isInstallationRenewal {
173 | sensor, err = uuid.NewRandom()
174 | if err != nil {
175 | log.Warn().Err(err).Msg("Failed to generate new sensor identifier")
176 | responseWriter.WriteHeader(http.StatusInternalServerError)
177 | return
178 | }
179 | }
180 |
181 | token, expiresAt, issuedAt, err := installAPI.tokenProcessor.NewToken(group, sensor)
182 | if err != nil {
183 | log.Warn().Err(err).Msg("Failed to create new sensor token")
184 | responseWriter.WriteHeader(http.StatusInternalServerError)
185 | return
186 | }
187 |
188 | // The installation was successful
189 | // We need to generate an event for this before responding
190 | dataMap := make(map[string]interface{})
191 | dataMap["label"] = installRequest.Label
192 | dataMap["isNewInstall"] = !isInstallationRenewal
193 | dataMap["userAgent"] = request.Header.Get("User-Agent")
194 | installEvent := events.SensorEvent{
195 | Type: "Install",
196 | Time: time.Now().UnixNano() / 1000,
197 | Sensor: sensor,
198 | Group: group,
199 | Data: dataMap,
200 | }
201 |
202 | installAPI.eventOutput.Consume([]events.SensorEvent{installEvent})
203 |
204 | response := InstallResponse{
205 | Token: token,
206 | Sensor: sensor,
207 | ExpiresAt: expiresAt * 1000000, // expiresAt is in seconds. convert to micros to match weaklayer convention
208 | IssuedAt: issuedAt * 1000000,
209 | }
210 |
211 | responseBytes, err := json.Marshal(response)
212 | if err != nil {
213 | log.Warn().Err(err).Msg("Failed to marshal response body")
214 | responseWriter.WriteHeader(http.StatusInternalServerError)
215 | return
216 | }
217 |
218 | // responseWriter.Write sets Content-Length and the status to 200
219 | // Therefore don't try to se the status on error
220 | responseWriter.Header().Add("Content-Type", "application/json")
221 | bytesWritten, err := responseWriter.Write(responseBytes)
222 | if err != nil {
223 | log.Warn().Err(err).Msg("Failed to write response body")
224 | }
225 |
226 | if bytesWritten != len(responseBytes) {
227 | log.Warn().Msgf("Failed to write entire response body. Wrote %d bytes out of %d total.", bytesWritten, len(responseBytes))
228 | }
229 |
230 | return
231 | }
232 |
233 | func (installAPI InstallAPI) isInstallRequestValid(installRequest InstallRequest) bool {
234 |
235 | // TODO: put the verifiers into a map keyed by groupid and then only go through the verifies for the given group
236 |
237 | for _, verifier := range installAPI.verifiers {
238 | if auth.UUIDEquals(verifier.Group, installRequest.Key.Group) {
239 | if auth.Verify(installRequest.Key, verifier) {
240 | return true
241 | }
242 | }
243 | }
244 |
245 | return false
246 | }
247 |
--------------------------------------------------------------------------------
/server/api/sensor.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package api
21 |
22 | import (
23 | "net/http"
24 | )
25 |
26 | var lincensePaths = map[string]struct{}{"": {}, "/": {}, "/index": {}, "/index/": {}, "/index.html": {}, "/license": {}, "/license/": {}, "/license.html": {}}
27 |
28 | // SensorAPI is the root HTTP Handler for the sensor API
29 | type SensorAPI struct {
30 | InstallHandler InstallAPI
31 | EventsHandler EventsAPI
32 | }
33 |
34 | func (sensorAPI SensorAPI) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
35 |
36 | if request.Method == http.MethodPost {
37 |
38 | // This api only accepts json
39 | if request.Header.Get("Content-type") != "application/json" {
40 | responseWriter.WriteHeader(http.StatusUnsupportedMediaType)
41 | return
42 | }
43 |
44 | // TODO: Other generic validations. E.g. request size
45 |
46 | switch request.URL.Path {
47 | case "/events":
48 | sensorAPI.EventsHandler.Handle(responseWriter, request)
49 | case "/install":
50 | sensorAPI.InstallHandler.Handle(responseWriter, request)
51 | default:
52 | responseWriter.WriteHeader(http.StatusNotFound)
53 | }
54 |
55 | } else if request.Method == http.MethodGet {
56 | if _, ok := lincensePaths[request.URL.Path]; ok {
57 | displayLicense(responseWriter, request)
58 | } else {
59 | responseWriter.WriteHeader(http.StatusNotFound)
60 | }
61 | } else {
62 | responseWriter.WriteHeader(http.StatusMethodNotAllowed)
63 | }
64 |
65 | }
66 |
67 | func displayLicense(responseWriter http.ResponseWriter, request *http.Request) {
68 | licenseInfo := []byte(`
69 |
70 |
71 |
72 |
73 | Weaklayer Gateway
74 |
75 |
76 |
77 | This is Weaklayer Gateway.
78 |
79 |
80 |
81 | Weaklayer Gateway is free software. It is available under the terms of the GNU Affero General Public License (GNU AGPL). Please see the program source for the exact GNU AGPL version.
82 |
83 |
84 |
85 | The Weaklayer Gateway source is available at
86 | https://github.com/weaklayer/gateway
87 |
88 |
89 |
90 | The Weaklayer Sensor source is available at
91 | https://github.com/weaklayer/sensor
92 |
93 |
94 |
95 | For more information, please see
96 | https://weaklayer.com
97 |
98 |
99 |
100 |
101 | `)
102 | responseWriter.Header().Set("Content-type", "text/html")
103 | responseWriter.Write(licenseInfo)
104 | }
105 |
--------------------------------------------------------------------------------
/server/api/sensor_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package api
21 |
22 | import (
23 | "bytes"
24 | "compress/gzip"
25 | "encoding/json"
26 | "net/http"
27 | "net/http/httptest"
28 | "strings"
29 | "testing"
30 |
31 | "github.com/google/uuid"
32 | "github.com/weaklayer/gateway/common/auth"
33 | "github.com/weaklayer/gateway/server/output"
34 | "github.com/weaklayer/gateway/server/token"
35 | )
36 |
37 | func TestInstallAndEvents(t *testing.T) {
38 |
39 | // Construct the api
40 | tokenSecret := []byte("seeeeeeecret")
41 | pastSecrets := make([][]byte, 2)
42 | pastSecrets[0] = []byte("seeeeeeecret1")
43 | pastSecrets[1] = []byte("seeeeeeecret2")
44 |
45 | var tokenDuration int64 = 10000
46 | tokenProcessor := token.NewProcessor(tokenSecret, pastSecrets, tokenDuration)
47 |
48 | group, err := uuid.NewRandom()
49 | if err != nil {
50 | t.Fatalf("Failed to create test group UUID: %v", err)
51 | }
52 |
53 | key, err := auth.NewKey(group)
54 | if err != nil {
55 | t.Fatalf("Failed to create test Key: %v", err)
56 | }
57 |
58 | verifier, err := auth.NewVerifier(key)
59 | if err != nil {
60 | t.Fatalf("Failed to create test Verifier: %v", err)
61 | }
62 |
63 | eventOutput := output.NewTopOutput([]output.Output{})
64 |
65 | installAPI, err := NewInstallAPI(tokenProcessor, eventOutput, []auth.Verifier{verifier})
66 | if err != nil {
67 | t.Fatalf("Falied to create install API endpoint: %v", err)
68 | }
69 |
70 | eventsAPI, err := NewEventsAPI(tokenProcessor, eventOutput)
71 | if err != nil {
72 | t.Fatalf("Falied to create events API endpoint: %v", err)
73 | }
74 |
75 | sensorAPI := SensorAPI{
76 | EventsHandler: eventsAPI,
77 | InstallHandler: installAPI,
78 | }
79 |
80 | // Now create and issue request to the api
81 | installRequest := InstallRequest{
82 | Key: key,
83 | Label: "Test sensor!",
84 | }
85 |
86 | bodyBytes, err := json.Marshal(installRequest)
87 | if err != nil {
88 | t.Fatalf("Failed to create request body: %v", err)
89 | }
90 |
91 | request, err := http.NewRequest("POST", "/install", bytes.NewReader(bodyBytes))
92 | if err != nil {
93 | t.Fatalf("Failed to create request: %v", err)
94 | }
95 |
96 | request.Header.Add("Content-type", "application/json")
97 |
98 | responseRecorder := httptest.NewRecorder()
99 | handler := http.HandlerFunc(sensorAPI.ServeHTTP)
100 | handler.ServeHTTP(responseRecorder, request)
101 |
102 | if responseRecorder.Code != http.StatusOK {
103 | t.Fatalf("Install request failed with status code %d", responseRecorder.Code)
104 | }
105 |
106 | if responseRecorder.HeaderMap.Get("Cache-Control") != "no-store" {
107 | t.Fatalf("Incorrect Cache-Control header value '%s'", responseRecorder.HeaderMap.Get("Cache-Control"))
108 | }
109 | if responseRecorder.HeaderMap.Get("Pragma") != "no-cache" {
110 | t.Fatalf("Incorrect Pragma header value '%s'", responseRecorder.HeaderMap.Get("Pragma"))
111 | }
112 |
113 | // First request succeeded. Now try renewing the token with the Authorization header set.
114 |
115 | var installResponse InstallResponse
116 |
117 | err = json.Unmarshal(responseRecorder.Body.Bytes(), &installResponse)
118 | if err != nil {
119 | t.Fatalf("Failed to unmarshall install response: %v", err)
120 | }
121 |
122 | request, err = http.NewRequest("POST", "/install", bytes.NewReader(bodyBytes))
123 | if err != nil {
124 | t.Fatalf("Failed to create request: %v", err)
125 | }
126 |
127 | request.Header.Add("Content-type", "application/json")
128 | request.Header.Add("Authorization", "Bearer "+installResponse.Token)
129 | responseRecorder = httptest.NewRecorder()
130 | handler.ServeHTTP(responseRecorder, request)
131 |
132 | if responseRecorder.Code != http.StatusOK {
133 | t.Fatalf("Install request failed with status code %d", responseRecorder.Code)
134 | }
135 |
136 | err = json.Unmarshal(responseRecorder.Body.Bytes(), &installResponse)
137 | if err != nil {
138 | t.Fatalf("Failed to unmarshall install response: %v", err)
139 | }
140 |
141 | // Now submit some events since you have the auth token.
142 | eventsBody := `[
143 | {"time":1, "type":"PageLoad", "protocol": "https", "hostname": "weaklayer.com", "port": 99},
144 | {"type":"PageLoad", "time":4, "protocol": "https", "hostname": "weaklayer.com", "port": 443},
145 | {"type":"PageLoad", "time":88, "protocol": "http", "hostname": "weaklayer.com", "port": 80}
146 | ]`
147 |
148 | var compressedEventsBody bytes.Buffer
149 | gz := gzip.NewWriter(&compressedEventsBody)
150 | _, err = gz.Write([]byte(eventsBody))
151 | if err != nil {
152 | t.Fatalf("Failed to create request: %v", err)
153 | }
154 | gz.Close()
155 | if err != nil {
156 | t.Fatalf("Failed to create request: %v", err)
157 | }
158 |
159 | request, err = http.NewRequest("POST", "/events", bytes.NewReader(compressedEventsBody.Bytes()))
160 | if err != nil {
161 | t.Fatalf("Failed to create request: %v", err)
162 | }
163 |
164 | request.Header.Add("Content-Type", "application/json")
165 | request.Header.Add("Content-Encoding", "gzip")
166 | request.Header.Add("Authorization", "Bearer "+installResponse.Token)
167 | responseRecorder = httptest.NewRecorder()
168 | handler.ServeHTTP(responseRecorder, request)
169 |
170 | if responseRecorder.Code != http.StatusOK {
171 | t.Fatalf("Install request failed with status code %d", responseRecorder.Code)
172 | }
173 | }
174 |
175 | func TestDisplayLicense(t *testing.T) {
176 | sensorAPI := SensorAPI{}
177 |
178 | request, err := http.NewRequest("GET", "/index", bytes.NewReader(make([]byte, 0)))
179 | if err != nil {
180 | t.Fatalf("Failed to create request: %v", err)
181 | }
182 |
183 | responseRecorder := httptest.NewRecorder()
184 | handler := http.HandlerFunc(sensorAPI.ServeHTTP)
185 | handler.ServeHTTP(responseRecorder, request)
186 |
187 | if responseRecorder.Code != http.StatusOK {
188 | t.Fatalf("License get request failed with status code %d", responseRecorder.Code)
189 | }
190 |
191 | body := string(responseRecorder.Body.Bytes())
192 | if !strings.Contains(body, "GNU") {
193 | t.Fatalf("License content did not reference the GNU")
194 | }
195 |
196 | if !strings.Contains(body, "AGPL") {
197 | t.Fatalf("License content did not reference the AGPL")
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/server/events/events_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package events
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/google/uuid"
26 | "github.com/weaklayer/gateway/common/auth"
27 | )
28 |
29 | func TestWindowLocationEvent(t *testing.T) {
30 | eventType := "WindowLocation"
31 | const validWindowLocationEvent = `{
32 | "type": "WindowLocation",
33 | "time": 45678,
34 | "protocol": "https",
35 | "hostname": "weaklayer.com",
36 | "port": 443,
37 | "path": "",
38 | "search": "",
39 | "hash": "",
40 | "windowReference": 1
41 | }`
42 |
43 | event := testValidParseEvent(t, validWindowLocationEvent)
44 | if event.Type != eventType {
45 | t.Fatalf("Parsed WindowLocation event as %s", event.Type)
46 | }
47 |
48 | if event.Time != 45678 {
49 | t.Fatalf("Event time didn't match")
50 | }
51 |
52 | if event.Data["hostname"] != "weaklayer.com" {
53 | t.Fatalf("Hostname didn't match")
54 | }
55 | }
56 |
57 | func testValidParseEvent(t *testing.T, data string) SensorEvent {
58 | sensor, err := uuid.NewRandom()
59 | if err != nil {
60 | t.Fatalf("Failed to generate UUID: %v", err)
61 | }
62 | group, err := uuid.NewRandom()
63 | if err != nil {
64 | t.Fatalf("Failed to generate UUID: %v", err)
65 | }
66 |
67 | event, err := ParseEvent([]byte(data), sensor, group)
68 | if err != nil {
69 | t.Fatalf("Failed to parse valid event: %v", err)
70 | }
71 |
72 | if !auth.UUIDEquals(sensor, event.Sensor) {
73 | t.Fatalf("Sensor UUIDs don't match")
74 | }
75 |
76 | if !auth.UUIDEquals(group, event.Group) {
77 | t.Fatalf("Group UUIDs don't match")
78 | }
79 |
80 | return event
81 | }
82 |
--------------------------------------------------------------------------------
/server/events/sensor_event.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package events
21 |
22 | import (
23 | "encoding/json"
24 | "fmt"
25 |
26 | "github.com/google/uuid"
27 | )
28 |
29 | // SensorEvent gives all the fields common to sensor events
30 | // Other fields fall into a map
31 | type SensorEvent struct {
32 | Type string `json:"type"`
33 | Time int64 `json:"time"`
34 | Sensor uuid.UUID `json:"sensor"`
35 | Group uuid.UUID `json:"group"`
36 | Data map[string]interface{} `json:"-"`
37 | }
38 |
39 | // ParseEvent parses incoming events in JSON form and inject the sensor and group ids
40 | func ParseEvent(data json.RawMessage, sensor uuid.UUID, group uuid.UUID) (SensorEvent, error) {
41 | var sensorEvent SensorEvent
42 |
43 | err := json.Unmarshal(data, &sensorEvent)
44 | if err != nil {
45 | return sensorEvent, fmt.Errorf("Could not parse request body JSON entry: %w", err)
46 | }
47 |
48 | if sensorEvent.Time <= 0 {
49 | return sensorEvent, fmt.Errorf("Invalid or unspecified time value in sensor event")
50 | }
51 |
52 | err = json.Unmarshal(data, &sensorEvent.Data)
53 | if err != nil {
54 | return sensorEvent, fmt.Errorf("Could not parse request body JSON entry: %w", err)
55 | }
56 |
57 | delete(sensorEvent.Data, "type")
58 | delete(sensorEvent.Data, "time")
59 | delete(sensorEvent.Data, "sensor")
60 | delete(sensorEvent.Data, "group")
61 |
62 | sensorEvent.Sensor = sensor
63 | sensorEvent.Group = group
64 |
65 | return sensorEvent, nil
66 | }
67 |
68 | // MarshalJSON produces the desired json serialization for sensor events
69 | func (sensorEvent SensorEvent) MarshalJSON() ([]byte, error) {
70 | dataMap := sensorEvent.Data
71 | dataMap["type"] = sensorEvent.Type
72 | dataMap["time"] = sensorEvent.Time
73 | dataMap["sensor"] = sensorEvent.Sensor
74 | dataMap["group"] = sensorEvent.Group
75 |
76 | return json.Marshal(dataMap)
77 | }
78 |
--------------------------------------------------------------------------------
/server/output/filesystem/file.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package filesystem
21 |
22 | import (
23 | "fmt"
24 | "os"
25 | "path/filepath"
26 | "strconv"
27 | "sync"
28 | "time"
29 |
30 | "github.com/rs/zerolog/log"
31 | )
32 |
33 | func newFile(groupDirectory string, maxFileSize int) (file, error) {
34 |
35 | startTimeString := strconv.FormatInt(time.Now().UnixNano()/1000, 10)
36 | filename := startTimeString + ".json"
37 |
38 | // files being written to are 'dot' files
39 | inProgressPath := filepath.Join(groupDirectory, "."+filename)
40 | fileInstance, err := os.OpenFile(inProgressPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640)
41 | if err != nil {
42 | return file{}, fmt.Errorf("Failed to open file %s: %w", inProgressPath, err)
43 | }
44 | _, err = writeToFile(fileInstance, []byte("[\n"))
45 | if err != nil {
46 | return file{}, fmt.Errorf("Failed to initialize %s with JSON array opening: %w", fileInstance.Name(), err)
47 | }
48 |
49 | content := make(chan []byte)
50 | done := make(chan struct{})
51 | fileOutput := file{
52 | content: content,
53 | doneChannel: done,
54 | doneFlag: false,
55 | closeGuard: &sync.Once{},
56 | }
57 |
58 | go process(fileInstance, groupDirectory, startTimeString, maxFileSize, content, done)
59 |
60 | return fileOutput, nil
61 | }
62 |
63 | type file struct {
64 | content chan<- []byte
65 | doneChannel <-chan struct{}
66 | doneFlag bool
67 | closeGuard *sync.Once
68 | }
69 |
70 | // Close closes the content channel to tell the file writer goroutine that no more data is coming
71 | // It is not safe to call write and close concurrently
72 | // Calling close will cause write to return false in the future
73 | func (file file) Close() {
74 | // we surround this in a sync.Once since there are a couple palces Close can be called from
75 | // this makes it okay to call multiple times
76 | file.closeGuard.Do(func() {
77 | file.doneFlag = true
78 | close(file.content)
79 | })
80 | }
81 |
82 | func writeToFile(file *os.File, content []byte) (int, error) {
83 | n, err := file.Write(content)
84 |
85 | if err != nil {
86 | return n, err
87 | }
88 | if n != len(content) {
89 | return n, fmt.Errorf("Wrote only %d out of %d bytes to %s", n, len(content), file.Name())
90 | }
91 |
92 | return n, nil
93 | }
94 |
95 | // Write queues data from writing to the file
96 | // It is not safe to call write/close multiple times concurrently
97 | // returns true if the data will be written
98 | // returns false if the file is closed and data will not be written
99 | func (file file) Write(data []byte) bool {
100 |
101 | if file.doneFlag {
102 | return false
103 | }
104 |
105 | // early exit if the file is done
106 | // prevents trying to read from file.content if it is closed
107 | select {
108 | case <-file.doneChannel:
109 | file.Close()
110 | return false
111 | default:
112 | }
113 |
114 | select {
115 | case <-file.doneChannel:
116 | file.Close()
117 | return false
118 | case file.content <- data:
119 | return true
120 | }
121 | }
122 |
123 | func process(file *os.File, groupDirectory string, startTimeString string, maxFileSize int, content <-chan []byte, doneChannel chan<- struct{}) {
124 | // Closure to indicate we are done
125 | done := false
126 | sayDone := func() {
127 | if !done {
128 | done = true
129 | close(doneChannel)
130 | }
131 | }
132 |
133 | var totalBytesWritten int = 0
134 | write := func(content []byte) error {
135 | n, err := writeToFile(file, content)
136 | totalBytesWritten = totalBytesWritten + n
137 | return err
138 | }
139 |
140 | closeFile := func() {
141 | sayDone()
142 |
143 | // Only do the newline if we wrote an event
144 | // Otherwise there is a blank line between the start array and end array
145 | endString := "]"
146 | if totalBytesWritten > 0 {
147 | endString = "\n]"
148 | }
149 |
150 | err := write([]byte(endString))
151 | if err != nil {
152 | log.Warn().Err(err).Msgf("Failed to write JSON array closure to %s", file.Name())
153 | }
154 |
155 | err = file.Close()
156 | if err != nil {
157 | log.Warn().Err(err).Msgf("Failed to properly close file %v", file.Name())
158 | }
159 |
160 | endTimeString := strconv.FormatInt(time.Now().UnixNano()/1000, 10)
161 | finalFileName := startTimeString + "-" + endTimeString + ".json"
162 | finalPath := filepath.Join(groupDirectory, finalFileName)
163 | err = os.Rename(file.Name(), finalPath)
164 | if err != nil {
165 | log.Warn().Err(err).Msgf("Failed to rename %s to %s", file.Name(), finalPath)
166 | }
167 | }
168 |
169 | defer closeFile()
170 |
171 | isFirstEvent := true
172 | for eventContent := range content {
173 | // Do one event per line. Append a comma and newline to previous event.
174 | if !isFirstEvent {
175 | err := write([]byte(",\n"))
176 | if err != nil {
177 | log.Warn().Err(err).Msgf("Error in filesystem output writing to %s", file.Name())
178 | return
179 | }
180 | }
181 | isFirstEvent = false
182 |
183 | // Write the event
184 | err := write(eventContent)
185 | if err != nil {
186 | log.Warn().Err(err).Msgf("Error in filesystem output writing to %s", file.Name())
187 | return
188 | }
189 |
190 | if totalBytesWritten >= maxFileSize {
191 | sayDone()
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/server/output/filesystem/filesystem.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package filesystem
21 |
22 | import (
23 | "fmt"
24 | "os"
25 | "path/filepath"
26 | "sync"
27 | "time"
28 |
29 | "github.com/google/uuid"
30 | "github.com/weaklayer/gateway/server/events"
31 | )
32 |
33 | // NewFilesystemOutput creates a FilesystemOutput instance
34 | func NewFilesystemOutput(directory string, maxFileAge time.Duration, maxFileSize int) (FilesystemOutput, error) {
35 |
36 | // Attempt to create the directory if it does not exist.
37 | err := createDirectory(directory)
38 | if err != nil {
39 | return FilesystemOutput{}, fmt.Errorf("Failed to create directory %s for filesystem output: %w", directory, err)
40 | }
41 |
42 | filesystemOutput := FilesystemOutput{
43 | directory: directory,
44 | metaFiles: make(map[uuid.UUID]metaFile),
45 | maxFileAge: maxFileAge,
46 | maxFileSize: maxFileSize,
47 | metaFileCreationMutex: &sync.Mutex{},
48 | }
49 |
50 | return filesystemOutput, nil
51 | }
52 |
53 | // FilesystemOutput is an event output that writes events to the filesystem
54 | type FilesystemOutput struct {
55 | directory string
56 | metaFiles map[uuid.UUID]metaFile
57 | maxFileAge time.Duration
58 | maxFileSize int
59 | metaFileCreationMutex *sync.Mutex
60 | }
61 |
62 | // Close closes are underlying file descriptors for the FilesystemOutput
63 | // Close should only be called once after Consume is guaranteed not to be called again
64 | func (filesystemOutput FilesystemOutput) Close() {
65 |
66 | metaFiles := filesystemOutput.metaFiles
67 | filesystemOutput.metaFiles = make(map[uuid.UUID]metaFile)
68 |
69 | for _, metaFile := range metaFiles {
70 | metaFile.Close()
71 | }
72 | }
73 |
74 | // Consume takes the events and writes them to a channel for processing
75 | func (filesystemOutput FilesystemOutput) Consume(events []events.SensorEvent) error {
76 | // The filesystem output relies on there being an event to get a group id
77 | if len(events) <= 0 {
78 | return nil
79 | }
80 |
81 | // All events in a single call will have the same group and sensor
82 | group := events[0].Group
83 |
84 | metaFile, err := filesystemOutput.getGroupMetaFile(group)
85 | if err != nil {
86 | return fmt.Errorf("Failed to write event to filesystem: %w", err)
87 | }
88 |
89 | return metaFile.Consume(events)
90 | }
91 |
92 | func (filesystemOutput FilesystemOutput) getGroupMetaFile(group uuid.UUID) (metaFile, error) {
93 | var metaFileInstance metaFile
94 | var ok bool
95 | if metaFileInstance, ok = filesystemOutput.metaFiles[group]; !ok {
96 | return filesystemOutput.createAndStoreGroupMetaFile(group)
97 | }
98 |
99 | return metaFileInstance, nil
100 | }
101 |
102 | func (filesystemOutput FilesystemOutput) createAndStoreGroupMetaFile(group uuid.UUID) (metaFile, error) {
103 | filesystemOutput.metaFileCreationMutex.Lock()
104 | defer filesystemOutput.metaFileCreationMutex.Unlock()
105 |
106 | var metaFileInstance metaFile
107 | var ok bool
108 | if metaFileInstance, ok = filesystemOutput.metaFiles[group]; !ok {
109 |
110 | metaFileDirectoryPath := filepath.Join(filesystemOutput.directory, group.String())
111 | err := createDirectory(metaFileDirectoryPath)
112 | if err != nil {
113 | return metaFileInstance, fmt.Errorf("Failed to create directory %s for filesystem output: %w", metaFileDirectoryPath, err)
114 | }
115 |
116 | metaFileInstance, err = newMetaFile(metaFileDirectoryPath, filesystemOutput.maxFileAge, filesystemOutput.maxFileSize)
117 | if err != nil {
118 | return metaFileInstance, fmt.Errorf("Failed to create file for writing: %w", err)
119 | }
120 |
121 | filesystemOutput.metaFiles[group] = metaFileInstance
122 | }
123 |
124 | return metaFileInstance, nil
125 | }
126 |
127 | func createDirectory(path string) error {
128 | _, err := os.Stat(path)
129 | if os.IsNotExist(err) {
130 | err = os.Mkdir(path, 0750)
131 | }
132 | return err
133 | }
134 |
--------------------------------------------------------------------------------
/server/output/filesystem/filesystem_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package filesystem
21 |
22 | import (
23 | "encoding/json"
24 | "io/ioutil"
25 | "os"
26 | "path/filepath"
27 | "testing"
28 | "time"
29 |
30 | "github.com/google/uuid"
31 | "github.com/weaklayer/gateway/server/events"
32 | )
33 |
34 | func TestWritingEvents(t *testing.T) {
35 | group, err := uuid.NewRandom()
36 | if err != nil {
37 | t.Fatalf("Failed to generate UUID: %v", err)
38 | }
39 | sensor, err := uuid.NewRandom()
40 | if err != nil {
41 | t.Fatalf("Failed to generate UUID: %v", err)
42 | }
43 |
44 | filesystemOutput, err := NewFilesystemOutput(".", 60*time.Second, 100000000)
45 |
46 | event1 := events.SensorEvent{
47 | Type: "Unknown",
48 | Time: 1,
49 | Sensor: sensor,
50 | Group: group,
51 | Data: make(map[string]interface{}),
52 | }
53 |
54 | event2 := events.SensorEvent{
55 | Type: "Unknown",
56 | Time: 1,
57 | Sensor: sensor,
58 | Group: group,
59 | Data: make(map[string]interface{}),
60 | }
61 |
62 | err = filesystemOutput.Consume([]events.SensorEvent{event1, event2})
63 | if err != nil {
64 | t.Fatalf("Failed to write events to filesystem: %v", err)
65 | }
66 |
67 | filesystemOutput.Close()
68 | // wait for the file to close
69 | time.Sleep(1 * time.Second)
70 |
71 | dirPath := "./" + group.String()
72 | dir, err := os.Open(dirPath)
73 | if err != nil {
74 | t.Fatalf("Failed to open directory for reading: %v", err)
75 | }
76 | files, err := dir.Readdir(0)
77 | if err != nil {
78 | t.Fatalf("Failed to open directory for reading: %v", err)
79 | }
80 |
81 | for _, f := range files {
82 | fileName := f.Name()
83 | path := filepath.Join(dirPath, fileName)
84 | fileBytes, err := ioutil.ReadFile(path)
85 | if err != nil {
86 | t.Fatalf("Failed to read file: %v", err)
87 | }
88 |
89 | fileEvents := make([]events.SensorEvent, 0)
90 | json.Unmarshal(fileBytes, &fileEvents)
91 | if err != nil {
92 | t.Fatalf("Could not deserialize file contents into array of events: %v", err)
93 | }
94 |
95 | if len(fileEvents) != 2 {
96 | t.Fatalf("Wrong number of events found in file")
97 | }
98 |
99 | for _, event := range fileEvents {
100 | if group.String() != event.Group.String() || sensor.String() != event.Sensor.String() {
101 | t.Fatalf("Event identifiers do not match")
102 | }
103 | }
104 | os.Remove(path)
105 | }
106 | os.Remove(dirPath)
107 | }
108 |
--------------------------------------------------------------------------------
/server/output/filesystem/meta_file.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package filesystem
21 |
22 | import (
23 | "encoding/json"
24 | "fmt"
25 | "time"
26 |
27 | "github.com/rs/zerolog/log"
28 | "github.com/weaklayer/gateway/server/events"
29 | )
30 |
31 | func newMetaFile(groupDirectory string, maxFileAge time.Duration, maxFileSize int) (metaFile, error) {
32 | newFile, err := newFile(groupDirectory, maxFileSize)
33 | if err != nil {
34 | return metaFile{}, fmt.Errorf("Failed to create first file in directory %s: %w", groupDirectory, err)
35 | }
36 |
37 | eventData := make(chan []byte, 10000)
38 |
39 | go metaProcess(groupDirectory, maxFileAge, maxFileSize, newFile, eventData)
40 |
41 | return metaFile{
42 | groupDirectory: groupDirectory,
43 | eventData: eventData,
44 | }, nil
45 | }
46 |
47 | // metaFile handles the writing to and rotation of files
48 | type metaFile struct {
49 | groupDirectory string
50 | eventData chan<- []byte
51 | }
52 |
53 | // Close should only be called once after Consume is guaranteed not to be called again
54 | func (metaFile metaFile) Close() {
55 | close(metaFile.eventData)
56 | }
57 |
58 | func (metaFile metaFile) Consume(events []events.SensorEvent) error {
59 |
60 | var encounteredError = false
61 |
62 | for _, event := range events {
63 | serializedBytes, err := json.Marshal(event)
64 | if err != nil {
65 | encounteredError = true
66 | log.Info().Err(err).Msg("Failed to serialize event. Discarding Event")
67 | continue
68 | }
69 |
70 | select {
71 | case metaFile.eventData <- serializedBytes:
72 | default:
73 | encounteredError = true
74 | log.Info().Msgf("Event queue for filesystem output directory %s full. Discarding Event", metaFile.groupDirectory)
75 | continue
76 | }
77 | }
78 |
79 | if encounteredError {
80 | return fmt.Errorf("Encountered errors serializing events for filesystem")
81 | }
82 |
83 | return nil
84 | }
85 |
86 | func metaProcess(groupDirectory string, maxFileAge time.Duration, maxFileSize int, initialFile file, contentChannel <-chan []byte) {
87 | writingFile := initialFile
88 | fileTimer := time.NewTimer(maxFileAge)
89 |
90 | rotateFile := func() error {
91 | fileTimer = time.NewTimer(maxFileAge)
92 |
93 | newFile, err := newFile(groupDirectory, maxFileSize)
94 | if err != nil {
95 | return err
96 | }
97 |
98 | // only rotate the files if creating the new file succeeded
99 | oldFile := writingFile
100 | writingFile = newFile
101 | oldFile.Close()
102 |
103 | return nil
104 | }
105 |
106 | readLoop:
107 | for {
108 | select {
109 | case eventContent, ok := <-contentChannel:
110 | if !ok {
111 | // contentChannel closed. Time to shut down.
112 | break readLoop
113 | }
114 |
115 | contentWritten := writingFile.Write(eventContent)
116 | if contentWritten {
117 | continue
118 | }
119 |
120 | err := rotateFile()
121 | if err != nil {
122 | log.Info().Err(err).Msg("File rotation failed. Discarding event")
123 | continue
124 | }
125 |
126 | contentWritten = writingFile.Write(eventContent)
127 | if !contentWritten {
128 | log.Info().Msg("Writing to file failed after file rotation. Discarding event")
129 | }
130 | case <-fileTimer.C:
131 | err := rotateFile()
132 | if err != nil {
133 | log.Info().Err(err).Msg("File rotation on timer failed.")
134 | }
135 | }
136 | }
137 |
138 | writingFile.Close()
139 | }
140 |
--------------------------------------------------------------------------------
/server/output/output.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package output
21 |
22 | import (
23 | "fmt"
24 |
25 | "github.com/weaklayer/gateway/server/events"
26 | )
27 |
28 | // Output is the interface that all outputs implement
29 | // It allows the top-level output handler to keep a list of
30 | // many outputs to send events to without knowing their implenetation
31 | type Output interface {
32 | Consume(events []events.SensorEvent) error
33 | // Close performs any nessecary cleanup in an output (e.g. close file descriptor)
34 | Close()
35 | }
36 |
37 | // NewTopOutput creates a NewTopOutput instance
38 | func NewTopOutput(outputs []Output) TopOutput {
39 | return TopOutput{
40 | outputs: outputs,
41 | }
42 | }
43 |
44 | // TopOutput is the main destination for events from the sensor API
45 | // It dispatches events to all the different outputs
46 | type TopOutput struct {
47 | outputs []Output
48 | }
49 |
50 | // Close closes all outputs
51 | func (topOutput TopOutput) Close() {
52 | for _, output := range topOutput.outputs {
53 | output.Close()
54 | }
55 | }
56 |
57 | // Consume is the main destination for sensor events.
58 | func (topOutput TopOutput) Consume(events []events.SensorEvent) error {
59 | var errors []error = nil
60 | for _, output := range topOutput.outputs {
61 | err := output.Consume(events)
62 | if err != nil {
63 | errors = append(errors, err)
64 | }
65 | }
66 |
67 | if errors != nil {
68 | return fmt.Errorf("Error(s) encountered consuming events: %v", errors)
69 | }
70 |
71 | return nil
72 | }
73 |
--------------------------------------------------------------------------------
/server/output/output_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package output
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/google/uuid"
26 | "github.com/weaklayer/gateway/server/events"
27 | )
28 |
29 | type dummyConsumer struct {
30 | eventsReceived *int
31 | }
32 |
33 | func (dummyConsumer dummyConsumer) Consume(events []events.SensorEvent) error {
34 | *dummyConsumer.eventsReceived = *dummyConsumer.eventsReceived + len(events)
35 | return nil
36 | }
37 |
38 | func (dummyConsumer dummyConsumer) Close() {
39 | }
40 |
41 | func TestOutputConsume(t *testing.T) {
42 |
43 | sensor, err := uuid.NewRandom()
44 | if err != nil {
45 | t.Fatalf("Failed to generate UUID: %v", err)
46 | }
47 | group, err := uuid.NewRandom()
48 | if err != nil {
49 | t.Fatalf("Failed to generate UUID: %v", err)
50 | }
51 |
52 | event1 := events.SensorEvent{
53 | Type: "Unknown",
54 | Time: 1,
55 | Sensor: sensor,
56 | Group: group,
57 | }
58 |
59 | event2 := events.SensorEvent{
60 | Type: "Unknown",
61 | Time: 1,
62 | Sensor: sensor,
63 | Group: group,
64 | }
65 |
66 | events := []events.SensorEvent{event1, event2}
67 |
68 | er1 := 0
69 | er2 := 0
70 | output1 := dummyConsumer{eventsReceived: &er1}
71 | output2 := dummyConsumer{eventsReceived: &er2}
72 |
73 | topOutput := TopOutput{outputs: []Output{output1, output2}}
74 |
75 | err = topOutput.Consume(events)
76 | if err != nil {
77 | t.Fatalf("Error consuming event: %v", err)
78 | }
79 |
80 | if *output1.eventsReceived != 2 {
81 | t.Fatalf("Output 1 received %d events instead of the expected 2", *output1.eventsReceived)
82 | }
83 | if *output2.eventsReceived != 2 {
84 | t.Fatalf("Output 2 received %d events instead of the expected 2", *output2.eventsReceived)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/server/output/stdout/stdout.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package stdout
21 |
22 | import (
23 | "encoding/json"
24 | "fmt"
25 |
26 | "github.com/rs/zerolog/log"
27 |
28 | "github.com/weaklayer/gateway/server/events"
29 | )
30 |
31 | // NewStdoutOutput creates an StdoutOutput instance
32 | func NewStdoutOutput() StdoutOutput {
33 | eventStrings := make(chan string, 10000)
34 | stdoutput := StdoutOutput{
35 | eventStrings: eventStrings,
36 | }
37 |
38 | go process(eventStrings)
39 |
40 | return stdoutput
41 | }
42 |
43 | // StdoutOutput is an event output that writes events to stdout
44 | type StdoutOutput struct {
45 | eventStrings chan<- string
46 | }
47 |
48 | // Close does nothing for StdoutOutput
49 | // It is implemented to conform with the Output interface
50 | func (stdoutOutput StdoutOutput) Close() {
51 | }
52 |
53 | // Consume takes the events and writes them to a channel for processing
54 | func (stdoutOutput StdoutOutput) Consume(events []events.SensorEvent) error {
55 | var encounteredError = false
56 |
57 | for _, event := range events {
58 | serializedBytes, err := json.Marshal(event)
59 | if err != nil {
60 | encounteredError = true
61 | log.Info().Err(err).Msg("Failed to serialized event. Discarding Event.")
62 | continue
63 | }
64 |
65 | select {
66 | case stdoutOutput.eventStrings <- string(serializedBytes):
67 | default:
68 | encounteredError = true
69 | log.Info().Msgf("Event queue for stdout output full. Discarding Event.")
70 | continue
71 | }
72 | }
73 |
74 | if encounteredError {
75 | return fmt.Errorf("Encountered errors serializing events for stdout")
76 | }
77 |
78 | return nil
79 | }
80 |
81 | func process(eventStrings <-chan string) {
82 | for eventString := range eventStrings {
83 | n, err := fmt.Println(eventString)
84 | if err != nil {
85 | log.Info().Err(err).Msg("Error printing event to stdout.")
86 | }
87 |
88 | if n < len(eventString) {
89 | log.Info().Msg("Failed to print all event bytes to stdout.")
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/server/output/stdout/stdout_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package stdout
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/google/uuid"
26 | "github.com/weaklayer/gateway/server/events"
27 | )
28 |
29 | func TestStdoutOutputConsume(t *testing.T) {
30 |
31 | sensor, err := uuid.NewRandom()
32 | if err != nil {
33 | t.Fatalf("Failed to generate UUID: %v", err)
34 | }
35 | group, err := uuid.NewRandom()
36 | if err != nil {
37 | t.Fatalf("Failed to generate UUID: %v", err)
38 | }
39 |
40 | event := events.SensorEvent{
41 | Type: "Unknown",
42 | Time: 1,
43 | Sensor: sensor,
44 | Group: group,
45 | Data: make(map[string]interface{}),
46 | }
47 |
48 | stdoutput := NewStdoutOutput()
49 |
50 | err = stdoutput.Consume([]events.SensorEvent{event})
51 | if err != nil {
52 | t.Fatalf("Error consuming event: %v", err)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package server
21 |
22 | import (
23 | "context"
24 | "fmt"
25 | stdlog "log"
26 | "net/http"
27 | "os"
28 | "os/signal"
29 | "syscall"
30 | "time"
31 |
32 | "github.com/rs/zerolog/log"
33 |
34 | "github.com/weaklayer/gateway/common/auth"
35 | "github.com/weaklayer/gateway/server/api"
36 | "github.com/weaklayer/gateway/server/output"
37 | "github.com/weaklayer/gateway/server/output/filesystem"
38 | "github.com/weaklayer/gateway/server/output/stdout"
39 | "github.com/weaklayer/gateway/server/token"
40 | )
41 |
42 | // Config is the struct of values required to start the Weaklayer Gateway Server
43 | type Config struct {
44 | Sensor struct {
45 | API struct {
46 | Host string
47 | Port int32
48 | HTTPS struct {
49 | Certificate string
50 | Key string
51 | Password string
52 | }
53 | }
54 | Token struct {
55 | Duration int64 // this is the number of microseconds new tokens are valid for
56 | Secrets struct {
57 | Current []byte
58 | Past [][]byte
59 | }
60 | }
61 | Install struct {
62 | Verifiers []auth.Verifier
63 | }
64 | }
65 | Outputs []struct {
66 | Type string
67 | Directory string
68 | Age int64
69 | Size int
70 | }
71 | }
72 |
73 | func createEventOutput(config Config) (output.Output, error) {
74 | outputs := []output.Output{}
75 |
76 | for _, configOutput := range config.Outputs {
77 | if configOutput.Type == "stdout" {
78 | outputs = append(outputs, stdout.NewStdoutOutput())
79 | } else if configOutput.Type == "filesystem" {
80 | directory := configOutput.Directory
81 | age := configOutput.Age
82 | size := configOutput.Size
83 | filesystemOutput, err := filesystem.NewFilesystemOutput(directory,
84 | time.Duration(age)*time.Microsecond,
85 | size)
86 | if err != nil {
87 | return output.NewTopOutput([]output.Output{}), err
88 | }
89 | outputs = append(outputs, filesystemOutput)
90 | }
91 | }
92 |
93 | return output.NewTopOutput(outputs), nil
94 | }
95 |
96 | // Run runs the Weaklayer Gateway Server
97 | func Run(config Config) error {
98 |
99 | log.Info().Msg("Starting Weaklayer Gateway Server")
100 |
101 | topLevelEventOutput, err := createEventOutput(config)
102 | if err != nil {
103 | return fmt.Errorf("Failed to create desired outputs: %w", err)
104 | }
105 |
106 | tokenProcessor := token.NewProcessor(config.Sensor.Token.Secrets.Current, config.Sensor.Token.Secrets.Past, config.Sensor.Token.Duration/1000000)
107 | installAPI, err := api.NewInstallAPI(tokenProcessor, topLevelEventOutput, config.Sensor.Install.Verifiers)
108 | if err != nil {
109 | return fmt.Errorf("Failed to create sensor install API endpoint: %w", err)
110 | }
111 |
112 | eventsAPI, err := api.NewEventsAPI(tokenProcessor, topLevelEventOutput)
113 | if err != nil {
114 | return fmt.Errorf("Failed to create sensor events API endpoint: %w", err)
115 | }
116 |
117 | sensorAPI := api.SensorAPI{
118 | EventsHandler: eventsAPI,
119 | InstallHandler: installAPI,
120 | }
121 |
122 | var server *http.Server
123 | if useTLS(config.Sensor.API.HTTPS.Certificate, config.Sensor.API.HTTPS.Key) {
124 | tlsConfig, err := getTLSConfig(config.Sensor.API.HTTPS.Certificate, config.Sensor.API.HTTPS.Key, config.Sensor.API.HTTPS.Password)
125 | if err != nil {
126 | return fmt.Errorf("Failed to produce TLS config: %w", err)
127 | }
128 |
129 | server = &http.Server{
130 | ErrorLog: stdlog.New(log.Logger, "", 0),
131 | Addr: fmt.Sprintf("%s:%d", config.Sensor.API.Host, config.Sensor.API.Port),
132 | Handler: sensorAPI,
133 | TLSConfig: tlsConfig,
134 | }
135 |
136 | go func() {
137 | err := server.ListenAndServeTLS("", "")
138 | if err != nil && err != http.ErrServerClosed {
139 | log.Error().Err(err).Msg("HTTP server error")
140 | }
141 | }()
142 | } else {
143 | server = &http.Server{
144 | ErrorLog: stdlog.New(log.Logger, "", 0),
145 | Addr: fmt.Sprintf("%s:%d", config.Sensor.API.Host, config.Sensor.API.Port),
146 | Handler: sensorAPI,
147 | }
148 | go func() {
149 | err := server.ListenAndServe()
150 | if err != nil && err != http.ErrServerClosed {
151 | log.Error().Err(err).Msg("HTTP server error")
152 | }
153 | }()
154 | }
155 |
156 | shutdown := make(chan os.Signal, 1)
157 | signal.Notify(shutdown, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
158 | <-shutdown
159 |
160 | // Stop the HTTP server. Give 5 seconds max for this.
161 | context, cancel := context.WithTimeout(context.Background(), 5*time.Second)
162 | defer cancel()
163 | err = server.Shutdown(context)
164 | if err != nil {
165 | log.Error().Err(err).Msg("Error shutting down HTTP server")
166 | }
167 |
168 | // Requests are stopped now.
169 | // Close outputs. Wait 1 seconds for it to happen.
170 | topLevelEventOutput.Close()
171 | time.Sleep(1 * time.Second)
172 |
173 | return nil
174 | }
175 |
--------------------------------------------------------------------------------
/server/test-crypt/example_cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIFoDCCA4gCCQDYmCoIoudoiDANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UEBhMC
3 | Q0ExEDAOBgNVBAgMB0FsYmVydGExEDAOBgNVBAcMB0NhbGdhcnkxEjAQBgNVBAoM
4 | CVdlYWtsYXllcjEQMA4GA1UECwwHR2F0ZXdheTESMBAGA1UEAwwJbG9jYWxob3N0
5 | MSQwIgYJKoZIhvcNAQkBFhVjb250YWN0QHdlYWtsYXllci5jb20wHhcNMjAwNzE1
6 | MTMxNTQ0WhcNMzAwNzEzMTMxNTQ0WjCBkTELMAkGA1UEBhMCQ0ExEDAOBgNVBAgM
7 | B0FsYmVydGExEDAOBgNVBAcMB0NhbGdhcnkxEjAQBgNVBAoMCVdlYWtsYXllcjEQ
8 | MA4GA1UECwwHR2F0ZXdheTESMBAGA1UEAwwJbG9jYWxob3N0MSQwIgYJKoZIhvcN
9 | AQkBFhVjb250YWN0QHdlYWtsYXllci5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4IC
10 | DwAwggIKAoICAQDUFbZEHcSnfttKFjJO/PseLctAbhYLWy1H4b1oDpyWwlt9WWyB
11 | aPXQxP5IaZZq3C6tNqgiwX0KIOh9HNxHVsBvGdGlouP1hjMmtpCmxfGifZgjM8Ay
12 | gUfT5USzDxqpOO+quCx/oS5Keb1iEcycXip7fDJ/BCT+XJCMM5hhNBfW8Z4hAo4B
13 | z4kkxwKzzpf9PBSrEA4buhdD0HDL/Nwpa0/x9ey0JlOr7fcCuinuWDAjGBx0Cw6X
14 | XNTdNIO6q2uSz5sjjtb/huaKjSGeMjjxjJO9+sA54MB1hsZ2xmAhDWIuSUgemht6
15 | PL1ls4GWogNxLwdxGX9btSwvjTufD1srion03blg19WpabQ9fd0psrd8UlQnlzUp
16 | uDj9ciQulcUFJ9T8FmHdFUBSX65kQ1Ojy5dTFsYXMCk9Vj1nt7C4NErfNYQ+sRIT
17 | y4BVR3Ays0e2t1mUaIYXT/kpCVMdp2ubcMXJZ9iOX1r8dDugiE+z8nhKHHqMvvMQ
18 | LL3+VP5lVQhg8xvfrzfGZ/7sVVIBwdB8MPjlvLaFNgrurLS3Uj0X8Cv5iDA9cWwM
19 | T8edlmTpg/Zqvkshq2VJ31n7vn6de5JkYPZImSfVPGE4LZUuJVcR04Z7XORH8t1L
20 | dtqTf0PYo12jyFGHyPNPQoJpVfYCNsoFhqT3cHBI+tGX/zgauRMPPCy9NQIDAQAB
21 | MA0GCSqGSIb3DQEBCwUAA4ICAQBwO5H1x2uCrdjuUt7uLsXkMGTSKGxcAxHo0WaQ
22 | v9UikIxIeYaeZeR0RohRJO1F/VgZbyDA2IirF9/SKuZf1nlutWd17miU7Jsb0e2a
23 | pZNvVHNpELEtJHdNbPBwj57rwact+bd6MqJvIg/f+fepLNay/KPssOd1APROugTW
24 | 3O48dCNjbku8cYC2lmQlYfaHWVf9c+Yi5kYSjxT7IWLJFHMEYr6/zY2eLwg+JcGR
25 | phZ+FvnRp4t5sySBVpYv8RbOGqXlT2xlDxHv2VfpB9ncWXs5TG6tCcZKuKemqN+K
26 | rB02qcb0YF6AJ8DdIJbDxjUo69cdmRS4MfX6TgKqvKF/z+I0JSYaIAew/CC8q98G
27 | eYdYQbXFtwlmw82AudJqQhwIiLKadfD8S/JUcNGlCG5RpuNOIoSfIo5yyQHgTh+Z
28 | kaMtNUB8rUXjihEjxAWNhTf24g4atW92fXlHk3lw71wy4ubjcMkXHLBV/4m2TPhl
29 | x1uAIgrrIgb9huyRPeyK7SAcDM+OFfKSiw1pvC497oPJGUc1pHByDxc96c+i4PgP
30 | Fm47cYMCLxbC9n1smlnaQG9oSlAbUcRpTstYr5VmOaYg0y9jhKgWEhJtRq20fT65
31 | 23EWKVtn1uM77wSHXrQRQbH0db3HmdYALQSJl44NIwRpEYwIZMheQByEjFkeIa/v
32 | ffrYVQ==
33 | -----END CERTIFICATE-----
34 |
--------------------------------------------------------------------------------
/server/test-crypt/example_key_encrypted.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | Proc-Type: 4,ENCRYPTED
3 | DEK-Info: AES-256-CBC,43E4266D9EA047074BB07A806CBE24A0
4 |
5 | K753dHDjVowSuQsdljzeYmbV5+L9l7sVSaR59JWxs3PDmk1AYCy8BuH7pvtQua8L
6 | DZlRVH0NclGp1D1q2xbiFgiwe+rYWYMPCiIzjwCqQOUm5wbbn+UwVgP8nOwnHkcU
7 | OaqRUljCyq4DwfLRvs3Japr5jJUJjtEjtQlMwSsjx+HJMEEd1sq5oDFTNWUFcmf+
8 | PQ7fDi1ggMBeZantcx5sSHy2xuLrPhQNEj5K282dQvbV2/JJqVcKfduaxEBQtyyr
9 | 3AbHgw56kKjSBeMz56lciEEi/0w76WtWGCF5FfSQWyEQjY3A0YT3abWexfzA+btj
10 | jK0A2tKNSE8n0Ed8E+GqWF2sgmhFcptUyAHxBO1OM2JboCF7Tt6td2nsN83IJS6y
11 | 5uydBBYiCXQIONZdKaVuZJmkvN1d05Nks5Oqc+MiWBtIsKddeEGb+kCpKZmAWrya
12 | QCvMbSgFzibXYQeqOznSAiEe6nBEAjWdinmVx4oSVyabM9XvVanWt4fdUXChDYXa
13 | tJ0/VnaGGSmQBeeuAIR+qyI2Y0z8uMCgNK1gJ0SuYvnfjs30VHvOChNpCwFN16vk
14 | 8vShq9/esOf1BPu8loj+6Kj5nzWrRHPFzQDWYQqS3HQ3m8QActR5pakAHyWS4T2c
15 | DwowsECdko0aTCpudesbQj+8AjyFb+gx5Jbpz7mMtJzctyWiMKLuJPuh81HqxZVM
16 | D0Etc5JIzAu5Ql9lYFDaQIB9RwfJqcEPIvgZMShE95SPL/sU1sKqQ0Ct4FL5hPFb
17 | EM2jKY+BceTHa8QTyihrtATUjfKasD2zfcf27rPrmEd3c+7NpLHhjxljW9JINiqz
18 | f01/dyW/oqKtTssxx3GmlkuUTJbFCoVma1AFHgGFncqjpuYHfxpck6zNhQ7jlBpF
19 | sSP7JqroR9au749u1O8eoouE1ENmxkfV04BE13v3qHXxNYhXRSt+EfMMz4UIxM1G
20 | 7O+FT6ZrTmDToPmZ3sstAQwTmNQ7y58gzDk27/Yf6ltQaHylVIlHTrXtAg2IDdLA
21 | w1AZ10a25YyQB/DfGcGdaT1UbPkYQH/aAauxgpx15N4ua9Rdols9Gj1RxcCmQ9Jz
22 | AFNgPyYKA3gY6UDIynuLoRgagU051+ttt2fILafXNQVV5qaWJcJDWIKx03SNYJlI
23 | 8bdMtJ2IYQo32bZlX2ZRr6U2WeJMach9V3sfHjCBIkAqDVXZnoH2TWIIzxlFGUL7
24 | RulGEYQaSXTHfEIUvw22n1/YqOcNebz77pNuuJ6ZhGtkI9oOMCXPCSBIYTEQU28R
25 | VSY4vB6sTfN57+BIsprETRfA0dhAHEXJiu7NlOscR0zqlk6MyEWqKALwcjM4pSt4
26 | 5QfbzQh+c2Vq0nk36/zGT1SGZcT7116IoG7PMagY7EU1HgPYJlitpsz+SLgwW8ur
27 | 67lA5wn68nS78He4C2yBjzTSz7rTQ7TCXUbCiSeOunTJRgQltjDldxrp2S7FNZqh
28 | Eue7zF8mDrf+LQf2TWVBTTyOUy2BsydkGAaSht+8Y5HynB5Fv843ccR0s/XPr1Xa
29 | 6OnDyryOObPEBYGUSIxQkoqAq0XYS7X0a3Fh/815noPP+MyngsVCdRHcesY1BAId
30 | 1dBzQWPoF5p1ekvntwhoxXbSGPoDP7q4KcixPbgoy3Ak6jdvgRqS5wDcyoonBF1n
31 | V399RWfzbDgPkX1SsTAmE4qEOg8IAXnnNetbD5w0M1xCpp3gPXcJudzmwyr1PUwK
32 | +UG+JQJwS/u9nvi617p4/Fh5Ag9pXRnGpc+5PJ2rgtsrlNcPTSfDEV7YqhPz+Gtl
33 | KpXztPMXCxnREREOww6jNrxKwL0fJ92vIPs3qj30leN1dLmn3V/hV1t6URDQKsxm
34 | JAhbNM2vBGuOKUBTKgrMYYXd1faDelO0yqK2zKnii8mjRABkpXNaiBRqecMEvyj+
35 | VZgeJudkdGEz546flDUqQ1LsF/fUUjqdqcpqNCPDhpxmLGqNJwfNxVBJsTl5kC6C
36 | 4y2wqzJY31Kvs6Up2bkKKz7Yc8f6kvhjkUuLzjrNzgwc0ap28m6TS6WUxPB7vtGw
37 | Pwk+wNvT/JEK6XZJTkXeNAWO4hsbeJXlQh2n6//4FL4bTZvRYlUJS01gihC7efkc
38 | P/O2mGMT+UOtn/9UcxOuz5rCctZ/O13pgoJ3R+MGOmBrfd2+SV0uuNLAljfZF/za
39 | tI+u/0ikZQUcfrCevWV+l+B2afoABT9cxMSS4F6265c9RxnjOJLRuBYJEEwv6QIs
40 | y5O/vnTacTzUAkyUqb7LAwpUP6hp3SXKnnghZqkMoxAUAUyGzJihv9yGOm8Mnep6
41 | ZcHKMRVzLaED7F3KHBo5j2L1fQVn2WFH5mfBt2fbg433HEnA+5AYmkK+nqBzxnNd
42 | Gspnx46cmPXT00s9sJ7EwvrVXowxeT3GRk1XredX1r2mbwas/XytyRg4ob708STy
43 | myesphxFDGXg8ly922J+reuIZKiOHEdcN7jvIDU2HTIfe/9vOTOQXh/FmDsktKLb
44 | sehM16hV7FiFOE7InZia15JoyNAXXP8BfyYSh4ZRXWbwZ8jYDH8RnIwL6QUy14l2
45 | UxtkaL9gXZNcOpc1I6tOR8nyXMm95dG3SI1TeBUXcifn6BjVipFvgcFkR46JDmxU
46 | J2VmdkomY4j1Qx9AJjrQUQ0Gri/p+T3oi7+awdTSfX7zPLNVasNGY41I3A0AExFW
47 | diZHdp0QcyF74WD+aX3YlPTufnboFxwpzXNoNNMzHMXvlQ4tDDT6SD85YsKTOGYk
48 | YNt5rZql0uQLfNv99dGG5IDNq61GUjHGy680Wu8Kofp/PCHSB6ZfyCtEvHjirzbc
49 | pAWPeCy8i+tkI7DGBowBDOnKtGaB7F9JECjrepLyETXu23PC4qvSufr2P8fSFfhI
50 | rVi/nMvJX2KMBmr0Gc2Aj8Yzxl63njzeDPgj0Sp8yFgRHVZ2ziIhuFwO1GBNopPS
51 | Xi1GEt4OaZGQVFqoXnggPxSRiTp6CbsW8e7Wt7iMLAWny0vCix3TdIZY/b85snjz
52 | tUx/S3QXT6VPriCKwlA1F+/Mhm/Aa1uNn+BsUug6P39+q6+j4NJOi1qXoD4Uz2zg
53 | NfwAopox4ETAGIjPtVKhHrBS6FVFK0uQ8zwbzz5UG3SDJ9b8m4jzUXPvuGIxZn2H
54 | -----END RSA PRIVATE KEY-----
55 |
--------------------------------------------------------------------------------
/server/test-crypt/example_key_unencrypted.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIJKAIBAAKCAgEA1BW2RB3Ep37bShYyTvz7Hi3LQG4WC1stR+G9aA6clsJbfVls
3 | gWj10MT+SGmWatwurTaoIsF9CiDofRzcR1bAbxnRpaLj9YYzJraQpsXxon2YIzPA
4 | MoFH0+VEsw8aqTjvqrgsf6EuSnm9YhHMnF4qe3wyfwQk/lyQjDOYYTQX1vGeIQKO
5 | Ac+JJMcCs86X/TwUqxAOG7oXQ9Bwy/zcKWtP8fXstCZTq+33Arop7lgwIxgcdAsO
6 | l1zU3TSDuqtrks+bI47W/4bmio0hnjI48YyTvfrAOeDAdYbGdsZgIQ1iLklIHpob
7 | ejy9ZbOBlqIDcS8HcRl/W7UsL407nw9bK4qJ9N25YNfVqWm0PX3dKbK3fFJUJ5c1
8 | Kbg4/XIkLpXFBSfU/BZh3RVAUl+uZENTo8uXUxbGFzApPVY9Z7ewuDRK3zWEPrES
9 | E8uAVUdwMrNHtrdZlGiGF0/5KQlTHadrm3DFyWfYjl9a/HQ7oIhPs/J4Shx6jL7z
10 | ECy9/lT+ZVUIYPMb3683xmf+7FVSAcHQfDD45by2hTYK7qy0t1I9F/Ar+YgwPXFs
11 | DE/HnZZk6YP2ar5LIatlSd9Z+75+nXuSZGD2SJkn1TxhOC2VLiVXEdOGe1zkR/Ld
12 | S3bak39D2KNdo8hRh8jzT0KCaVX2AjbKBYak93BwSPrRl/84GrkTDzwsvTUCAwEA
13 | AQKCAgATJrrEv+NoNJ5cvFBirZmHih8WfFplQCCAozPVV6xwOLDGSvLvZKj95ywH
14 | IfHBSIy2e38HVG2UIpCb66VAk+bgoXY6/NCU9T9dOZMqXCmLwMeiNiQe62mvCr7/
15 | ZFfF/Cw4QFVUWRuzAfdGCdFuNVqkt/xfV+J3TaiH4IjcDjYw5LSgynhEVZdOyOHH
16 | ltpGtcwv/k0n19AOAZ6N7RF8dAJOGST8x5E6r0xk5KawrSn7oUvAdtckd++I4hsb
17 | GiumoDSkK1viyy26STBF9scJmOYme6QsFc2LT1ZQl21KzsiU4W0vxj7A9QctTd5f
18 | 2dVxvQtw/RpxD5Uj3h8KJ/rVnbcDSYSvwmEKrFlDC4RhL+0wA1YleWRQXnn7Smic
19 | k4F/ttfypflq7GQIQzrTDILvIm3arDDvo+yPceNtFE7K/ZShDvMaS/MAzj3XXXfm
20 | slLSWk+fJMl06I9PbixXCD2p9cj84nzCe1t1HTCFqNNPAUDHS3C2t3Z9JvmlJPX1
21 | FFG/+iclqrOqPoWTgo5rGnPl0ICNxoUqh5IwufJBLv91snOyF+5Bo+GiwngjCUsF
22 | w/GUeobI0f5IAc4cFd1ecLoKRs6ClyzOVYZo+JAWExBLqeNVnIxIWBJm7suWe9DI
23 | VTPp3TTnpVKgeyUjrBeP/suGECN0YAR1Gm+AhxI5kxB7mSOCqQKCAQEA63T195rY
24 | TY+QbYFdMzMkUFX/kYY4KnYqsXjFjyIBWc3AHEpViWAJfBHd7XcZ3fgWUxY32tha
25 | JvMO9dA0oAldCFIejHxwRR5Co+6VyQj/m/KlQtlZ/EyrHLGwQ+uPHtGXQtdiE1Pv
26 | S4zozwmjtgP28C2w1oHcu2PIprhM7CP3kgufM9wf+bKyg5ZPqIx6e1Ebqf2U4Y4x
27 | K9tTDRcjNjfPD68EeTCZuK/g3wb8/IjLsYCDfX++vjSV+RyojcEBHlwmvw0P/EbH
28 | bD2rhoU3SmqPGgoFuz6Hu/tjrRim2Ao1+lpi8nQQhiFInB3Fa6bUEp1L7ftWOn7U
29 | jabEArbJUmo2PwKCAQEA5pa5pUckn7yJjwrgTAYetjK5CuIr1MZ3jFTwPrIaMpL6
30 | qTJlo/j//QABedpOABf3NCyipgbFUUDf6Ak065ETSBhgYOqQhi6c67EwgEi4ydGh
31 | sqteVPZKS/sXijqnYVAL7WPumzEqBvUEqhdwVLArihU4/cJpn/MqD7q3gArl90rq
32 | 1nzq8Xad/60W2hDdROjWT9kp6uguDen0LZaRwZfqo2bEbVG9rmnGhGgWRjlC0dHT
33 | sukBC/HWwvva4jQ7VvxaiIveJxFr3kdkB9V0stpdmhCjchKQzpMrsaup9xeZtMGN
34 | xYc2hte0nVPdWejCCR4OKQzSROz6s85kEQbEVDZ3iwKCAQA4qEpPrIkEENm2H/zn
35 | RxUj5625vMxjG1AmqGMRkCM3EtV5eUGf7uYZXstCSviEeyAUGjKSjKEU4kPlTpZ7
36 | NAWY7PnA4Gi7mQ58F/sfBvVK91ZhAaAvn9tE6lT54wLrbY+yW9WTxQy950hWGYG1
37 | WLrhDH2TAGi3BsnPpGWOJRF7qSRD/GINWbyvAUplynfmmJvJieV7aRXX8czIR5sH
38 | fuJqabjv2IwE7v1zbWEO+3PYYI5DcqvZZRn5ebXtdlXoklAYhPIlyHpQR36wvfmS
39 | BJYus96xMdjDmThg/J+ZOMvIVFue7+LSA8xPFLLetqQtXoBY4bPcG4zWERz1cjp7
40 | hGdNAoIBAQCjPRLBkaK4vmT3cbClwTNesnvS25hB8hpRqxPEnprzMJ/oxbe0pCK8
41 | vZNN6yCK6mxjcDHYrTRkuDOKdXIUTcjDohYgpg2YIEmQhQib2F+Q6Cm7bZvDM/QW
42 | 4/OfNLSnrF41OSiCjwR8llot+5fXLDw2bZ3B7y6NQDyHlBwxtgVk0pF7NEWymNFz
43 | Xdqul3+9N8P4FPmaxbFMwlHNMCmRTnWaJbdq2JIadeFGfNU/82t9VDi1EERga10o
44 | /djPiEB17QI+Zh3ntGqTmiCTdyY9M19OIPdEWyZOR1uIVMlnFjsG/nrZ2kXcobnI
45 | IsWxz5gFklRd9SiqCMXWIocukobMQx7PAoIBAEWoJz4P6O91mxgAKAIQEkCt2aGU
46 | x+87Ai3t4/EAGEdrwwW/Zh0ljRW1cQ85XfNL8ysd2+1Jlcb9i/m77EnecdMGQa3G
47 | qaqCs+couZXCXBuS5Uv8hUwfaBWhqAG69M46WuqRRqeuPhF8bz1QU2Gs0ofnRCXe
48 | JMx8zhUVBTFqUWChz+rbgWf1j55zZ5NQVQwW7S+oXWA/ekPUn3OxgRGuMtVrB2ul
49 | q9sfrtlakIlsPO6+qqZDb+05X13VWT5R5Fgz45+i1SyGPGxCPNNPj03VNMgeJTQC
50 | tQ6pObSzdaFMAnFG++IV/2Pw+4UV2Od4kNqPy49dMJvBi/kv75ghgAuJ3YQ=
51 | -----END RSA PRIVATE KEY-----
52 |
--------------------------------------------------------------------------------
/server/tls.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package server
21 |
22 | import (
23 | "crypto/tls"
24 | "crypto/x509"
25 | "encoding/pem"
26 | "fmt"
27 | "io/ioutil"
28 | )
29 |
30 | func useTLS(certificatePath string, keyPath string) bool {
31 | return certificatePath != "" && keyPath != ""
32 | }
33 |
34 | func getTLSConfig(certificatePath string, keyPath string, keyPassword string) (*tls.Config, error) {
35 |
36 | certificateFileBytes, err := ioutil.ReadFile(certificatePath)
37 | if err != nil {
38 | return nil, fmt.Errorf("Failed to read certificate file contents: %w", err)
39 | }
40 |
41 | keyPemBytes, err := ioutil.ReadFile(keyPath)
42 | if err != nil {
43 | return nil, fmt.Errorf("Failed to read private key file contents: %w", err)
44 | }
45 |
46 | keyPem, _ := pem.Decode(keyPemBytes)
47 | if keyPem == nil {
48 | return nil, fmt.Errorf("Private key file is not in PEM format")
49 | }
50 |
51 | // Decrypt private key if it is encrypted
52 | if x509.IsEncryptedPEMBlock(keyPem) {
53 | decryptedKeyBytes, err := x509.DecryptPEMBlock(keyPem, []byte(keyPassword))
54 | if err != nil {
55 | return nil, fmt.Errorf("Failed to decrypt private key: %w", err)
56 | }
57 | decryptedKeyPem := &pem.Block{
58 | Type: keyPem.Type,
59 | Bytes: decryptedKeyBytes,
60 | }
61 |
62 | // override the key pem bytes from the file
63 | keyPemBytes = pem.EncodeToMemory(decryptedKeyPem)
64 | if keyPemBytes == nil {
65 | return nil, fmt.Errorf("Failed to convert decrypted private key to PEM format")
66 | }
67 | }
68 |
69 | certificate, err := tls.X509KeyPair(certificateFileBytes, keyPemBytes)
70 |
71 | tlsConfig := &tls.Config{
72 | MinVersion: tls.VersionTLS12,
73 | Certificates: []tls.Certificate{certificate},
74 | }
75 |
76 | return tlsConfig, nil
77 | }
78 |
--------------------------------------------------------------------------------
/server/tls_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package server
21 |
22 | import (
23 | "crypto/tls"
24 | "testing"
25 | )
26 |
27 | func TestUseTLS(t *testing.T) {
28 | if useTLS("", "") {
29 | t.Fatal("Trying to use TLS when no certificate or key specified")
30 | }
31 | }
32 |
33 | func TestCertAndEncryptedKeyParsing(t *testing.T) {
34 | certPath := "test-crypt/example_cert.pem"
35 | keyPath := "test-crypt/example_key_encrypted.pem"
36 | keyPassword := "examplekeypassword"
37 |
38 | config, err := getTLSConfig(certPath, keyPath, keyPassword)
39 | if err != nil {
40 | t.Fatal("Failed to import TLS certificate and private key", err)
41 | }
42 |
43 | if config.MinVersion != tls.VersionTLS12 {
44 | t.Fatal("Minimum TLS version is not TLS1.2")
45 | }
46 | }
47 | func TestCertAndUnencryptedKeyParsing(t *testing.T) {
48 | certPath := "test-crypt/example_cert.pem"
49 | keyPath := "test-crypt/example_key_unencrypted.pem"
50 | keyPassword := ""
51 |
52 | config, err := getTLSConfig(certPath, keyPath, keyPassword)
53 | if err != nil {
54 | t.Fatal("Failed to import TLS certificate and private key", err)
55 | }
56 |
57 | if config.MinVersion != tls.VersionTLS12 {
58 | t.Fatal("Minimum TLS version is not TLS1.2")
59 | }
60 | }
61 |
62 | func TestMissingCert(t *testing.T) {
63 | certPath := "doesntexist.pem"
64 | keyPath := "test-crypt/example_key_unencrypted.pem"
65 | keyPassword := ""
66 |
67 | _, err := getTLSConfig(certPath, keyPath, keyPassword)
68 | if err == nil {
69 | t.Fatal("No error when trying to import missing certificate")
70 | }
71 | }
72 |
73 | func TestMissingKey(t *testing.T) {
74 | certPath := "test-crypt/example_cert.pem"
75 | keyPath := "doesntexist.pem"
76 | keyPassword := ""
77 |
78 | _, err := getTLSConfig(certPath, keyPath, keyPassword)
79 | if err == nil {
80 | t.Fatal("No error when trying to import missing key")
81 | }
82 | }
83 |
84 | func TestBadPassword(t *testing.T) {
85 | certPath := "test-crypt/example_cert.pem"
86 | keyPath := "test-crypt/example_key_encrypted.pem"
87 | keyPassword := "examplekeypassword11111"
88 |
89 | _, err := getTLSConfig(certPath, keyPath, keyPassword)
90 | if err == nil {
91 | t.Fatal("No error when trying to import encrypted key with bad password")
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/server/token/token.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package token
21 |
22 | import (
23 | "fmt"
24 | "time"
25 |
26 | "github.com/dgrijalva/jwt-go"
27 | "github.com/google/uuid"
28 | "github.com/rs/zerolog/log"
29 | )
30 |
31 | // Processor is a class for constructing and verifying JWTs
32 | type Processor struct {
33 | currentSecret []byte
34 | pastSecrets [][]byte
35 | duration int64
36 | }
37 |
38 | // Claims contains the information that is put into the sensor JWTs
39 | type Claims struct {
40 | Group uuid.UUID `json:"group"`
41 | Sensor uuid.UUID `json:"sensor"`
42 | jwt.StandardClaims
43 | }
44 |
45 | // NewProcessor creates a new instance capable of creating and verifying tokens
46 | // duration is the number of seconds new tokens are valid for
47 | func NewProcessor(currentSecret []byte, pastSecrets [][]byte, duration int64) Processor {
48 | return Processor{
49 | currentSecret: currentSecret,
50 | pastSecrets: pastSecrets,
51 | duration: duration,
52 | }
53 | }
54 |
55 | // NewToken creates a newly issued JWT for the given group/sensor combination
56 | func (tokenProcessor Processor) NewToken(group uuid.UUID, sensor uuid.UUID) (string, int64, int64, error) {
57 | var retVal string
58 |
59 | issuedAt := time.Now().Unix()
60 | expiresAt := issuedAt + tokenProcessor.duration
61 |
62 | claims := Claims{
63 | group,
64 | sensor,
65 | jwt.StandardClaims{
66 | ExpiresAt: expiresAt,
67 | IssuedAt: issuedAt,
68 | NotBefore: issuedAt,
69 | },
70 | }
71 |
72 | unsignedToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
73 |
74 | tokenString, err := unsignedToken.SignedString(tokenProcessor.currentSecret)
75 | if err != nil {
76 | return retVal, expiresAt, issuedAt, fmt.Errorf("Failed to create signed token: %w", err)
77 | }
78 |
79 | retVal = tokenString
80 |
81 | return retVal, expiresAt, issuedAt, nil
82 | }
83 |
84 | // VerifyToken checks the token signature and time validity.
85 | // Returns a boolean indicating if the token is valid or not.
86 | func (tokenProcessor Processor) VerifyToken(tokenString string) (bool, Claims) {
87 |
88 | isValid, claims := tryTokenVerification(tokenString, tokenProcessor.currentSecret)
89 | if isValid {
90 | return true, claims
91 | }
92 |
93 | for _, secret := range tokenProcessor.pastSecrets {
94 | isValid, claims = tryTokenVerification(tokenString, secret)
95 | if isValid {
96 | return true, claims
97 | }
98 | }
99 |
100 | log.Info().Str("token", tokenString).Msg("Invalid token provided")
101 | return false, claims
102 | }
103 |
104 | func tryTokenVerification(tokenString string, secret []byte) (bool, Claims) {
105 | var claims Claims
106 |
107 | token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
108 | if method, ok := token.Method.(*jwt.SigningMethodHMAC); !ok || method.Name != "HS256" {
109 | return nil, fmt.Errorf("Unexpected token signature algorithm '%s'", method.Name)
110 | }
111 | return secret, nil
112 | })
113 |
114 | if err != nil {
115 | log.Warn().Err(err).Msg("Token parsing failed")
116 | return false, claims
117 | }
118 |
119 | if !token.Valid {
120 | return false, claims
121 | }
122 |
123 | return true, claims
124 | }
125 |
--------------------------------------------------------------------------------
/server/token/token_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-or-later
2 |
3 | // Copyright (C) 2020 Mitchell Wasson
4 |
5 | // This file is part of Weaklayer Gateway.
6 |
7 | // Weaklayer Gateway is free software: you can redistribute it and/or modify
8 | // it under the terms of the GNU Affero General Public License as published by
9 | // the Free Software Foundation, either version 3 of the License, or
10 | // (at your option) any later version.
11 |
12 | // This program is distributed in the hope that it will be useful,
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | // GNU Affero General Public License for more details.
16 |
17 | // You should have received a copy of the GNU Affero General Public License
18 | // along with this program. If not, see .
19 |
20 | package token
21 |
22 | import (
23 | "testing"
24 | "time"
25 |
26 | "github.com/dgrijalva/jwt-go"
27 | "github.com/google/uuid"
28 | "github.com/weaklayer/gateway/common/auth"
29 | )
30 |
31 | func TestNewToken(t *testing.T) {
32 | pastSecrets := make([][]byte, 2)
33 | pastSecrets[0] = []byte("seeeeeeecret1")
34 | pastSecrets[1] = []byte("seeeeeeecret2")
35 | tokenProcessor := NewProcessor([]byte("hello"), pastSecrets, 24*60)
36 |
37 | group, err := uuid.NewRandom()
38 | if err != nil {
39 | t.Fatal("Failed to generate group identifier.", err)
40 | }
41 |
42 | sensor, err := uuid.NewRandom()
43 | if err != nil {
44 | t.Fatal("Failed to generate sensor identifier.", err)
45 | }
46 |
47 | tokenString, _, _, err := tokenProcessor.NewToken(group, sensor)
48 | if err != nil {
49 | t.Fatal("Failed to create token", err)
50 | }
51 |
52 | isTokenValid, claims := tokenProcessor.VerifyToken(tokenString)
53 |
54 | if !isTokenValid {
55 | t.Fatal("Token validation failed.", err)
56 | }
57 |
58 | if !auth.UUIDEquals(group, claims.Group) {
59 | t.Fatal("Token group identifier did not match expected value.")
60 | }
61 |
62 | if !auth.UUIDEquals(sensor, claims.Sensor) {
63 | t.Fatal("Token group identifier did not match expected value.")
64 | }
65 | }
66 |
67 | func TestPastTokenSecret(t *testing.T) {
68 | pastSecrets1 := make([][]byte, 0)
69 | tokenProcessor1 := NewProcessor([]byte("hello"), pastSecrets1, 24*60)
70 |
71 | pastSecrets2 := make([][]byte, 1)
72 | pastSecrets2[0] = []byte("hello")
73 | tokenProcessor2 := NewProcessor([]byte("hellosdfa"), pastSecrets2, 24*60)
74 |
75 | group, err := uuid.NewRandom()
76 | if err != nil {
77 | t.Fatal("Failed to generate group identifier.", err)
78 | }
79 |
80 | sensor, err := uuid.NewRandom()
81 | if err != nil {
82 | t.Fatal("Failed to generate sensor identifier.", err)
83 | }
84 |
85 | tokenString, _, _, err := tokenProcessor1.NewToken(group, sensor)
86 | if err != nil {
87 | t.Fatal("Failed to create token", err)
88 | }
89 |
90 | isTokenValid, claims := tokenProcessor2.VerifyToken(tokenString)
91 |
92 | if !isTokenValid {
93 | t.Fatal("Token validation failed.", err)
94 | }
95 |
96 | if !auth.UUIDEquals(group, claims.Group) {
97 | t.Fatal("Token group identifier did not match expected value.")
98 | }
99 |
100 | if !auth.UUIDEquals(sensor, claims.Sensor) {
101 | t.Fatal("Token group identifier did not match expected value.")
102 | }
103 | }
104 |
105 | func TestSigningAlgNone(t *testing.T) {
106 | pastSecrets := make([][]byte, 2)
107 | pastSecrets[0] = []byte("seeeeeeecret1")
108 | pastSecrets[1] = []byte("seeeeeeecret2")
109 | tokenProcessor := NewProcessor([]byte("hello"), pastSecrets, 24*60)
110 |
111 | group, err := uuid.NewRandom()
112 | if err != nil {
113 | t.Fatal("Failed to generate group identifier.", err)
114 | }
115 | sensor, err := uuid.NewRandom()
116 | if err != nil {
117 | t.Fatal("Failed to generate sensor identifier.", err)
118 | }
119 | issuedAt := time.Now().Unix()
120 | expiresAt := issuedAt + 10000
121 |
122 | claims := Claims{
123 | group,
124 | sensor,
125 | jwt.StandardClaims{
126 | ExpiresAt: expiresAt,
127 | IssuedAt: issuedAt,
128 | NotBefore: issuedAt,
129 | },
130 | }
131 |
132 | token := jwt.NewWithClaims(jwt.SigningMethodNone, claims)
133 |
134 | tokenString, err := token.SigningString()
135 | if err != nil {
136 | t.Fatal("Failed to create test token.", err)
137 | }
138 |
139 | isTokenValid, _ := tokenProcessor.VerifyToken(tokenString)
140 | if isTokenValid {
141 | t.Fatal("Token with None signing method should not be seen as valid.")
142 | }
143 | }
144 |
--------------------------------------------------------------------------------