├── .gitignore ├── .golangci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── constants └── constants.go ├── docs ├── docs.go ├── swagger.json ├── swagger.yaml └── swagger │ ├── swagger.json │ └── swagger.yaml ├── go.mod ├── go.sum ├── healthcheck └── controller.go ├── main.go ├── metrics ├── controller.go ├── controller_test.go ├── db.go ├── db_test.go ├── fixtures │ └── fixtures.go ├── metrics_suite_test.go ├── mocks │ ├── IDb.go │ ├── IService.go │ └── Sanitizer.go ├── sanitizer.go ├── sanitizer_test.go ├── service.go └── service_test.go ├── models ├── common.go ├── metrics.go └── models_suite_test.go ├── nym_directory_suite_test.go └── server ├── html ├── index.go ├── index.html └── template.go ├── server.go └── websocket ├── client.go ├── hub.go └── mocks └── IHub.go /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /scripts/deploy.sh 3 | /main 4 | /nym-directory 5 | /mixmining/nym-mixmining.db 6 | /nym-mixmining.db 7 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | concurrency: 6 3 | deadline: 1m 4 | issues-exit-code: 1 5 | tests: true 6 | skip-dirs: 7 | - vendor$ 8 | - build$ 9 | # skip-files: 10 | # - 11 | linters-settings: 12 | govet: 13 | check-shadowing: true 14 | golint: 15 | # minimal confidence for issues, default is 0.8 16 | min-confidence: 0 17 | gocyclo: 18 | min-complexity: 20 19 | maligned: 20 | suggest-new: true 21 | dupl: 22 | threshold: 100 23 | goconst: 24 | min-len: 3 25 | min-occurrences: 3 26 | depguard: 27 | list-type: whitelist 28 | include-go-root: false 29 | packages: 30 | misspell: 31 | locale: UK 32 | # ignore-words: 33 | # - foo 34 | lll: 35 | line-length: 120 36 | unused: 37 | check-exported: false 38 | unparam: 39 | check-exported: false 40 | nakedret: 41 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 42 | max-func-lines: 30 43 | prealloc: 44 | # XXX: we don't recommend using this linter before doing performance profiling. 45 | # For most programs usage of prealloc will be a premature optimization. 46 | simple: true 47 | range-loops: true # Report preallocation suggestions on range loops, true by default 48 | for-loops: false # Report preallocation suggestions on for loops, false by default 49 | gocritic: 50 | disabled-checks: 51 | - captLocal 52 | 53 | linters: 54 | enable-all: true 55 | disable-all: false 56 | # disable: 57 | # - maligned 58 | # - prealloc 59 | # - gochecknoglobals 60 | fast: false -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.14" 5 | 6 | env: 7 | - GO111MODULE=on 8 | 9 | script: 10 | - go test -v ./... -race -coverprofile=cover.out.tmp -covermode=atomic 11 | 12 | after_success: 13 | - cat cover.out.tmp | grep -v ".pb.go" > cover.out 14 | - bash <(curl https://codecov.io/bash) 15 | 16 | git: 17 | depth: 1 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nym Metrics Server 2 | 3 | A central metrics server which keeps track of the current mixing state of the network. 4 | 5 | ## Dependencies 6 | 7 | * Go 1.12 or later 8 | 9 | ## Building and running 10 | 11 | `go run main.go` builds and runs the metrics server 12 | 13 | 14 | ## Usage 15 | 16 | Nym nodes periodically send metrics information (how many Sphinx packets they've sent and received in a given interval). These metrics allow us to easily build visualizations of the network for demonstration, education, and debugging purposes during development and testnet. 17 | 18 | To see documentation of the server's capabilities, go to http://localhost:8080/swagger/index.html in your browser. All methods are runnable through the Swagger docs interface, so you can poke at the server to see what it does. 19 | 20 | ## Developing 21 | 22 | `go test ./...` will run the test suite. 23 | 24 | `swag init` rebuilds the Swagger docs if you've changed anything there. Otherwise it should not be needed. 25 | 26 | If you update any of the HTML assets, `go-assets-builder server/html/index.html -o server/html/index.go` will put it in the correct place to be built into the binary. 27 | 28 | -------------------------------------------------------------------------------- /constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | DefaultMixPort = "1789" 5 | ) 6 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT 2 | // This file was generated by swaggo/swag at 3 | // 2020-11-12 11:31:47.2571781 +0000 GMT m=+0.055881401 4 | 5 | package docs 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "strings" 11 | 12 | "github.com/alecthomas/template" 13 | "github.com/swaggo/swag" 14 | ) 15 | 16 | var doc = `{ 17 | "schemes": {{ marshal .Schemes }}, 18 | "swagger": "2.0", 19 | "info": { 20 | "description": "{{.Description}}", 21 | "title": "{{.Title}}", 22 | "termsOfService": "http://swagger.io/terms/", 23 | "contact": {}, 24 | "license": { 25 | "name": "Apache 2.0", 26 | "url": "https://github.com/nymtech/nym-metrics-server/" 27 | }, 28 | "version": "{{.Version}}" 29 | }, 30 | "host": "{{.Host}}", 31 | "basePath": "{{.BasePath}}", 32 | "paths": { 33 | "/api/healthcheck": { 34 | "get": { 35 | "description": "Returns a 200 if the metrics server is available. Good route to use for automated monitoring.", 36 | "consumes": [ 37 | "application/json" 38 | ], 39 | "produces": [ 40 | "application/json" 41 | ], 42 | "tags": [ 43 | "healthcheck" 44 | ], 45 | "summary": "Lets the metrics server tell the world it's alive.", 46 | "operationId": "healthCheck", 47 | "responses": { 48 | "200": {} 49 | } 50 | } 51 | }, 52 | "/api/metrics/mixes": { 53 | "get": { 54 | "description": "For demo and debug purposes it gives us the ability to generate useful visualisations of network traffic.", 55 | "consumes": [ 56 | "application/json" 57 | ], 58 | "produces": [ 59 | "application/json" 60 | ], 61 | "tags": [ 62 | "metrics" 63 | ], 64 | "summary": "Lists mixnode activity in the past 3 seconds", 65 | "operationId": "listMixMetrics", 66 | "responses": { 67 | "200": { 68 | "description": "OK", 69 | "schema": { 70 | "type": "array", 71 | "items": { 72 | "$ref": "#/definitions/models.MixMetric" 73 | } 74 | } 75 | }, 76 | "400": { 77 | "description": "Bad Request", 78 | "schema": { 79 | "$ref": "#/definitions/models.Error" 80 | } 81 | }, 82 | "404": { 83 | "description": "Not Found", 84 | "schema": { 85 | "$ref": "#/definitions/models.Error" 86 | } 87 | }, 88 | "500": { 89 | "description": "Internal Server Error", 90 | "schema": { 91 | "$ref": "#/definitions/models.Error" 92 | } 93 | } 94 | } 95 | }, 96 | "post": { 97 | "description": "For demo and debug purposes it gives us the ability to generate useful visualisations of network traffic.", 98 | "consumes": [ 99 | "application/json" 100 | ], 101 | "produces": [ 102 | "application/json" 103 | ], 104 | "tags": [ 105 | "metrics" 106 | ], 107 | "summary": "Create a metric detailing how many messages a given mixnode sent and received", 108 | "operationId": "createMixMetric", 109 | "parameters": [ 110 | { 111 | "description": "object", 112 | "name": "object", 113 | "in": "body", 114 | "required": true, 115 | "schema": { 116 | "$ref": "#/definitions/models.MixMetric" 117 | } 118 | } 119 | ], 120 | "responses": { 121 | "201": { 122 | "description": "Created", 123 | "schema": { 124 | "$ref": "#/definitions/models.MixMetricInterval" 125 | } 126 | }, 127 | "400": { 128 | "description": "Bad Request", 129 | "schema": { 130 | "$ref": "#/definitions/models.Error" 131 | } 132 | }, 133 | "404": { 134 | "description": "Not Found", 135 | "schema": { 136 | "$ref": "#/definitions/models.Error" 137 | } 138 | }, 139 | "500": { 140 | "description": "Internal Server Error", 141 | "schema": { 142 | "$ref": "#/definitions/models.Error" 143 | } 144 | } 145 | } 146 | } 147 | } 148 | }, 149 | "definitions": { 150 | "models.Error": { 151 | "type": "object", 152 | "properties": { 153 | "error": { 154 | "type": "string" 155 | } 156 | } 157 | }, 158 | "models.MixMetric": { 159 | "type": "object", 160 | "required": [ 161 | "pubKey", 162 | "received", 163 | "sent" 164 | ], 165 | "properties": { 166 | "pubKey": { 167 | "type": "string" 168 | }, 169 | "received": { 170 | "type": "integer" 171 | }, 172 | "sent": { 173 | "type": "object", 174 | "additionalProperties": { 175 | "type": "integer" 176 | } 177 | } 178 | } 179 | }, 180 | "models.MixMetricInterval": { 181 | "type": "object", 182 | "properties": { 183 | "nextReportIn": { 184 | "type": "integer" 185 | } 186 | } 187 | } 188 | } 189 | }` 190 | 191 | type swaggerInfo struct { 192 | Version string 193 | Host string 194 | BasePath string 195 | Schemes []string 196 | Title string 197 | Description string 198 | } 199 | 200 | // SwaggerInfo holds exported Swagger Info so clients can modify it 201 | var SwaggerInfo = swaggerInfo{ 202 | Version: "0.9.0", 203 | Host: "", 204 | BasePath: "", 205 | Schemes: []string{}, 206 | Title: "Nym Metrics API", 207 | Description: "This is a temporarily centralized metrics API to allow us to get the other Nym node types running. Its functionality will eventually be folded into other parts of Nym.", 208 | } 209 | 210 | type s struct{} 211 | 212 | func (s *s) ReadDoc() string { 213 | sInfo := SwaggerInfo 214 | sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) 215 | 216 | t, err := template.New("swagger_info").Funcs(template.FuncMap{ 217 | "marshal": func(v interface{}) string { 218 | a, _ := json.Marshal(v) 219 | return string(a) 220 | }, 221 | }).Parse(doc) 222 | if err != nil { 223 | return doc 224 | } 225 | 226 | var tpl bytes.Buffer 227 | if err := t.Execute(&tpl, sInfo); err != nil { 228 | return doc 229 | } 230 | 231 | return tpl.String() 232 | } 233 | 234 | func init() { 235 | swag.Register(swag.Name, &s{}) 236 | } 237 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a temporarily centralized metrics API to allow us to get the other Nym node types running. Its functionality will eventually be folded into other parts of Nym.", 5 | "title": "Nym Metrics API", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": {}, 8 | "license": { 9 | "name": "Apache 2.0", 10 | "url": "https://github.com/nymtech/nym-metrics-server/" 11 | }, 12 | "version": "0.9.0" 13 | }, 14 | "paths": { 15 | "/api/healthcheck": { 16 | "get": { 17 | "description": "Returns a 200 if the metrics server is available. Good route to use for automated monitoring.", 18 | "consumes": [ 19 | "application/json" 20 | ], 21 | "produces": [ 22 | "application/json" 23 | ], 24 | "tags": [ 25 | "healthcheck" 26 | ], 27 | "summary": "Lets the metrics server tell the world it's alive.", 28 | "operationId": "healthCheck", 29 | "responses": { 30 | "200": {} 31 | } 32 | } 33 | }, 34 | "/api/metrics/mixes": { 35 | "get": { 36 | "description": "For demo and debug purposes it gives us the ability to generate useful visualisations of network traffic.", 37 | "consumes": [ 38 | "application/json" 39 | ], 40 | "produces": [ 41 | "application/json" 42 | ], 43 | "tags": [ 44 | "metrics" 45 | ], 46 | "summary": "Lists mixnode activity in the past 3 seconds", 47 | "operationId": "listMixMetrics", 48 | "responses": { 49 | "200": { 50 | "description": "OK", 51 | "schema": { 52 | "type": "array", 53 | "items": { 54 | "$ref": "#/definitions/models.MixMetric" 55 | } 56 | } 57 | }, 58 | "400": { 59 | "description": "Bad Request", 60 | "schema": { 61 | "$ref": "#/definitions/models.Error" 62 | } 63 | }, 64 | "404": { 65 | "description": "Not Found", 66 | "schema": { 67 | "$ref": "#/definitions/models.Error" 68 | } 69 | }, 70 | "500": { 71 | "description": "Internal Server Error", 72 | "schema": { 73 | "$ref": "#/definitions/models.Error" 74 | } 75 | } 76 | } 77 | }, 78 | "post": { 79 | "description": "For demo and debug purposes it gives us the ability to generate useful visualisations of network traffic.", 80 | "consumes": [ 81 | "application/json" 82 | ], 83 | "produces": [ 84 | "application/json" 85 | ], 86 | "tags": [ 87 | "metrics" 88 | ], 89 | "summary": "Create a metric detailing how many messages a given mixnode sent and received", 90 | "operationId": "createMixMetric", 91 | "parameters": [ 92 | { 93 | "description": "object", 94 | "name": "object", 95 | "in": "body", 96 | "required": true, 97 | "schema": { 98 | "$ref": "#/definitions/models.MixMetric" 99 | } 100 | } 101 | ], 102 | "responses": { 103 | "201": { 104 | "description": "Created", 105 | "schema": { 106 | "$ref": "#/definitions/models.MixMetricInterval" 107 | } 108 | }, 109 | "400": { 110 | "description": "Bad Request", 111 | "schema": { 112 | "$ref": "#/definitions/models.Error" 113 | } 114 | }, 115 | "404": { 116 | "description": "Not Found", 117 | "schema": { 118 | "$ref": "#/definitions/models.Error" 119 | } 120 | }, 121 | "500": { 122 | "description": "Internal Server Error", 123 | "schema": { 124 | "$ref": "#/definitions/models.Error" 125 | } 126 | } 127 | } 128 | } 129 | } 130 | }, 131 | "definitions": { 132 | "models.Error": { 133 | "type": "object", 134 | "properties": { 135 | "error": { 136 | "type": "string" 137 | } 138 | } 139 | }, 140 | "models.MixMetric": { 141 | "type": "object", 142 | "required": [ 143 | "pubKey", 144 | "received", 145 | "sent" 146 | ], 147 | "properties": { 148 | "pubKey": { 149 | "type": "string" 150 | }, 151 | "received": { 152 | "type": "integer" 153 | }, 154 | "sent": { 155 | "type": "object", 156 | "additionalProperties": { 157 | "type": "integer" 158 | } 159 | } 160 | } 161 | }, 162 | "models.MixMetricInterval": { 163 | "type": "object", 164 | "properties": { 165 | "nextReportIn": { 166 | "type": "integer" 167 | } 168 | } 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | models.Error: 3 | properties: 4 | error: 5 | type: string 6 | type: object 7 | models.MixMetric: 8 | properties: 9 | pubKey: 10 | type: string 11 | received: 12 | type: integer 13 | sent: 14 | additionalProperties: 15 | type: integer 16 | type: object 17 | required: 18 | - pubKey 19 | - received 20 | - sent 21 | type: object 22 | models.MixMetricInterval: 23 | properties: 24 | nextReportIn: 25 | type: integer 26 | type: object 27 | info: 28 | contact: {} 29 | description: This is a temporarily centralized metrics API to allow us to get the 30 | other Nym node types running. Its functionality will eventually be folded into 31 | other parts of Nym. 32 | license: 33 | name: Apache 2.0 34 | url: https://github.com/nymtech/nym-metrics-server/ 35 | termsOfService: http://swagger.io/terms/ 36 | title: Nym Metrics API 37 | version: 0.9.0 38 | paths: 39 | /api/healthcheck: 40 | get: 41 | consumes: 42 | - application/json 43 | description: Returns a 200 if the metrics server is available. Good route to 44 | use for automated monitoring. 45 | operationId: healthCheck 46 | produces: 47 | - application/json 48 | responses: 49 | "200": {} 50 | summary: Lets the metrics server tell the world it's alive. 51 | tags: 52 | - healthcheck 53 | /api/metrics/mixes: 54 | get: 55 | consumes: 56 | - application/json 57 | description: For demo and debug purposes it gives us the ability to generate 58 | useful visualisations of network traffic. 59 | operationId: listMixMetrics 60 | produces: 61 | - application/json 62 | responses: 63 | "200": 64 | description: OK 65 | schema: 66 | items: 67 | $ref: '#/definitions/models.MixMetric' 68 | type: array 69 | "400": 70 | description: Bad Request 71 | schema: 72 | $ref: '#/definitions/models.Error' 73 | "404": 74 | description: Not Found 75 | schema: 76 | $ref: '#/definitions/models.Error' 77 | "500": 78 | description: Internal Server Error 79 | schema: 80 | $ref: '#/definitions/models.Error' 81 | summary: Lists mixnode activity in the past 3 seconds 82 | tags: 83 | - metrics 84 | post: 85 | consumes: 86 | - application/json 87 | description: For demo and debug purposes it gives us the ability to generate 88 | useful visualisations of network traffic. 89 | operationId: createMixMetric 90 | parameters: 91 | - description: object 92 | in: body 93 | name: object 94 | required: true 95 | schema: 96 | $ref: '#/definitions/models.MixMetric' 97 | produces: 98 | - application/json 99 | responses: 100 | "201": 101 | description: Created 102 | schema: 103 | $ref: '#/definitions/models.MixMetricInterval' 104 | "400": 105 | description: Bad Request 106 | schema: 107 | $ref: '#/definitions/models.Error' 108 | "404": 109 | description: Not Found 110 | schema: 111 | $ref: '#/definitions/models.Error' 112 | "500": 113 | description: Internal Server Error 114 | schema: 115 | $ref: '#/definitions/models.Error' 116 | summary: Create a metric detailing how many messages a given mixnode sent and 117 | received 118 | tags: 119 | - metrics 120 | swagger: "2.0" 121 | -------------------------------------------------------------------------------- /docs/swagger/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a temporarily centralized directory/PKI/metrics API to allow us to get the other Nym node types running. Its functionality will eventually be folded into other parts of Nym.", 5 | "title": "Nym Directory API", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": {}, 8 | "license": { 9 | "name": "Apache 2.0", 10 | "url": "https://github.com/nymtech/nym-directory/license" 11 | }, 12 | "version": "0.0.4" 13 | }, 14 | "paths": { 15 | "/api/healthcheck": { 16 | "get": { 17 | "description": "Returns a 200 if the directory server is available. Good route to use for automated monitoring.", 18 | "consumes": [ 19 | "application/json" 20 | ], 21 | "produces": [ 22 | "application/json" 23 | ], 24 | "tags": [ 25 | "healthcheck" 26 | ], 27 | "summary": "Lets the directory server tell the world it's alive.", 28 | "operationId": "healthCheck", 29 | "responses": { 30 | "200": {} 31 | } 32 | } 33 | }, 34 | "/api/metrics/mixes": { 35 | "get": { 36 | "description": "For demo and debug purposes it gives us the ability to generate useful visualisations of network traffic.", 37 | "consumes": [ 38 | "application/json" 39 | ], 40 | "produces": [ 41 | "application/json" 42 | ], 43 | "tags": [ 44 | "metrics" 45 | ], 46 | "summary": "Lists mixnode activity in the past 3 seconds", 47 | "operationId": "listMixMetrics", 48 | "responses": { 49 | "200": { 50 | "description": "OK", 51 | "schema": { 52 | "type": "array", 53 | "items": { 54 | "$ref": "#/definitions/models.MixMetric" 55 | } 56 | } 57 | }, 58 | "400": { 59 | "description": "Bad Request", 60 | "schema": { 61 | "type": "object", 62 | "$ref": "#/definitions/models.Error" 63 | } 64 | }, 65 | "404": { 66 | "description": "Not Found", 67 | "schema": { 68 | "type": "object", 69 | "$ref": "#/definitions/models.Error" 70 | } 71 | }, 72 | "500": { 73 | "description": "Internal Server Error", 74 | "schema": { 75 | "type": "object", 76 | "$ref": "#/definitions/models.Error" 77 | } 78 | } 79 | } 80 | }, 81 | "post": { 82 | "description": "For demo and debug purposes it gives us the ability to generate useful visualisations of network traffic.", 83 | "consumes": [ 84 | "application/json" 85 | ], 86 | "produces": [ 87 | "application/json" 88 | ], 89 | "tags": [ 90 | "metrics" 91 | ], 92 | "summary": "Create a metric detailing how many messages a given mixnode sent and received", 93 | "operationId": "createMixMetric", 94 | "parameters": [ 95 | { 96 | "description": "object", 97 | "name": "object", 98 | "in": "body", 99 | "required": true, 100 | "schema": { 101 | "type": "object", 102 | "$ref": "#/definitions/models.MixMetric" 103 | } 104 | } 105 | ], 106 | "responses": { 107 | "201": {}, 108 | "400": { 109 | "description": "Bad Request", 110 | "schema": { 111 | "type": "object", 112 | "$ref": "#/definitions/models.Error" 113 | } 114 | }, 115 | "404": { 116 | "description": "Not Found", 117 | "schema": { 118 | "type": "object", 119 | "$ref": "#/definitions/models.Error" 120 | } 121 | }, 122 | "500": { 123 | "description": "Internal Server Error", 124 | "schema": { 125 | "type": "object", 126 | "$ref": "#/definitions/models.Error" 127 | } 128 | } 129 | } 130 | } 131 | }, 132 | "/api/presence/allow": { 133 | "post": { 134 | "description": "Sometimes when a node isn't working we need to temporarily remove it. This allows us to re-enable it once it's working again.", 135 | "consumes": [ 136 | "application/json" 137 | ], 138 | "produces": [ 139 | "application/json" 140 | ], 141 | "tags": [ 142 | "presence" 143 | ], 144 | "summary": "Removes a disallowed node from the disallowed nodes list", 145 | "operationId": "allow", 146 | "parameters": [ 147 | { 148 | "description": "object", 149 | "name": "object", 150 | "in": "body", 151 | "required": true, 152 | "schema": { 153 | "type": "object", 154 | "$ref": "#/definitions/models.MixNodeID" 155 | } 156 | } 157 | ], 158 | "responses": { 159 | "200": {}, 160 | "400": { 161 | "description": "Bad Request", 162 | "schema": { 163 | "type": "object", 164 | "$ref": "#/definitions/models.Error" 165 | } 166 | }, 167 | "404": { 168 | "description": "Not Found", 169 | "schema": { 170 | "type": "object", 171 | "$ref": "#/definitions/models.Error" 172 | } 173 | }, 174 | "500": { 175 | "description": "Internal Server Error", 176 | "schema": { 177 | "type": "object", 178 | "$ref": "#/definitions/models.Error" 179 | } 180 | } 181 | } 182 | } 183 | }, 184 | "/api/presence/coconodes": { 185 | "post": { 186 | "description": "Nym Coconut nodes can ping this method to let the directory server know they're up. We can then use this info to create topologies of the overall Nym network.", 187 | "consumes": [ 188 | "application/json" 189 | ], 190 | "produces": [ 191 | "application/json" 192 | ], 193 | "tags": [ 194 | "presence" 195 | ], 196 | "summary": "Lets a coconut node tell the directory server it's alive", 197 | "operationId": "addCocoNode", 198 | "parameters": [ 199 | { 200 | "description": "object", 201 | "name": "object", 202 | "in": "body", 203 | "required": true, 204 | "schema": { 205 | "type": "object", 206 | "$ref": "#/definitions/models.CocoHostInfo" 207 | } 208 | } 209 | ], 210 | "responses": { 211 | "201": {}, 212 | "400": { 213 | "description": "Bad Request", 214 | "schema": { 215 | "type": "object", 216 | "$ref": "#/definitions/models.Error" 217 | } 218 | }, 219 | "404": { 220 | "description": "Not Found", 221 | "schema": { 222 | "type": "object", 223 | "$ref": "#/definitions/models.Error" 224 | } 225 | }, 226 | "500": { 227 | "description": "Internal Server Error", 228 | "schema": { 229 | "type": "object", 230 | "$ref": "#/definitions/models.Error" 231 | } 232 | } 233 | } 234 | } 235 | }, 236 | "/api/presence/disallow": { 237 | "post": { 238 | "description": "Sometimes when a node isn't working we need to temporarily remove it from use so that it doesn't mess up QoS for the whole network.", 239 | "consumes": [ 240 | "application/json" 241 | ], 242 | "produces": [ 243 | "application/json" 244 | ], 245 | "tags": [ 246 | "presence" 247 | ], 248 | "summary": "Takes a node out of the regular topology and puts it in the disallowed nodes list", 249 | "operationId": "disallow", 250 | "parameters": [ 251 | { 252 | "description": "object", 253 | "name": "object", 254 | "in": "body", 255 | "required": true, 256 | "schema": { 257 | "type": "object", 258 | "$ref": "#/definitions/models.MixNodeID" 259 | } 260 | } 261 | ], 262 | "responses": { 263 | "201": {}, 264 | "400": { 265 | "description": "Bad Request", 266 | "schema": { 267 | "type": "object", 268 | "$ref": "#/definitions/models.Error" 269 | } 270 | }, 271 | "404": { 272 | "description": "Not Found", 273 | "schema": { 274 | "type": "object", 275 | "$ref": "#/definitions/models.Error" 276 | } 277 | }, 278 | "500": { 279 | "description": "Internal Server Error", 280 | "schema": { 281 | "type": "object", 282 | "$ref": "#/definitions/models.Error" 283 | } 284 | } 285 | } 286 | } 287 | }, 288 | "/api/presence/disallowed": { 289 | "get": { 290 | "description": "Sometimes we need to take mixnodes out of the network for repair. This shows which ones are currently disallowed.", 291 | "consumes": [ 292 | "application/json" 293 | ], 294 | "produces": [ 295 | "application/json" 296 | ], 297 | "tags": [ 298 | "presence" 299 | ], 300 | "summary": "Lists Nym mixnodes that are currently disallowed", 301 | "operationId": "disallowed", 302 | "responses": { 303 | "200": { 304 | "description": "OK", 305 | "schema": { 306 | "type": "array", 307 | "items": { 308 | "$ref": "#/definitions/models.MixNodePresence" 309 | } 310 | } 311 | }, 312 | "400": { 313 | "description": "Bad Request", 314 | "schema": { 315 | "type": "object", 316 | "$ref": "#/definitions/models.Error" 317 | } 318 | }, 319 | "404": { 320 | "description": "Not Found", 321 | "schema": { 322 | "type": "object", 323 | "$ref": "#/definitions/models.Error" 324 | } 325 | }, 326 | "500": { 327 | "description": "Internal Server Error", 328 | "schema": { 329 | "type": "object", 330 | "$ref": "#/definitions/models.Error" 331 | } 332 | } 333 | } 334 | } 335 | }, 336 | "/api/presence/gateways": { 337 | "post": { 338 | "description": "Nym mix gateways can ping this method to let the directory server know they're up. We can then use this info to create topologies of the overall Nym network.", 339 | "consumes": [ 340 | "application/json" 341 | ], 342 | "produces": [ 343 | "application/json" 344 | ], 345 | "tags": [ 346 | "presence" 347 | ], 348 | "summary": "Lets a gateway tell the directory server it's alive", 349 | "operationId": "addGateway", 350 | "parameters": [ 351 | { 352 | "description": "object", 353 | "name": "object", 354 | "in": "body", 355 | "required": true, 356 | "schema": { 357 | "type": "object", 358 | "$ref": "#/definitions/models.GatewayHostInfo" 359 | } 360 | } 361 | ], 362 | "responses": { 363 | "201": {}, 364 | "400": { 365 | "description": "Bad Request", 366 | "schema": { 367 | "type": "object", 368 | "$ref": "#/definitions/models.Error" 369 | } 370 | }, 371 | "404": { 372 | "description": "Not Found", 373 | "schema": { 374 | "type": "object", 375 | "$ref": "#/definitions/models.Error" 376 | } 377 | }, 378 | "500": { 379 | "description": "Internal Server Error", 380 | "schema": { 381 | "type": "object", 382 | "$ref": "#/definitions/models.Error" 383 | } 384 | } 385 | } 386 | } 387 | }, 388 | "/api/presence/mixnodes": { 389 | "post": { 390 | "description": "Nym mixnodes can ping this method to let the directory server know they're up. We can then use this info to create topologies of the overall Nym network.", 391 | "consumes": [ 392 | "application/json" 393 | ], 394 | "produces": [ 395 | "application/json" 396 | ], 397 | "tags": [ 398 | "presence" 399 | ], 400 | "summary": "Lets mixnode a node tell the directory server it's alive", 401 | "operationId": "addMixNode", 402 | "parameters": [ 403 | { 404 | "description": "object", 405 | "name": "object", 406 | "in": "body", 407 | "required": true, 408 | "schema": { 409 | "type": "object", 410 | "$ref": "#/definitions/models.MixHostInfo" 411 | } 412 | } 413 | ], 414 | "responses": { 415 | "201": {}, 416 | "400": { 417 | "description": "Bad Request", 418 | "schema": { 419 | "type": "object", 420 | "$ref": "#/definitions/models.Error" 421 | } 422 | }, 423 | "404": { 424 | "description": "Not Found", 425 | "schema": { 426 | "type": "object", 427 | "$ref": "#/definitions/models.Error" 428 | } 429 | }, 430 | "500": { 431 | "description": "Internal Server Error", 432 | "schema": { 433 | "type": "object", 434 | "$ref": "#/definitions/models.Error" 435 | } 436 | } 437 | } 438 | } 439 | }, 440 | "/api/presence/topology": { 441 | "get": { 442 | "description": "Nym nodes periodically ping the directory server to register that they're alive. This method provides a list of nodes which have been most recently seen.", 443 | "consumes": [ 444 | "application/json" 445 | ], 446 | "produces": [ 447 | "application/json" 448 | ], 449 | "tags": [ 450 | "presence" 451 | ], 452 | "summary": "Lists which Nym mixnodes, providers, gateways, and coconodes are alive", 453 | "operationId": "topology", 454 | "responses": { 455 | "200": { 456 | "description": "OK", 457 | "schema": { 458 | "type": "object", 459 | "$ref": "#/definitions/models.Topology" 460 | } 461 | }, 462 | "400": { 463 | "description": "Bad Request", 464 | "schema": { 465 | "type": "object", 466 | "$ref": "#/definitions/models.Error" 467 | } 468 | }, 469 | "404": { 470 | "description": "Not Found", 471 | "schema": { 472 | "type": "object", 473 | "$ref": "#/definitions/models.Error" 474 | } 475 | }, 476 | "500": { 477 | "description": "Internal Server Error", 478 | "schema": { 479 | "type": "object", 480 | "$ref": "#/definitions/models.Error" 481 | } 482 | } 483 | } 484 | } 485 | } 486 | }, 487 | "definitions": { 488 | "models.CocoHostInfo": { 489 | "type": "object", 490 | "required": [ 491 | "type", 492 | "pubKey", 493 | "version" 494 | ], 495 | "properties": { 496 | "host": { 497 | "type": "string" 498 | }, 499 | "location": { 500 | "type": "string" 501 | }, 502 | "pubKey": { 503 | "type": "string" 504 | }, 505 | "type": { 506 | "type": "string" 507 | }, 508 | "version": { 509 | "type": "string" 510 | } 511 | } 512 | }, 513 | "models.CocoPresence": { 514 | "type": "object", 515 | "required": [ 516 | "pubKey", 517 | "version", 518 | "type", 519 | "lastSeen" 520 | ], 521 | "properties": { 522 | "host": { 523 | "type": "string" 524 | }, 525 | "lastSeen": { 526 | "type": "integer" 527 | }, 528 | "location": { 529 | "type": "string" 530 | }, 531 | "pubKey": { 532 | "type": "string" 533 | }, 534 | "type": { 535 | "type": "string" 536 | }, 537 | "version": { 538 | "type": "string" 539 | } 540 | } 541 | }, 542 | "models.Error": { 543 | "type": "object", 544 | "properties": { 545 | "error": { 546 | "type": "string" 547 | } 548 | } 549 | }, 550 | "models.GatewayHostInfo": { 551 | "type": "object", 552 | "required": [ 553 | "pubKey", 554 | "version" 555 | ], 556 | "properties": { 557 | "clientListener": { 558 | "type": "string" 559 | }, 560 | "location": { 561 | "type": "string" 562 | }, 563 | "mixnetListener": { 564 | "type": "string" 565 | }, 566 | "pubKey": { 567 | "type": "string" 568 | }, 569 | "registeredClients": { 570 | "type": "array", 571 | "items": { 572 | "$ref": "#/definitions/models.RegisteredClient" 573 | } 574 | }, 575 | "version": { 576 | "type": "string" 577 | } 578 | } 579 | }, 580 | "models.GatewayPresence": { 581 | "type": "object", 582 | "required": [ 583 | "lastSeen", 584 | "pubKey", 585 | "version" 586 | ], 587 | "properties": { 588 | "clientListener": { 589 | "type": "string" 590 | }, 591 | "lastSeen": { 592 | "type": "integer" 593 | }, 594 | "location": { 595 | "type": "string" 596 | }, 597 | "mixnetListener": { 598 | "type": "string" 599 | }, 600 | "pubKey": { 601 | "type": "string" 602 | }, 603 | "registeredClients": { 604 | "type": "array", 605 | "items": { 606 | "$ref": "#/definitions/models.RegisteredClient" 607 | } 608 | }, 609 | "version": { 610 | "type": "string" 611 | } 612 | } 613 | }, 614 | "models.MixHostInfo": { 615 | "type": "object", 616 | "required": [ 617 | "pubKey", 618 | "version", 619 | "layer" 620 | ], 621 | "properties": { 622 | "host": { 623 | "type": "string" 624 | }, 625 | "layer": { 626 | "type": "integer" 627 | }, 628 | "location": { 629 | "type": "string" 630 | }, 631 | "pubKey": { 632 | "type": "string" 633 | }, 634 | "version": { 635 | "type": "string" 636 | } 637 | } 638 | }, 639 | "models.MixMetric": { 640 | "type": "object", 641 | "required": [ 642 | "received", 643 | "pubKey" 644 | ], 645 | "properties": { 646 | "pubKey": { 647 | "type": "string" 648 | }, 649 | "received": { 650 | "type": "integer" 651 | }, 652 | "sent": { 653 | "type": "object", 654 | "required": [ 655 | "sent" 656 | ] 657 | } 658 | } 659 | }, 660 | "models.MixNodeID": { 661 | "type": "object", 662 | "properties": { 663 | "pubKey": { 664 | "type": "string" 665 | } 666 | } 667 | }, 668 | "models.MixNodePresence": { 669 | "type": "object", 670 | "required": [ 671 | "layer", 672 | "lastSeen", 673 | "pubKey", 674 | "version" 675 | ], 676 | "properties": { 677 | "host": { 678 | "type": "string" 679 | }, 680 | "lastSeen": { 681 | "type": "integer" 682 | }, 683 | "layer": { 684 | "type": "integer" 685 | }, 686 | "location": { 687 | "type": "string" 688 | }, 689 | "pubKey": { 690 | "type": "string" 691 | }, 692 | "version": { 693 | "type": "string" 694 | } 695 | } 696 | }, 697 | "models.MixProviderPresence": { 698 | "type": "object", 699 | "required": [ 700 | "pubKey", 701 | "version", 702 | "lastSeen" 703 | ], 704 | "properties": { 705 | "clientListener": { 706 | "type": "string" 707 | }, 708 | "lastSeen": { 709 | "type": "integer" 710 | }, 711 | "location": { 712 | "type": "string" 713 | }, 714 | "mixnetListener": { 715 | "type": "string" 716 | }, 717 | "pubKey": { 718 | "type": "string" 719 | }, 720 | "registeredClients": { 721 | "type": "array", 722 | "items": { 723 | "$ref": "#/definitions/models.RegisteredClient" 724 | } 725 | }, 726 | "version": { 727 | "type": "string" 728 | } 729 | } 730 | }, 731 | "models.RegisteredClient": { 732 | "type": "object", 733 | "required": [ 734 | "pubKey" 735 | ], 736 | "properties": { 737 | "pubKey": { 738 | "type": "string" 739 | } 740 | } 741 | }, 742 | "models.Topology": { 743 | "type": "object", 744 | "properties": { 745 | "cocoNodes": { 746 | "type": "array", 747 | "items": { 748 | "$ref": "#/definitions/models.CocoPresence" 749 | } 750 | }, 751 | "gatewayNodes": { 752 | "type": "array", 753 | "items": { 754 | "$ref": "#/definitions/models.GatewayPresence" 755 | } 756 | }, 757 | "mixNodes": { 758 | "type": "array", 759 | "items": { 760 | "$ref": "#/definitions/models.MixNodePresence" 761 | } 762 | }, 763 | "mixProviderNodes": { 764 | "type": "array", 765 | "items": { 766 | "$ref": "#/definitions/models.MixProviderPresence" 767 | } 768 | } 769 | } 770 | } 771 | } 772 | } -------------------------------------------------------------------------------- /docs/swagger/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | models.CocoHostInfo: 3 | properties: 4 | host: 5 | type: string 6 | location: 7 | type: string 8 | pubKey: 9 | type: string 10 | type: 11 | type: string 12 | version: 13 | type: string 14 | required: 15 | - type 16 | - pubKey 17 | - version 18 | type: object 19 | models.CocoPresence: 20 | properties: 21 | host: 22 | type: string 23 | lastSeen: 24 | type: integer 25 | location: 26 | type: string 27 | pubKey: 28 | type: string 29 | type: 30 | type: string 31 | version: 32 | type: string 33 | required: 34 | - pubKey 35 | - version 36 | - type 37 | - lastSeen 38 | type: object 39 | models.Error: 40 | properties: 41 | error: 42 | type: string 43 | type: object 44 | models.GatewayHostInfo: 45 | properties: 46 | clientListener: 47 | type: string 48 | location: 49 | type: string 50 | mixnetListener: 51 | type: string 52 | pubKey: 53 | type: string 54 | registeredClients: 55 | items: 56 | $ref: '#/definitions/models.RegisteredClient' 57 | type: array 58 | version: 59 | type: string 60 | required: 61 | - pubKey 62 | - version 63 | type: object 64 | models.GatewayPresence: 65 | properties: 66 | clientListener: 67 | type: string 68 | lastSeen: 69 | type: integer 70 | location: 71 | type: string 72 | mixnetListener: 73 | type: string 74 | pubKey: 75 | type: string 76 | registeredClients: 77 | items: 78 | $ref: '#/definitions/models.RegisteredClient' 79 | type: array 80 | version: 81 | type: string 82 | required: 83 | - lastSeen 84 | - pubKey 85 | - version 86 | type: object 87 | models.MixHostInfo: 88 | properties: 89 | host: 90 | type: string 91 | layer: 92 | type: integer 93 | location: 94 | type: string 95 | pubKey: 96 | type: string 97 | version: 98 | type: string 99 | required: 100 | - pubKey 101 | - version 102 | - layer 103 | type: object 104 | models.MixMetric: 105 | properties: 106 | pubKey: 107 | type: string 108 | received: 109 | type: integer 110 | sent: 111 | required: 112 | - sent 113 | type: object 114 | required: 115 | - received 116 | - pubKey 117 | type: object 118 | models.MixNodeID: 119 | properties: 120 | pubKey: 121 | type: string 122 | type: object 123 | models.MixNodePresence: 124 | properties: 125 | host: 126 | type: string 127 | lastSeen: 128 | type: integer 129 | layer: 130 | type: integer 131 | location: 132 | type: string 133 | pubKey: 134 | type: string 135 | version: 136 | type: string 137 | required: 138 | - layer 139 | - lastSeen 140 | - pubKey 141 | - version 142 | type: object 143 | models.MixProviderPresence: 144 | properties: 145 | clientListener: 146 | type: string 147 | lastSeen: 148 | type: integer 149 | location: 150 | type: string 151 | mixnetListener: 152 | type: string 153 | pubKey: 154 | type: string 155 | registeredClients: 156 | items: 157 | $ref: '#/definitions/models.RegisteredClient' 158 | type: array 159 | version: 160 | type: string 161 | required: 162 | - pubKey 163 | - version 164 | - lastSeen 165 | type: object 166 | models.RegisteredClient: 167 | properties: 168 | pubKey: 169 | type: string 170 | required: 171 | - pubKey 172 | type: object 173 | models.Topology: 174 | properties: 175 | cocoNodes: 176 | items: 177 | $ref: '#/definitions/models.CocoPresence' 178 | type: array 179 | gatewayNodes: 180 | items: 181 | $ref: '#/definitions/models.GatewayPresence' 182 | type: array 183 | mixNodes: 184 | items: 185 | $ref: '#/definitions/models.MixNodePresence' 186 | type: array 187 | mixProviderNodes: 188 | items: 189 | $ref: '#/definitions/models.MixProviderPresence' 190 | type: array 191 | type: object 192 | info: 193 | contact: {} 194 | description: This is a temporarily centralized directory/PKI/metrics API to allow 195 | us to get the other Nym node types running. Its functionality will eventually 196 | be folded into other parts of Nym. 197 | license: 198 | name: Apache 2.0 199 | url: https://github.com/nymtech/nym-directory/license 200 | termsOfService: http://swagger.io/terms/ 201 | title: Nym Directory API 202 | version: 0.0.4 203 | paths: 204 | /api/healthcheck: 205 | get: 206 | consumes: 207 | - application/json 208 | description: Returns a 200 if the directory server is available. Good route 209 | to use for automated monitoring. 210 | operationId: healthCheck 211 | produces: 212 | - application/json 213 | responses: 214 | "200": {} 215 | summary: Lets the directory server tell the world it's alive. 216 | tags: 217 | - healthcheck 218 | /api/metrics/mixes: 219 | get: 220 | consumes: 221 | - application/json 222 | description: For demo and debug purposes it gives us the ability to generate 223 | useful visualisations of network traffic. 224 | operationId: listMixMetrics 225 | produces: 226 | - application/json 227 | responses: 228 | "200": 229 | description: OK 230 | schema: 231 | items: 232 | $ref: '#/definitions/models.MixMetric' 233 | type: array 234 | "400": 235 | description: Bad Request 236 | schema: 237 | $ref: '#/definitions/models.Error' 238 | type: object 239 | "404": 240 | description: Not Found 241 | schema: 242 | $ref: '#/definitions/models.Error' 243 | type: object 244 | "500": 245 | description: Internal Server Error 246 | schema: 247 | $ref: '#/definitions/models.Error' 248 | type: object 249 | summary: Lists mixnode activity in the past 3 seconds 250 | tags: 251 | - metrics 252 | post: 253 | consumes: 254 | - application/json 255 | description: For demo and debug purposes it gives us the ability to generate 256 | useful visualisations of network traffic. 257 | operationId: createMixMetric 258 | parameters: 259 | - description: object 260 | in: body 261 | name: object 262 | required: true 263 | schema: 264 | $ref: '#/definitions/models.MixMetric' 265 | type: object 266 | produces: 267 | - application/json 268 | responses: 269 | "201": {} 270 | "400": 271 | description: Bad Request 272 | schema: 273 | $ref: '#/definitions/models.Error' 274 | type: object 275 | "404": 276 | description: Not Found 277 | schema: 278 | $ref: '#/definitions/models.Error' 279 | type: object 280 | "500": 281 | description: Internal Server Error 282 | schema: 283 | $ref: '#/definitions/models.Error' 284 | type: object 285 | summary: Create a metric detailing how many messages a given mixnode sent and 286 | received 287 | tags: 288 | - metrics 289 | /api/presence/allow: 290 | post: 291 | consumes: 292 | - application/json 293 | description: Sometimes when a node isn't working we need to temporarily remove 294 | it. This allows us to re-enable it once it's working again. 295 | operationId: allow 296 | parameters: 297 | - description: object 298 | in: body 299 | name: object 300 | required: true 301 | schema: 302 | $ref: '#/definitions/models.MixNodeID' 303 | type: object 304 | produces: 305 | - application/json 306 | responses: 307 | "200": {} 308 | "400": 309 | description: Bad Request 310 | schema: 311 | $ref: '#/definitions/models.Error' 312 | type: object 313 | "404": 314 | description: Not Found 315 | schema: 316 | $ref: '#/definitions/models.Error' 317 | type: object 318 | "500": 319 | description: Internal Server Error 320 | schema: 321 | $ref: '#/definitions/models.Error' 322 | type: object 323 | summary: Removes a disallowed node from the disallowed nodes list 324 | tags: 325 | - presence 326 | /api/presence/coconodes: 327 | post: 328 | consumes: 329 | - application/json 330 | description: Nym Coconut nodes can ping this method to let the directory server 331 | know they're up. We can then use this info to create topologies of the overall 332 | Nym network. 333 | operationId: addCocoNode 334 | parameters: 335 | - description: object 336 | in: body 337 | name: object 338 | required: true 339 | schema: 340 | $ref: '#/definitions/models.CocoHostInfo' 341 | type: object 342 | produces: 343 | - application/json 344 | responses: 345 | "201": {} 346 | "400": 347 | description: Bad Request 348 | schema: 349 | $ref: '#/definitions/models.Error' 350 | type: object 351 | "404": 352 | description: Not Found 353 | schema: 354 | $ref: '#/definitions/models.Error' 355 | type: object 356 | "500": 357 | description: Internal Server Error 358 | schema: 359 | $ref: '#/definitions/models.Error' 360 | type: object 361 | summary: Lets a coconut node tell the directory server it's alive 362 | tags: 363 | - presence 364 | /api/presence/disallow: 365 | post: 366 | consumes: 367 | - application/json 368 | description: Sometimes when a node isn't working we need to temporarily remove 369 | it from use so that it doesn't mess up QoS for the whole network. 370 | operationId: disallow 371 | parameters: 372 | - description: object 373 | in: body 374 | name: object 375 | required: true 376 | schema: 377 | $ref: '#/definitions/models.MixNodeID' 378 | type: object 379 | produces: 380 | - application/json 381 | responses: 382 | "201": {} 383 | "400": 384 | description: Bad Request 385 | schema: 386 | $ref: '#/definitions/models.Error' 387 | type: object 388 | "404": 389 | description: Not Found 390 | schema: 391 | $ref: '#/definitions/models.Error' 392 | type: object 393 | "500": 394 | description: Internal Server Error 395 | schema: 396 | $ref: '#/definitions/models.Error' 397 | type: object 398 | summary: Takes a node out of the regular topology and puts it in the disallowed 399 | nodes list 400 | tags: 401 | - presence 402 | /api/presence/disallowed: 403 | get: 404 | consumes: 405 | - application/json 406 | description: Sometimes we need to take mixnodes out of the network for repair. 407 | This shows which ones are currently disallowed. 408 | operationId: disallowed 409 | produces: 410 | - application/json 411 | responses: 412 | "200": 413 | description: OK 414 | schema: 415 | items: 416 | $ref: '#/definitions/models.MixNodePresence' 417 | type: array 418 | "400": 419 | description: Bad Request 420 | schema: 421 | $ref: '#/definitions/models.Error' 422 | type: object 423 | "404": 424 | description: Not Found 425 | schema: 426 | $ref: '#/definitions/models.Error' 427 | type: object 428 | "500": 429 | description: Internal Server Error 430 | schema: 431 | $ref: '#/definitions/models.Error' 432 | type: object 433 | summary: Lists Nym mixnodes that are currently disallowed 434 | tags: 435 | - presence 436 | /api/presence/gateways: 437 | post: 438 | consumes: 439 | - application/json 440 | description: Nym mix gateways can ping this method to let the directory server 441 | know they're up. We can then use this info to create topologies of the overall 442 | Nym network. 443 | operationId: addGateway 444 | parameters: 445 | - description: object 446 | in: body 447 | name: object 448 | required: true 449 | schema: 450 | $ref: '#/definitions/models.GatewayHostInfo' 451 | type: object 452 | produces: 453 | - application/json 454 | responses: 455 | "201": {} 456 | "400": 457 | description: Bad Request 458 | schema: 459 | $ref: '#/definitions/models.Error' 460 | type: object 461 | "404": 462 | description: Not Found 463 | schema: 464 | $ref: '#/definitions/models.Error' 465 | type: object 466 | "500": 467 | description: Internal Server Error 468 | schema: 469 | $ref: '#/definitions/models.Error' 470 | type: object 471 | summary: Lets a gateway tell the directory server it's alive 472 | tags: 473 | - presence 474 | /api/presence/mixnodes: 475 | post: 476 | consumes: 477 | - application/json 478 | description: Nym mixnodes can ping this method to let the directory server know 479 | they're up. We can then use this info to create topologies of the overall 480 | Nym network. 481 | operationId: addMixNode 482 | parameters: 483 | - description: object 484 | in: body 485 | name: object 486 | required: true 487 | schema: 488 | $ref: '#/definitions/models.MixHostInfo' 489 | type: object 490 | produces: 491 | - application/json 492 | responses: 493 | "201": {} 494 | "400": 495 | description: Bad Request 496 | schema: 497 | $ref: '#/definitions/models.Error' 498 | type: object 499 | "404": 500 | description: Not Found 501 | schema: 502 | $ref: '#/definitions/models.Error' 503 | type: object 504 | "500": 505 | description: Internal Server Error 506 | schema: 507 | $ref: '#/definitions/models.Error' 508 | type: object 509 | summary: Lets mixnode a node tell the directory server it's alive 510 | tags: 511 | - presence 512 | /api/presence/topology: 513 | get: 514 | consumes: 515 | - application/json 516 | description: Nym nodes periodically ping the directory server to register that 517 | they're alive. This method provides a list of nodes which have been most recently 518 | seen. 519 | operationId: topology 520 | produces: 521 | - application/json 522 | responses: 523 | "200": 524 | description: OK 525 | schema: 526 | $ref: '#/definitions/models.Topology' 527 | type: object 528 | "400": 529 | description: Bad Request 530 | schema: 531 | $ref: '#/definitions/models.Error' 532 | type: object 533 | "404": 534 | description: Not Found 535 | schema: 536 | $ref: '#/definitions/models.Error' 537 | type: object 538 | "500": 539 | description: Internal Server Error 540 | schema: 541 | $ref: '#/definitions/models.Error' 542 | type: object 543 | summary: Lists which Nym mixnodes, providers, gateways, and coconodes are alive 544 | tags: 545 | - presence 546 | swagger: "2.0" 547 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nymtech/nym-directory 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/BorisBorshevsky/timemock v0.0.0-20180501151413-a469e345aaba 7 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 8 | github.com/blang/semver/v4 v4.0.0 9 | github.com/gin-contrib/cors v1.3.0 10 | github.com/gin-gonic/gin v1.6.3 11 | github.com/go-openapi/spec v0.19.9 // indirect 12 | github.com/go-openapi/swag v0.19.9 // indirect 13 | github.com/golang/mock v1.3.1 // indirect 14 | github.com/gorilla/websocket v1.4.2 15 | github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 16 | github.com/jinzhu/gorm v1.9.16 17 | github.com/kr/text v0.2.0 // indirect 18 | github.com/mailru/easyjson v0.7.6 // indirect 19 | github.com/microcosm-cc/bluemonday v1.0.2 20 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 21 | github.com/onsi/ginkgo v1.14.1 22 | github.com/onsi/gomega v1.10.1 23 | github.com/pkg/errors v0.9.1 // indirect 24 | github.com/stretchr/testify v1.5.1 25 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 26 | github.com/swaggo/gin-swagger v1.2.0 27 | github.com/swaggo/swag v1.6.7 28 | golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect 29 | golang.org/x/sys v0.0.0-20200922070232-aee5d888a860 // indirect 30 | golang.org/x/tools v0.0.0-20200921210052-fa0125251cc4 // indirect 31 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 32 | gorm.io/driver/sqlite v1.1.3 33 | gorm.io/gorm v1.20.2 34 | gotest.tools v2.2.0+incompatible 35 | ) 36 | -------------------------------------------------------------------------------- /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/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 15 | github.com/BorisBorshevsky/timemock v0.0.0-20180501151413-a469e345aaba h1:+HVNUmgTsWkO37uDvZ1Ro6oDICHHMvvuZ+Jk3ZUK96o= 16 | github.com/BorisBorshevsky/timemock v0.0.0-20180501151413-a469e345aaba/go.mod h1:fmy4Q4OSwtHk6zpBXYCXclHYw7xnZBu3eAw2AUKswmU= 17 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 18 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 19 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 20 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 21 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 22 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 23 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 24 | github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 25 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 26 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 27 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 28 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 29 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 30 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 31 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 32 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 33 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 34 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 35 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 36 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 37 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 38 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 39 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 40 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 41 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 42 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 43 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 44 | github.com/cespare/reflex v0.3.0/go.mod h1:I+0Pnu2W693i7Hv6ZZG76qHTY0mgUa7uCIfCtikXojE= 45 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 46 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 47 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 48 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 49 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 50 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 51 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 52 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 53 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 54 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 55 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 56 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 57 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 58 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 59 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 60 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 61 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 62 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 63 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 64 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 65 | github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= 66 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 67 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 68 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 69 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 70 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 71 | github.com/ethereum/go-ethereum v1.9.3/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY= 72 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 73 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 74 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 75 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 76 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 77 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 78 | github.com/gin-contrib/cors v1.3.0 h1:PolezCc89peu+NgkIWt9OB01Kbzt6IP0J/JvkG6xxlg= 79 | github.com/gin-contrib/cors v1.3.0/go.mod h1:artPvLlhkF7oG06nK8v3U8TNz6IeX+w1uzCSEId5/Vc= 80 | github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= 81 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 82 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 83 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 84 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 85 | github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 86 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 87 | github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 88 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 89 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 90 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 91 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 92 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 93 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 94 | github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 95 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 96 | github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= 97 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 98 | github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 99 | github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 100 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= 101 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= 102 | github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg= 103 | github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= 104 | github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 105 | github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= 106 | github.com/go-openapi/spec v0.19.7/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= 107 | github.com/go-openapi/spec v0.19.9 h1:9z9cbFuZJ7AcvOHKIY+f6Aevb4vObNDkTEyoMfO7rAc= 108 | github.com/go-openapi/spec v0.19.9/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28= 109 | github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 110 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 111 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 112 | github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE= 113 | github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= 114 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 115 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 116 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 117 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 118 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 119 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 120 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 121 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 122 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 123 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 124 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 125 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 126 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 127 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 128 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 129 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 130 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 131 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 132 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 133 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 134 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 135 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 136 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 137 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 138 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 139 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 140 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 141 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 142 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 143 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 144 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 145 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 146 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 147 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 148 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 149 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 150 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 151 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 152 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 153 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 154 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 155 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 156 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 157 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 158 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 159 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 160 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 161 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 162 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 163 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 164 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 165 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 166 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 167 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 168 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 169 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 170 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 171 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 172 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 173 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 174 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 175 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 176 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 177 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 178 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 179 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 180 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 181 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 182 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 183 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 184 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 185 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 186 | github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 h1:cW/amwGEJK5MSKntPXRjX4dxs/nGxGT8gXKIsKFmHGc= 187 | github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15/go.mod h1:Fdm/oWRW+CH8PRbLntksCNtmcCBximKPkVQYvmMl80k= 188 | github.com/jessevdk/go-assets-builder v0.0.0-20130903091706-b8483521738f/go.mod h1:GjkD6wGIxVEccQ4pa27Ebe00zAi1EEpAcL6rL0ADvwU= 189 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 190 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= 191 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 192 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 193 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 194 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 195 | github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= 196 | github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 197 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 198 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 199 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 200 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 201 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 202 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 203 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 204 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 205 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 206 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 207 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 208 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 209 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 210 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 211 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 212 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 213 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 214 | github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 215 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 216 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 217 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 218 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 219 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 220 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 221 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 222 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 223 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 224 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 225 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 226 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 227 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 228 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 229 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 230 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 231 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 232 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 233 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 234 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 235 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 236 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 237 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 238 | github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= 239 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 240 | github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA= 241 | github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= 242 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 243 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 244 | github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= 245 | github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= 246 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 247 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 248 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 249 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 250 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 251 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 252 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 253 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 254 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 255 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 256 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 257 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 258 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 259 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 260 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 261 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 262 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 263 | github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= 264 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 265 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 266 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 267 | github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= 268 | github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 269 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 270 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 271 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 272 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 273 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 274 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 275 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 276 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 277 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 278 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 279 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 280 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 281 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 282 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 283 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 284 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 285 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 286 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 287 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 288 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 289 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 290 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 291 | github.com/rakyll/gotest v0.0.5/go.mod h1:SkoesdNCWmiD4R2dljIUcfSnNdVZ12y8qK4ojDkc2Sc= 292 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 293 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 294 | github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 295 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 296 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 297 | github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= 298 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 299 | github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 300 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 301 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 302 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 303 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 304 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 305 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 306 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 307 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 308 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 309 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 310 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 311 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 312 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 313 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 314 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 315 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 316 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 317 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 318 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 319 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 320 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 321 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 322 | github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= 323 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 324 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 325 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 326 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 327 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 328 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 329 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 330 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= 331 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= 332 | github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0= 333 | github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= 334 | github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= 335 | github.com/swaggo/swag v1.6.7 h1:e8GC2xDllJZr3omJkm9YfmK0Y56+rMO3cg0JBKNz09s= 336 | github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc= 337 | github.com/tav/golly v0.0.0-20180823113506-ad032321f11e/go.mod h1:DYKtPPxKBRsX/fJcltMPl3Mdsdl+x18y6VtHlbXFfKE= 338 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 339 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 340 | github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= 341 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 342 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 343 | github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 344 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 345 | github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= 346 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 347 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 348 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 349 | github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 350 | github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 351 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 352 | github.com/vektra/mockery/v2 v2.2.1/go.mod h1:rBZUbbhMbiSX1WlCGsOgAi6xjuJGxB7KKbnoL0XNYW8= 353 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 354 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 355 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 356 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 357 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 358 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 359 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 360 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 361 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 362 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 363 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 364 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 365 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 366 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 367 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 368 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 369 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 370 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 371 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 372 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 373 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 374 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 375 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 376 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 377 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 378 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 379 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 380 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 381 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 382 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 383 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 384 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 385 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 386 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 387 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 388 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 389 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 390 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 391 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 392 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 393 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 394 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 395 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 396 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 397 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 398 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 399 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 400 | golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 401 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 402 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 403 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 404 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 405 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 406 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 407 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 408 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 409 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 410 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 411 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 412 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 413 | golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 414 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 415 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 416 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 417 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 418 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 419 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 420 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 421 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 422 | golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= 423 | golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 424 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 425 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 426 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 427 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 428 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 429 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 430 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 431 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 432 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 433 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 434 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 435 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 436 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 437 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 438 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 439 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 440 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 441 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 442 | golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 443 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 444 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 445 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 448 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 449 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 450 | golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 451 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 452 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 453 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 454 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 455 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 456 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 457 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 458 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 459 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 460 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 461 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 462 | golang.org/x/sys v0.0.0-20200922070232-aee5d888a860 h1:YEu4SMq7D0cmT7CBbXfcH0NZeuChAXwsHe/9XueUO6o= 463 | golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 464 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 465 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 466 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 467 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 468 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 469 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 470 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 471 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 472 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 473 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 474 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 475 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 476 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 477 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 478 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 479 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 480 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 481 | golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 482 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 483 | golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 484 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 485 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 486 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 487 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 488 | golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 489 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 490 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 491 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 492 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 493 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 494 | golang.org/x/tools v0.0.0-20200323144430-8dcfad9e016e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 495 | golang.org/x/tools v0.0.0-20200407041343-bf15fae40dea/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 496 | golang.org/x/tools v0.0.0-20200921210052-fa0125251cc4 h1:v8Jgq9X6Es9K9otVr9jxENEJigepKMZgA9OmrIZDtFA= 497 | golang.org/x/tools v0.0.0-20200921210052-fa0125251cc4/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 498 | golang.org/x/tools/gopls v0.4.0/go.mod h1:fdOZ8zb6nqlePvfek79JCskQXI4W+i2e1xT+xOPKMcY= 499 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 500 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 501 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 502 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 503 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 504 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 505 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 506 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 507 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 508 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 509 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 510 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 511 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 512 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 513 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 514 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 515 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 516 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 517 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 518 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 519 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 520 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 521 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 522 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 523 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 524 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 525 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 526 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 527 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 528 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 529 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 530 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 531 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 532 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 533 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 534 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 535 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 536 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 537 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 538 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 539 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 540 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 541 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 542 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 543 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 544 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 545 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 546 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 547 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 548 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 549 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 550 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 551 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 552 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 553 | gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc= 554 | gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c= 555 | gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 556 | gorm.io/gorm v1.20.2 h1:bZzSEnq7NDGsrd+n3evOOedDrY5oLM5QPlCjZJUK2ro= 557 | gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 558 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 559 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 560 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 561 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 562 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 563 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 564 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 565 | mvdan.cc/xurls/v2 v2.1.0/go.mod h1:5GrSd9rOnKOpZaji1OZLYL/yeAAtGDlo/cFe+8K5n8E= 566 | mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8= 567 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 568 | -------------------------------------------------------------------------------- /healthcheck/controller.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // controller is the presence controller 10 | type controller struct{} 11 | 12 | // Controller is the presence controller 13 | type Controller interface { 14 | HealthCheck(c *gin.Context) 15 | RegisterRoutes(router *gin.Engine) 16 | } 17 | 18 | // New returns a new pki.Controller 19 | func New() Controller { 20 | return &controller{} 21 | } 22 | 23 | func (controller *controller) RegisterRoutes(router *gin.Engine) { 24 | router.GET("/api/healthcheck", controller.HealthCheck) 25 | } 26 | 27 | // HealthCheck ... 28 | // @Summary Lets the metrics server tell the world it's alive. 29 | // @Description Returns a 200 if the metrics server is available. Good route to use for automated monitoring. 30 | // @ID healthCheck 31 | // @Accept json 32 | // @Produce json 33 | // @Tags healthcheck 34 | // @Success 200 35 | // @Router /api/healthcheck [get] 36 | func (controller *controller) HealthCheck(c *gin.Context) { 37 | c.JSON(http.StatusOK, gin.H{"ok": true}) 38 | } 39 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | _ "github.com/nymtech/nym-directory/docs" 8 | "github.com/nymtech/nym-directory/metrics" 9 | "github.com/nymtech/nym-directory/server" 10 | ) 11 | 12 | // @title Nym Metrics API 13 | // @version 0.9.0 14 | // @description This is a temporarily centralized metrics API to allow us to get the other Nym node types running. Its functionality will eventually be folded into other parts of Nym. 15 | // @termsOfService http://swagger.io/terms/ 16 | 17 | // @license.name Apache 2.0 18 | // @license.url https://github.com/nymtech/nym-metrics-server/ 19 | func main() { 20 | args := os.Args[1:] 21 | if len(args) != 1 { 22 | fmt.Fprint(os.Stderr, "Expected single argument to be passed - address of the validator server") 23 | return 24 | } 25 | validatorAddress := args[0] 26 | 27 | router := server.New() 28 | go metrics.DynamicallyUpdateReportDelay(validatorAddress) 29 | router.Run(":8080") 30 | } 31 | -------------------------------------------------------------------------------- /metrics/controller.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/nymtech/nym-directory/models" 14 | ) 15 | 16 | // Config for this controller 17 | type Config struct { 18 | Sanitizer Sanitizer 19 | Service IService 20 | } 21 | 22 | // controller is the metrics controller 23 | type controller struct { 24 | service IService 25 | sanitizer Sanitizer 26 | } 27 | 28 | const MaxDesiredRequests = 50 // per second 29 | const MinReportDelay uint64 = 5 // seconds 30 | 31 | var nextReportDelay = MinReportDelay 32 | 33 | 34 | // we don't care about structure itself, we just want to know the count 35 | type Topology struct { 36 | Gateways []interface{} `json:"gateways"` 37 | MixNodes []interface{} `json:"mixNodes"` 38 | } 39 | 40 | func nodesCount(validatorAddress string) int64 { 41 | resp, err := http.Get(validatorAddress + "/api/mixmining/topology") 42 | if err != nil { 43 | _, _ = fmt.Fprintf(os.Stderr, "failed to obtain network topology - %v", err) 44 | return - 1 45 | } 46 | defer resp.Body.Close() 47 | body, err := ioutil.ReadAll(resp.Body) 48 | if err != nil { 49 | _, _ = fmt.Fprintf(os.Stderr, "failed to obtain network topology - %v", err) 50 | return -1 51 | } 52 | 53 | var topology Topology 54 | err = json.Unmarshal(body, &topology) 55 | if err != nil { 56 | _, _ = fmt.Fprintf(os.Stderr, "failed to obtain network topology - %v", err) 57 | return - 1 58 | } 59 | 60 | 61 | return int64(len(topology.MixNodes)) 62 | } 63 | 64 | 65 | 66 | func DynamicallyUpdateReportDelay(validatorAddress string) { 67 | updateTicker := time.NewTicker(time.Minute) 68 | for { 69 | <-updateTicker.C 70 | onlineNodes := nodesCount(validatorAddress) 71 | 72 | if onlineNodes > 0 { 73 | newNextReportDelay := uint64(onlineNodes / MaxDesiredRequests) 74 | if newNextReportDelay < MinReportDelay { 75 | // no point in sending it SO often 76 | newNextReportDelay = MinReportDelay 77 | } 78 | 79 | atomic.StoreUint64(&nextReportDelay, newNextReportDelay) 80 | } 81 | } 82 | } 83 | 84 | // Controller ... 85 | type Controller interface { 86 | CreateMixMetric(c *gin.Context) 87 | RegisterRoutes(router *gin.Engine) 88 | } 89 | 90 | // New returns a new metrics.Controller... 91 | func New(cfg Config) Controller { 92 | return &controller{cfg.Service, cfg.Sanitizer} 93 | } 94 | 95 | func (controller *controller) RegisterRoutes(router *gin.Engine) { 96 | router.POST("/api/metrics/mixes", controller.CreateMixMetric) 97 | router.GET("/api/metrics/mixes", controller.ListMixMetrics) 98 | } 99 | 100 | // CreateMixMetric ... 101 | // @Summary Create a metric detailing how many messages a given mixnode sent and received 102 | // @Description For demo and debug purposes it gives us the ability to generate useful visualisations of network traffic. 103 | // @ID createMixMetric 104 | // @Accept json 105 | // @Produce json 106 | // @Tags metrics 107 | // @Param object body models.MixMetric true "object" 108 | // @Success 201 {object} models.MixMetricInterval 109 | // @Failure 400 {object} models.Error 110 | // @Failure 404 {object} models.Error 111 | // @Failure 500 {object} models.Error 112 | // @Router /api/metrics/mixes [post] 113 | func (controller *controller) CreateMixMetric(c *gin.Context) { 114 | var metric models.MixMetric 115 | if err := c.ShouldBindJSON(&metric); err != nil { 116 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 117 | return 118 | } 119 | sanitized := controller.sanitizer.Sanitize(metric) 120 | controller.service.CreateMixMetric(sanitized) 121 | 122 | nextReportDelay := atomic.LoadUint64(&nextReportDelay) 123 | 124 | interval := models.MixMetricInterval{ 125 | NextReportIn: nextReportDelay, 126 | } 127 | 128 | c.JSON(http.StatusCreated, interval) 129 | } 130 | 131 | // ListMixMetrics lists mixnode activity 132 | // @Summary Lists mixnode activity in the past 3 seconds 133 | // @Description For demo and debug purposes it gives us the ability to generate useful visualisations of network traffic. 134 | // @ID listMixMetrics 135 | // @Accept json 136 | // @Produce json 137 | // @Tags metrics 138 | // Param object body models.ObjectRequest true "object" 139 | // @Success 200 {array} models.MixMetric 140 | // @Failure 400 {object} models.Error 141 | // @Failure 404 {object} models.Error 142 | // @Failure 500 {object} models.Error 143 | // @Router /api/metrics/mixes [get] 144 | func (controller *controller) ListMixMetrics(c *gin.Context) { 145 | metrics := controller.service.List() 146 | c.JSON(http.StatusOK, metrics) 147 | } 148 | -------------------------------------------------------------------------------- /metrics/controller_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/nymtech/nym-directory/metrics/fixtures" 11 | "github.com/nymtech/nym-directory/metrics/mocks" 12 | "github.com/nymtech/nym-directory/models" 13 | . "github.com/onsi/ginkgo" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | var _ = Describe("MetricsController", func() { 18 | Describe("creating a metric", func() { 19 | Context("containing xss", func() { 20 | It("should strip the xss attack", func() { 21 | router, mockService, mockSanitizer := SetupRouter() 22 | mockSanitizer.On("Sanitize", xssMetric()).Return(goodMetric()) 23 | mockService.On("CreateMixMetric", goodMetric()) 24 | json, _ := json.Marshal(xssMetric()) 25 | 26 | resp := performRequest(router, "POST", "/api/metrics/mixes", json) 27 | 28 | assert.Equal(GinkgoT(), 201, resp.Code) 29 | mockSanitizer.AssertCalled(GinkgoT(), "Sanitize", xssMetric()) 30 | mockService.AssertCalled(GinkgoT(), "CreateMixMetric", goodMetric()) 31 | }) 32 | }) 33 | }) 34 | Describe("listing metrics", func() { 35 | Context("when no metrics exist", func() { 36 | It("should return an empty list of metrics", func() { 37 | router, mockService, mockSanitizer := SetupRouter() 38 | _ = mockSanitizer // nothing to sanitize here 39 | mockService.On("List").Return([]models.PersistedMixMetric{}) 40 | 41 | resp := performRequest(router, "GET", "/api/metrics/mixes", nil) 42 | 43 | assert.Equal(GinkgoT(), 200, resp.Code) 44 | mockService.AssertExpectations(GinkgoT()) 45 | }) 46 | }) 47 | Context("when metrics exist", func() { 48 | It("should return them", func() { 49 | router, mockService, mockSanitizer := SetupRouter() 50 | _ = mockSanitizer // nothing to sanitize here 51 | 52 | mockService.On("List").Return(fixtures.MixMetricsList()) 53 | 54 | resp := performRequest(router, "GET", "/api/metrics/mixes", nil) 55 | var response []models.PersistedMixMetric 56 | json.Unmarshal([]byte(resp.Body.String()), &response) 57 | 58 | assert.Equal(GinkgoT(), 200, resp.Code) 59 | assert.Equal(GinkgoT(), fixtures.MixMetricsList(), response) 60 | mockService.AssertExpectations(GinkgoT()) 61 | }) 62 | }) 63 | }) 64 | }) 65 | 66 | func SetupRouter() (*gin.Engine, *mocks.IService, *mocks.Sanitizer) { 67 | mockSanitizer := new(mocks.Sanitizer) 68 | mockService := new(mocks.IService) 69 | 70 | metricsConfig := Config{ 71 | Sanitizer: mockSanitizer, 72 | Service: mockService, 73 | } 74 | 75 | gin.SetMode(gin.TestMode) 76 | router := gin.Default() 77 | 78 | controller := New(metricsConfig) 79 | controller.RegisterRoutes(router) 80 | return router, mockService, mockSanitizer 81 | } 82 | 83 | func performRequest(r http.Handler, method, path string, body []byte) *httptest.ResponseRecorder { 84 | buf := bytes.NewBuffer(body) 85 | req, _ := http.NewRequest(method, path, buf) 86 | w := httptest.NewRecorder() 87 | r.ServeHTTP(w, req) 88 | return w 89 | } 90 | -------------------------------------------------------------------------------- /metrics/db.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/nymtech/nym-directory/models" 8 | ) 9 | 10 | // IDb holds metrics information 11 | type IDb interface { 12 | Add(models.PersistedMixMetric) 13 | List() []models.PersistedMixMetric 14 | } 15 | 16 | // Db holds data for metrics 17 | type Db struct { 18 | sync.Mutex 19 | incomingMetrics []models.PersistedMixMetric 20 | mixMetrics []models.PersistedMixMetric 21 | ticker *time.Ticker 22 | } 23 | 24 | // NewDb constructor 25 | func NewDb() *Db { 26 | ticker := time.NewTicker(3 * time.Second) 27 | 28 | d := Db{ 29 | incomingMetrics: []models.PersistedMixMetric{}, 30 | mixMetrics: []models.PersistedMixMetric{}, 31 | } 32 | d.ticker = ticker 33 | go dbCleaner(ticker, &d) 34 | 35 | return &d 36 | } 37 | 38 | // Add adds a models.PersistedMixMetric to the database 39 | func (db *Db) Add(metric models.PersistedMixMetric) { 40 | db.Lock() 41 | defer db.Unlock() 42 | db.incomingMetrics = append(db.incomingMetrics, metric) 43 | } 44 | 45 | // List returns all models.PersistedMixMetric in the database 46 | func (db *Db) List() []models.PersistedMixMetric { 47 | db.Lock() 48 | defer db.Unlock() 49 | return db.mixMetrics 50 | } 51 | 52 | // dbCleaner periodically clears the database 53 | func dbCleaner(ticker *time.Ticker, database *Db) { 54 | for { 55 | select { 56 | case <-ticker.C: 57 | database.clear() 58 | } 59 | } 60 | } 61 | 62 | // clear kills any stale metrics info 63 | // 64 | // This may look a little weird, but there's a logic to it. 65 | // 66 | // If we have only one array holding metrics, incoming metrics get stacked up 67 | // for a while, and then all destroyed at once, so the list we can provider 68 | // starts empty, swells, then becomes empty again. This doesn't offer clients 69 | // a consistent view of what happened. 70 | // 71 | // Instead we Add() to an `incomingMixMetrics` slice, and read from the 72 | // `mixMetrics` slice. When we clear the db, we can transfer everything from 73 | // `incoming` to `mixMetrics` and have a full list, clearing incoming. 74 | // This way we can offer a consistent view of what happened 75 | // over any individual bit of time. 76 | func (db *Db) clear() { 77 | db.Lock() 78 | defer db.Unlock() 79 | db.mixMetrics = db.incomingMetrics 80 | db.incomingMetrics = db.incomingMetrics[:0] 81 | } 82 | -------------------------------------------------------------------------------- /metrics/db_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/BorisBorshevsky/timemock" 5 | "github.com/nymtech/nym-directory/models" 6 | . "github.com/onsi/ginkgo" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var _ = Describe("Metrics Db", func() { 11 | var db *Db 12 | var metric1 models.MixMetric 13 | var metric2 models.MixMetric 14 | var p1 models.PersistedMixMetric 15 | var p2 models.PersistedMixMetric 16 | 17 | var received uint = 99 18 | var now = timemock.Now().UnixNano() 19 | 20 | // set up fixtures 21 | metric1 = models.MixMetric{ 22 | PubKey: "key1", 23 | Received: &received, 24 | Sent: map[string]uint{"mixnode3": 99, "mixnode4": 100}, 25 | } 26 | p1 = models.PersistedMixMetric{ 27 | MixMetric: metric1, 28 | Timestamp: now, 29 | } 30 | 31 | metric2 = models.MixMetric{ 32 | PubKey: "key2", 33 | Received: &received, 34 | Sent: map[string]uint{"mixnode3": 101, "mixnode4": 102}, 35 | } 36 | p2 = models.PersistedMixMetric{ 37 | MixMetric: metric2, 38 | Timestamp: now, 39 | } 40 | 41 | Describe("retrieving mixnet metrics", func() { 42 | Context("when no metrics have been added", func() { 43 | It("should return an empty metrics list", func() { 44 | db = NewDb() 45 | assert.Len(GinkgoT(), db.List(), 0) 46 | }) 47 | }) 48 | }) 49 | Describe("adding mixnet metrics", func() { 50 | Context("adding 1", func() { 51 | It("should contain 1 metric", func() { 52 | db = NewDb() 53 | db.Add(p1) 54 | assert.Len(GinkgoT(), db.List(), 0) // see note on db.clear() 55 | assert.Len(GinkgoT(), db.incomingMetrics, 1) // see note on db.clear() 56 | }) 57 | }) 58 | Context("adding 2", func() { 59 | It("should contain 2 metrics", func() { 60 | db = NewDb() 61 | db.Add(p1) 62 | db.Add(p2) 63 | assert.Len(GinkgoT(), db.List(), 0) // see note on db.clear() 64 | assert.Len(GinkgoT(), db.incomingMetrics, 2) // see note on db.clear() 65 | }) 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /metrics/fixtures/fixtures.go: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import "github.com/nymtech/nym-directory/models" 4 | 5 | func MixMetricsList() []models.PersistedMixMetric { 6 | r1 := uint(1) 7 | m1 := models.PersistedMixMetric{ 8 | MixMetric: models.MixMetric{ 9 | PubKey: "pubkey1", 10 | Received: &r1, 11 | }, 12 | } 13 | 14 | r2 := uint(2) 15 | m2 := models.PersistedMixMetric{ 16 | MixMetric: models.MixMetric{ 17 | PubKey: "pubkey2", 18 | Received: &r2, 19 | }, 20 | } 21 | 22 | metrics := []models.PersistedMixMetric{m1, m2} 23 | return metrics 24 | } 25 | -------------------------------------------------------------------------------- /metrics/metrics_suite_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestMetrics(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Metrics Suite") 13 | } 14 | -------------------------------------------------------------------------------- /metrics/mocks/IDb.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | import models "github.com/nymtech/nym-directory/models" 7 | 8 | // IDb is an autogenerated mock type for the IDb type 9 | type IDb struct { 10 | mock.Mock 11 | } 12 | 13 | // Add provides a mock function with given fields: _a0 14 | func (_m *IDb) Add(_a0 models.PersistedMixMetric) { 15 | _m.Called(_a0) 16 | } 17 | 18 | // List provides a mock function with given fields: 19 | func (_m *IDb) List() []models.PersistedMixMetric { 20 | ret := _m.Called() 21 | 22 | var r0 []models.PersistedMixMetric 23 | if rf, ok := ret.Get(0).(func() []models.PersistedMixMetric); ok { 24 | r0 = rf() 25 | } else { 26 | if ret.Get(0) != nil { 27 | r0 = ret.Get(0).([]models.PersistedMixMetric) 28 | } 29 | } 30 | 31 | return r0 32 | } 33 | -------------------------------------------------------------------------------- /metrics/mocks/IService.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | import models "github.com/nymtech/nym-directory/models" 7 | 8 | // IService is an autogenerated mock type for the IService type 9 | type IService struct { 10 | mock.Mock 11 | } 12 | 13 | // CreateMixMetric provides a mock function with given fields: metric 14 | func (_m *IService) CreateMixMetric(metric models.MixMetric) { 15 | _m.Called(metric) 16 | } 17 | 18 | // List provides a mock function with given fields: 19 | func (_m *IService) List() []models.PersistedMixMetric { 20 | ret := _m.Called() 21 | 22 | var r0 []models.PersistedMixMetric 23 | if rf, ok := ret.Get(0).(func() []models.PersistedMixMetric); ok { 24 | r0 = rf() 25 | } else { 26 | if ret.Get(0) != nil { 27 | r0 = ret.Get(0).([]models.PersistedMixMetric) 28 | } 29 | } 30 | 31 | return r0 32 | } 33 | -------------------------------------------------------------------------------- /metrics/mocks/Sanitizer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | import models "github.com/nymtech/nym-directory/models" 7 | 8 | // Sanitizer is an autogenerated mock type for the Sanitizer type 9 | type Sanitizer struct { 10 | mock.Mock 11 | } 12 | 13 | // Sanitize provides a mock function with given fields: input 14 | func (_m *Sanitizer) Sanitize(input models.MixMetric) models.MixMetric { 15 | ret := _m.Called(input) 16 | 17 | var r0 models.MixMetric 18 | if rf, ok := ret.Get(0).(func(models.MixMetric) models.MixMetric); ok { 19 | r0 = rf(input) 20 | } else { 21 | r0 = ret.Get(0).(models.MixMetric) 22 | } 23 | 24 | return r0 25 | } 26 | -------------------------------------------------------------------------------- /metrics/sanitizer.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/microcosm-cc/bluemonday" 5 | "github.com/nymtech/nym-directory/models" 6 | ) 7 | 8 | // Sanitizer sanitizes untrusted metrics data. It should be used in 9 | // controllers to wipe out any questionable input at our application's front 10 | // door. 11 | type Sanitizer interface { 12 | Sanitize(input models.MixMetric) models.MixMetric 13 | } 14 | 15 | type sanitizer struct { 16 | policy *bluemonday.Policy 17 | } 18 | 19 | // NewSanitizer returns a new input sanitizer for metrics 20 | func NewSanitizer(policy *bluemonday.Policy) Sanitizer { 21 | return sanitizer{ 22 | policy: policy, 23 | } 24 | } 25 | 26 | func (s sanitizer) Sanitize(input models.MixMetric) models.MixMetric { 27 | sanitized := newMetric() 28 | 29 | sanitized.PubKey = s.policy.Sanitize(input.PubKey) 30 | for key, value := range input.Sent { 31 | k := bluemonday.UGCPolicy().Sanitize(key) 32 | sanitized.Sent[k] = value 33 | } 34 | sanitized.Received = input.Received 35 | return sanitized 36 | } 37 | 38 | func newMetric() models.MixMetric { 39 | sent := make(map[string]uint) 40 | received := uint(1) 41 | return models.MixMetric{ 42 | PubKey: "", 43 | Sent: sent, 44 | Received: &received, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /metrics/sanitizer_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/microcosm-cc/bluemonday" 5 | "github.com/nymtech/nym-directory/models" 6 | . "github.com/onsi/ginkgo" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var _ = Describe("Sanitizer", func() { 11 | Describe("sanitizing inputs", func() { 12 | Context("when XSS is present", func() { 13 | It("sanitizes input", func() { 14 | policy := bluemonday.UGCPolicy() 15 | sanitizer := NewSanitizer(policy) 16 | result := sanitizer.Sanitize(xssMetric()) 17 | assert.Equal(GinkgoT(), goodMetric(), result) 18 | }) 19 | }) 20 | Context("when XSS is not present", func() { 21 | It("doesn't change input", func() { 22 | policy := bluemonday.UGCPolicy() 23 | sanitizer := NewSanitizer(policy) 24 | result := sanitizer.Sanitize(goodMetric()) 25 | assert.Equal(GinkgoT(), goodMetric(), result) 26 | }) 27 | }) 28 | }) 29 | }) 30 | 31 | func xssMetric() models.MixMetric { 32 | sent := make(map[string]uint) 33 | sent["foo"] = 1 34 | received := uint(3) 35 | m := models.MixMetric{ 36 | PubKey: "bar", 37 | Sent: sent, 38 | Received: &received, 39 | } 40 | return m 41 | } 42 | 43 | func goodMetric() models.MixMetric { 44 | sent := make(map[string]uint) 45 | sent["foo"] = 1 46 | received := uint(3) 47 | m := models.MixMetric{ 48 | PubKey: "bar", 49 | Sent: sent, 50 | Received: &received, 51 | } 52 | return m 53 | } 54 | -------------------------------------------------------------------------------- /metrics/service.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/BorisBorshevsky/timemock" 8 | "github.com/nymtech/nym-directory/models" 9 | "github.com/nymtech/nym-directory/server/websocket" 10 | ) 11 | 12 | // Service struct 13 | type Service struct { 14 | db IDb 15 | hub websocket.IHub 16 | } 17 | 18 | // IService defines the REST service interface for metrics. 19 | type IService interface { 20 | CreateMixMetric(metric models.MixMetric) 21 | List() []models.PersistedMixMetric 22 | } 23 | 24 | // NewService constructor 25 | func NewService(db IDb, hub websocket.IHub) *Service { 26 | return &Service{ 27 | db: db, 28 | hub: hub, 29 | } 30 | } 31 | 32 | // CreateMixMetric adds a new PersistedMixMetric in the database. 33 | func (service *Service) CreateMixMetric(metric models.MixMetric) { 34 | persist := models.PersistedMixMetric{ 35 | MixMetric: metric, 36 | Timestamp: timemock.Now().UnixNano(), 37 | } 38 | service.db.Add(persist) 39 | 40 | b, err := json.Marshal(persist) 41 | if err != nil { 42 | fmt.Println(err) 43 | return 44 | } 45 | service.hub.Notify(b) 46 | 47 | } 48 | 49 | // List lists all mix metrics in the database 50 | func (service *Service) List() []models.PersistedMixMetric { 51 | return service.db.List() 52 | } 53 | -------------------------------------------------------------------------------- /metrics/service_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/BorisBorshevsky/timemock" 7 | "github.com/nymtech/nym-directory/metrics/mocks" 8 | "github.com/nymtech/nym-directory/models" 9 | . "github.com/onsi/ginkgo" 10 | "gotest.tools/assert" 11 | 12 | wsMocks "github.com/nymtech/nym-directory/server/websocket/mocks" 13 | ) 14 | 15 | var _ = Describe("metrics.Service", func() { 16 | var mockDb mocks.IDb 17 | var m1 models.MixMetric 18 | var m2 models.MixMetric 19 | var p1 models.PersistedMixMetric 20 | var p2 models.PersistedMixMetric 21 | 22 | var serv Service 23 | var received uint = 99 24 | var now = timemock.Now() 25 | timemock.Freeze(now) 26 | var frozenNow = timemock.Now().UnixNano() 27 | 28 | // set up fixtures 29 | m1 = models.MixMetric{ 30 | PubKey: "key1", 31 | Received: &received, 32 | Sent: map[string]uint{"mixnode3": 99, "mixnode4": 101}, 33 | } 34 | 35 | p1 = models.PersistedMixMetric{ 36 | MixMetric: m1, 37 | Timestamp: frozenNow, 38 | } 39 | 40 | m2 = models.MixMetric{ 41 | PubKey: "key2", 42 | Received: &received, 43 | Sent: map[string]uint{"mixnode3": 102, "mixnode4": 103}, 44 | } 45 | 46 | p2 = models.PersistedMixMetric{ 47 | MixMetric: m2, 48 | Timestamp: frozenNow, 49 | } 50 | 51 | Describe("Adding a mixmetric", func() { 52 | It("should add a PersistedMixMetric to the db and notify the Hub", func() { 53 | mockDb = *new(mocks.IDb) 54 | mockHub := *new(wsMocks.IHub) 55 | serv = *NewService(&mockDb, &mockHub) 56 | mockDb.On("Add", p1) 57 | j, _ := json.Marshal(p1) 58 | mockHub.On("Notify", j) 59 | 60 | serv.CreateMixMetric(m1) 61 | 62 | mockDb.AssertCalled(GinkgoT(), "Add", p1) 63 | mockHub.AssertCalled(GinkgoT(), "Notify", j) 64 | }) 65 | }) 66 | Describe("Listing mixmetrics", func() { 67 | Context("when receiving a list request", func() { 68 | It("should call to the Db", func() { 69 | mockDb = *new(mocks.IDb) 70 | mockHub := *new(wsMocks.IHub) 71 | 72 | list := []models.PersistedMixMetric{p1, p2} 73 | 74 | serv = *NewService(&mockDb, &mockHub) 75 | mockDb.On("List").Return(list) 76 | 77 | result := serv.List() 78 | 79 | mockDb.AssertCalled(GinkgoT(), "List") 80 | assert.Equal(GinkgoT(), list[0].MixMetric.PubKey, result[0].MixMetric.PubKey) 81 | assert.Equal(GinkgoT(), list[1].MixMetric.PubKey, result[1].MixMetric.PubKey) 82 | }) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /models/common.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Error ... 4 | type Error struct { 5 | Error string `json:"error"` 6 | } 7 | -------------------------------------------------------------------------------- /models/metrics.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // MixMetric is a report from each mixnode detailing recent traffic. 4 | // Useful for creating visualisations. 5 | type MixMetric struct { 6 | PubKey string `json:"pubKey" binding:"required"` 7 | Sent map[string]uint `json:"sent" binding:"required"` 8 | Received *uint `json:"received" binding:"required"` 9 | } 10 | 11 | // PersistedMixMetric is a saved MixMetric with a timestamp recording when it 12 | // was seen by the metrics server. It can be used to build visualizations of 13 | // mixnet traffic. 14 | type PersistedMixMetric struct { 15 | MixMetric 16 | Timestamp int64 `json:"timestamp" binding:"required"` 17 | } 18 | 19 | // MixMetricInterval specifies when given node should submit its next report 20 | type MixMetricInterval struct { 21 | NextReportIn uint64 `json:"nextReportIn"` 22 | } -------------------------------------------------------------------------------- /models/models_suite_test.go: -------------------------------------------------------------------------------- 1 | package models_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestModels(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Models Suite") 13 | } 14 | -------------------------------------------------------------------------------- /nym_directory_suite_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestNymDirectory(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "NymDirectory Suite") 13 | } 14 | -------------------------------------------------------------------------------- /server/html/index.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jessevdk/go-assets" 7 | ) 8 | 9 | var _Assets87ef411210acb6cc2ef5dc23af8ae586f1a46c19 = "\n\n\n\n Chat Example\n \n \n\n\n\n
\n
\n \n \n
\n\n\n" 10 | 11 | // Assets returns go-assets FileSystem 12 | var Assets = assets.NewFileSystem(map[string][]string{"/": []string{"server"}, "/server": []string{"html"}, "/server/html": []string{"index.html"}}, map[string]*assets.File{ 13 | "/": &assets.File{ 14 | Path: "/", 15 | FileMode: 0x800001fd, 16 | Mtime: time.Unix(1569951733, 1569951733000002230), 17 | Data: nil, 18 | }, "/server": &assets.File{ 19 | Path: "/server", 20 | FileMode: 0x800001fd, 21 | Mtime: time.Unix(1569951733, 1569951733004002245), 22 | Data: nil, 23 | }, "/server/html": &assets.File{ 24 | Path: "/server/html", 25 | FileMode: 0x800001fd, 26 | Mtime: time.Unix(1569951733, 1569951733004002245), 27 | Data: nil, 28 | }, "/server/html/index.html": &assets.File{ 29 | Path: "/server/html/index.html", 30 | FileMode: 0x1b4, 31 | Mtime: time.Unix(1569951814, 1569951814816302011), 32 | Data: []byte(_Assets87ef411210acb6cc2ef5dc23af8ae586f1a46c19), 33 | }}, "") 34 | -------------------------------------------------------------------------------- /server/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chat Example 6 | 58 | 94 | 95 | 96 | 97 |
98 |
99 | 100 | 101 |
102 | 103 | 104 | -------------------------------------------------------------------------------- /server/html/template.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import ( 4 | "html/template" 5 | "io/ioutil" 6 | ) 7 | 8 | // LoadTemplate loads templates embedded by go-assets-builder 9 | func LoadTemplate() (*template.Template, error) { 10 | t := template.New("") 11 | for name, file := range Assets.Files { 12 | 13 | h, err := ioutil.ReadAll(file) 14 | if err != nil { 15 | return nil, err 16 | } 17 | t, err = t.New(name).Parse(string(h)) 18 | if err != nil { 19 | return nil, err 20 | } 21 | } 22 | return t, nil 23 | } 24 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/microcosm-cc/bluemonday" 6 | "github.com/nymtech/nym-directory/healthcheck" 7 | "github.com/nymtech/nym-directory/metrics" 8 | "github.com/nymtech/nym-directory/server/html" 9 | "github.com/nymtech/nym-directory/server/websocket" 10 | "net/http" 11 | 12 | "github.com/gin-contrib/cors" 13 | swaggerFiles "github.com/swaggo/files" 14 | ginSwagger "github.com/swaggo/gin-swagger" 15 | ) 16 | 17 | // New returns a new REST API server 18 | func New() *gin.Engine { 19 | gin.SetMode(gin.ReleaseMode) 20 | 21 | // Set the router as the default one shipped with Gin 22 | router := gin.Default() 23 | 24 | // Add cors middleware 25 | router.Use(cors.Default()) 26 | 27 | // Serve Swagger frontend static files using gin-swagger middleware 28 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 29 | 30 | // Add HTML templates to the router 31 | t, err := html.LoadTemplate() 32 | if err != nil { 33 | panic(err) 34 | } 35 | router.SetHTMLTemplate(t) 36 | router.GET("/", func(c *gin.Context) { 37 | c.HTML(http.StatusOK, "/server/html/index.html", nil) 38 | }) 39 | 40 | // Set up websocket handlers 41 | hub := websocket.NewHub() 42 | go hub.Run() 43 | router.GET("/ws", func(c *gin.Context) { 44 | websocket.Serve(hub, c.Writer, c.Request) 45 | }) 46 | 47 | // Sanitize controller input against XSS attacks using bluemonday.Policy 48 | policy := bluemonday.UGCPolicy() 49 | 50 | // Metrics: wire up dependency injection 51 | metricsCfg := injectMetrics(hub, policy) 52 | 53 | // Register all HTTP controller routes 54 | healthcheck.New().RegisterRoutes(router) 55 | metrics.New(metricsCfg).RegisterRoutes(router) 56 | 57 | return router 58 | } 59 | 60 | func injectMetrics(hub *websocket.Hub, policy *bluemonday.Policy) metrics.Config { 61 | sanitizer := metrics.NewSanitizer(policy) 62 | db := metrics.NewDb() 63 | metricsService := *metrics.NewService(db, hub) 64 | 65 | return metrics.Config{ 66 | Service: &metricsService, 67 | Sanitizer: sanitizer, 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /server/websocket/client.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | const ( 13 | // Time allowed to write a message to the peer. 14 | writeWait = 10 * time.Second 15 | 16 | // Time allowed to read the next pong message from the peer. 17 | pongWait = 60 * time.Second 18 | 19 | // Send pings to peer with this period. Must be less than pongWait. 20 | pingPeriod = (pongWait * 9) / 10 21 | 22 | // Maximum message size allowed from peer. 23 | maxMessageSize = 512 24 | ) 25 | 26 | var ( 27 | newline = []byte{'\n'} 28 | space = []byte{' '} 29 | ) 30 | 31 | var upgrader = websocket.Upgrader{ 32 | ReadBufferSize: 1024, 33 | WriteBufferSize: 1024, 34 | } 35 | 36 | // Client is a middleman between the websocket connection and the hub. 37 | type Client struct { 38 | hub *Hub 39 | 40 | // The websocket connection. 41 | conn *websocket.Conn 42 | 43 | // Buffered channel of outbound messages. 44 | send chan []byte 45 | } 46 | 47 | // readPump pumps messages from the websocket connection to the hub. 48 | // 49 | // The application runs readPump in a per-connection goroutine. The application 50 | // ensures that there is at most one reader on a connection by executing all 51 | // reads from this goroutine. 52 | func (c *Client) readPump() { 53 | defer func() { 54 | c.hub.unregister <- c 55 | c.conn.Close() 56 | }() 57 | c.conn.SetReadLimit(maxMessageSize) 58 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 59 | c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 60 | for { 61 | _, message, err := c.conn.ReadMessage() 62 | if err != nil { 63 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 64 | log.Printf("error: %v", err) 65 | } 66 | break 67 | } 68 | message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) 69 | c.hub.broadcast <- message 70 | } 71 | } 72 | 73 | // writePump pumps messages from the hub to the websocket connection. 74 | // 75 | // A goroutine running writePump is started for each connection. The 76 | // application ensures that there is at most one writer to a connection by 77 | // executing all writes from this goroutine. 78 | func (c *Client) writePump() { 79 | ticker := time.NewTicker(pingPeriod) 80 | defer func() { 81 | ticker.Stop() 82 | c.conn.Close() 83 | }() 84 | for { 85 | select { 86 | case message, ok := <-c.send: 87 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 88 | if !ok { 89 | // The hub closed the channel. 90 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 91 | return 92 | } 93 | 94 | w, err := c.conn.NextWriter(websocket.TextMessage) 95 | if err != nil { 96 | return 97 | } 98 | w.Write(message) 99 | 100 | // Add queued chat messages to the current websocket message. 101 | n := len(c.send) 102 | for i := 0; i < n; i++ { 103 | w.Write(newline) 104 | w.Write(<-c.send) 105 | } 106 | 107 | if err := w.Close(); err != nil { 108 | return 109 | } 110 | case <-ticker.C: 111 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 112 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 113 | return 114 | } 115 | } 116 | } 117 | } 118 | 119 | // Serve handles websocket requests from the peer. 120 | func Serve(hub *Hub, w http.ResponseWriter, r *http.Request) { 121 | conn, err := upgrader.Upgrade(w, r, nil) 122 | if err != nil { 123 | log.Println(err) 124 | return 125 | } 126 | client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)} 127 | client.hub.register <- client 128 | 129 | // Allow collection of memory referenced by the caller by doing all work in 130 | // new goroutines. 131 | go client.writePump() 132 | go client.readPump() 133 | } 134 | -------------------------------------------------------------------------------- /server/websocket/hub.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | // IHub notifies attached clients 8 | type IHub interface { 9 | Notify(msg []byte) 10 | } 11 | 12 | // Hub maintains the set of active clients and broadcasts messages to the 13 | // clients. 14 | type Hub struct { 15 | // Registered clients. 16 | clients map[*Client]bool 17 | 18 | // Inbound messages from the clients. 19 | broadcast chan []byte 20 | 21 | // Register requests from the clients. 22 | register chan *Client 23 | 24 | // Unregister requests from clients. 25 | unregister chan *Client 26 | } 27 | 28 | // NewHub constructor 29 | func NewHub() *Hub { 30 | return &Hub{ 31 | broadcast: make(chan []byte), 32 | register: make(chan *Client), 33 | unregister: make(chan *Client), 34 | clients: make(map[*Client]bool), 35 | } 36 | } 37 | 38 | // Run starts distributing messages to connected clients 39 | func (h *Hub) Run() { 40 | for { 41 | select { 42 | case client := <-h.register: 43 | h.clients[client] = true 44 | case client := <-h.unregister: 45 | if _, ok := h.clients[client]; ok { 46 | delete(h.clients, client) 47 | close(client.send) 48 | } 49 | case message := <-h.broadcast: 50 | for client := range h.clients { 51 | select { 52 | case client.send <- message: 53 | default: 54 | close(client.send) 55 | delete(h.clients, client) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | // Notify allows us to send information to all connected clients on the 63 | // broadcast channel. 64 | func (h *Hub) Notify(msg []byte) { 65 | h.broadcast <- msg 66 | } 67 | -------------------------------------------------------------------------------- /server/websocket/mocks/IHub.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // IHub is an autogenerated mock type for the IHub type 8 | type IHub struct { 9 | mock.Mock 10 | } 11 | 12 | // Notify provides a mock function with given fields: msg 13 | func (_m *IHub) Notify(msg []byte) { 14 | _m.Called(msg) 15 | } 16 | --------------------------------------------------------------------------------