├── .codecov.yml ├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── auto-release.yml │ ├── ci.yml │ ├── dependabot-approver.yml │ └── proj-xmidt-team.yml ├── .gitignore ├── .golangci.yaml ├── .release ├── docker │ ├── entrypoint.sh │ └── tr1d1um_spruce.yaml └── helm │ └── tr1d1um │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ └── tr1d1um.yaml │ └── values.yaml ├── .reuse └── dep5 ├── .whitesource ├── .yamllint.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── LICENSES └── Apache-2.0.txt ├── MAINTAINERS.md ├── Makefile ├── NOTICE ├── README.md ├── auth.go ├── basculeLogging.go ├── basculeLogging_test.go ├── go.mod ├── go.sum ├── main.go ├── metrics.go ├── primaryHandler.go ├── routes.go ├── setup.go ├── stat ├── endpoint.go ├── endpoint_test.go ├── mocks_test.go ├── service.go ├── service_test.go ├── transport.go └── transport_test.go ├── tr1d1um.yaml ├── transaction ├── context.go ├── errors.go ├── errors_test.go ├── transactor.go └── transactor_test.go └── translation ├── endpoint.go ├── endpoint_test.go ├── errors.go ├── mocks_test.go ├── service.go ├── service_test.go ├── transport.go ├── transport_test.go ├── transport_utils.go ├── transport_utils_test.go └── wdmp_type.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | coverage: 5 | range: 50..80 6 | round: down 7 | precision: 2 8 | 9 | ignore: 10 | - "*_test.go" 11 | - "vendor" 12 | 13 | fixes: 14 | - "github.com/xmidt-org/tr1d1um/::" 15 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor 2 | conf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | version: 2 5 | updates: 6 | 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | # Check for updates to GitHub Actions every day 11 | interval: "daily" 12 | labels: 13 | - "dependencies" 14 | commit-message: 15 | prefix: "chore" 16 | include: "scope" 17 | open-pull-requests-limit: 10 18 | 19 | - package-ecosystem: gomod 20 | directory: / 21 | schedule: 22 | interval: daily 23 | labels: 24 | - "dependencies" 25 | commit-message: 26 | prefix: "chore" 27 | include: "scope" 28 | open-pull-requests-limit: 10 29 | -------------------------------------------------------------------------------- /.github/workflows/auto-release.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | name: 'Auto Release' 5 | 6 | on: 7 | schedule: # Run every day at 12:00 UTC 8 | - cron: '0 12 * * *' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | release: 13 | uses: xmidt-org/shared-go/.github/workflows/auto-releaser.yml@58bcbad3b9da1c30ad6ccd1de226a95e6c238ed0 # v4.8.5 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - README.md 12 | - CONTRIBUTING.md 13 | - MAINTAINERS.md 14 | - LICENSE 15 | - NOTICE 16 | tags: 17 | - 'v[0-9]+.[0-9]+.[0-9]+' 18 | pull_request: 19 | workflow_dispatch: 20 | 21 | jobs: 22 | ci: 23 | uses: xmidt-org/shared-go/.github/workflows/ci.yml@58bcbad3b9da1c30ad6ccd1de226a95e6c238ed0 # v4.8.5 24 | with: 25 | release-type: program 26 | release-docker: true 27 | release-docker-latest: true 28 | release-arch-arm64: true 29 | release-docker-major: true 30 | release-docker-minor: true 31 | release-docker-extras: | 32 | .release/docker 33 | LICENSE 34 | NOTICE 35 | yaml-lint-skip: false 36 | secrets: inherit 37 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-approver.yml: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | ## SPDX-License-Identifier: Apache-2.0 3 | --- 4 | name: 'Dependabot auto approval' 5 | 6 | on: 7 | pull_request_target 8 | permissions: 9 | pull-requests: write 10 | contents: write 11 | 12 | jobs: 13 | package: 14 | uses: xmidt-org/.github/.github/workflows/dependabot-approver-template.yml@main 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.github/workflows/proj-xmidt-team.yml: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | ## SPDX-License-Identifier: Apache-2.0 3 | --- 4 | name: 'PROJ: xmidt-team' 5 | 6 | on: 7 | issues: 8 | types: 9 | - opened 10 | pull_request: 11 | types: 12 | - opened 13 | 14 | jobs: 15 | package: 16 | uses: xmidt-org/.github/.github/workflows/proj-template.yml@proj-v1 17 | secrets: inherit 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | bin 3 | pkg 4 | *.o 5 | *.out 6 | *.a 7 | *.so 8 | *.bak 9 | +src/tr1d1um/tr1d1um 10 | +src/tr1d1um/debug 11 | +src/tr1d1um/output 12 | 13 | # config 14 | -src/tr1d1um/tr1d1um.json 15 | +src/tr1d1um/tr1d1um.json 16 | +keys/*.private 17 | *.xml 18 | *.bk 19 | *.log 20 | *.iml 21 | 22 | src/tr1d1um/tr1d1um 23 | src/tr1d1um/debug 24 | src/tr1d1um/output 25 | 26 | # config 27 | src/tr1d1um/tr1d1um.json 28 | keys/*.private 29 | 30 | # VSCode 31 | *.code-workspace 32 | .vscode/* 33 | .dev/* 34 | 35 | # Vim 36 | *.swp 37 | *.out 38 | 39 | # Folders 40 | _obj 41 | _test 42 | coverage* 43 | vendor 44 | .ignore 45 | report.json 46 | 47 | # Architecture specific extensions/prefixes 48 | *.[568vq] 49 | [568vq].out 50 | 51 | *.cgo1.go 52 | *.cgo2.c 53 | _cgo_defun.c 54 | _cgo_gotypes.go 55 | _cgo_export.* 56 | _testmain.go 57 | 58 | *.exe 59 | *.test 60 | *.prof 61 | cpuprofile 62 | 63 | tr1d1um 64 | .ignore 65 | .vscode 66 | 67 | # helm 68 | !deploy/helm/tr1d1um 69 | rendered.yaml 70 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | ## SPDX-License-Identifier: Apache-2.0 3 | --- 4 | version: "2" 5 | linters: 6 | enable: 7 | - bodyclose 8 | - dupl 9 | - errorlint 10 | - goconst 11 | - gosec 12 | - misspell 13 | - prealloc 14 | - unconvert 15 | disable: 16 | - errcheck 17 | - ineffassign 18 | settings: 19 | errorlint: 20 | errorf: false 21 | misspell: 22 | locale: US 23 | exclusions: 24 | generated: lax 25 | presets: 26 | - comments 27 | - common-false-positives 28 | - legacy 29 | - std-error-handling 30 | rules: 31 | - linters: 32 | - dupl 33 | - funlen 34 | path: _test.go 35 | - path: main\.go # Accept TLSClientConfig with InsecureSkipVerify 36 | text: 'G402:' 37 | - path: main\.go # Accept pprof is automatically exposed 38 | text: 'G108:' 39 | - path: outboundSender\.go # Accept sha1 for signature 40 | text: 'G505:' 41 | - path: .go # Accept deprecated packages for now. 42 | text: 'SA1019:' 43 | paths: 44 | - third_party$ 45 | - builtin$ 46 | - examples$ 47 | formatters: 48 | exclusions: 49 | generated: lax 50 | paths: 51 | - third_party$ 52 | - builtin$ 53 | - examples$ 54 | -------------------------------------------------------------------------------- /.release/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 3 | # SPDX-License-Identifier: Apache-2.0 4 | set -e 5 | 6 | # check arguments for an option that would cause /tr1d1um to stop 7 | # return true if there is one 8 | _want_help() { 9 | local arg 10 | for arg; do 11 | case "$arg" in 12 | -'?'|--help|-v) 13 | return 0 14 | ;; 15 | esac 16 | done 17 | return 1 18 | } 19 | 20 | _main() { 21 | # if command starts with an option, prepend tr1d1um 22 | if [ "${1:0:1}" = '-' ]; then 23 | set -- /tr1d1um "$@" 24 | fi 25 | 26 | # skip setup if they aren't running /tr1d1um or want an option that stops /tr1d1um 27 | if [ "$1" = '/tr1d1um' ] && ! _want_help "$@"; then 28 | echo "Entrypoint script for tr1d1um Server ${VERSION} started." 29 | 30 | if [ ! -s /etc/tr1d1um/tr1d1um.yaml ]; then 31 | echo "Building out template for file" 32 | /bin/spruce merge /tmp/tr1d1um_spruce.yaml > /etc/tr1d1um/tr1d1um.yaml 33 | fi 34 | fi 35 | 36 | exec "$@" 37 | } 38 | 39 | _main "$@" 40 | -------------------------------------------------------------------------------- /.release/docker/tr1d1um_spruce.yaml: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | ## SPDX-License-Identifier: Apache-2.0 3 | --- 4 | 5 | ######################################## 6 | # Labeling/Tracing via HTTP Headers Configuration 7 | ######################################## 8 | 9 | # The unique fully-qualified-domain-name of the server. It is provided to 10 | # the X-Tr1d1um-Server header for showing what server fulfilled the request 11 | # sent. 12 | # (Optional) 13 | server: (( grab $HOSTNAME || "tr1d1um" )) 14 | 15 | # Provides this build number to the X-Tr1d1um-Build header for 16 | # showing machine version information. The build number SHOULD 17 | # match the scheme `version-build` but there is not a strict requirement. 18 | # (Optional) 19 | build: (( grab $BUILD || "unkown" )) 20 | 21 | # Provides the region information to the X-Tr1d1um-Region header 22 | # for showing what region this machine is located in. The region 23 | # is arbitrary and optional. 24 | # (Optional) 25 | region: "east" 26 | 27 | # Provides the flavor information to the X-Tr1d1um-Flavor header 28 | # for showing what flavor this machine is associated with. The flavor 29 | # is arbitrary and optional. 30 | # (Optional) 31 | flavor: "mint" 32 | 33 | touchstone: 34 | # DefaultNamespace is the prometheus namespace to apply when a metric has no namespace 35 | defaultNamespace: "xmidt" 36 | # DefaultSubsystem is the prometheus subsystem to apply when a metric has no subsystem 37 | defaultSubsystem: "tr1d1um" 38 | 39 | prometheus: 40 | defaultNamespace: xmidt 41 | defaultSubsystem: tr1d1um 42 | constLabels: 43 | development: "true" 44 | handler: 45 | maxRequestsInFlight: 5 46 | timeout: 5s 47 | instrumentMetricHandler: true 48 | 49 | health: 50 | disableLogging: false 51 | custom: 52 | server: development 53 | 54 | ######################################## 55 | # Primary Endpoint Configuration 56 | ######################################## 57 | 58 | servers: 59 | primary: 60 | address: :6100 61 | disableHTTPKeepAlives: true 62 | header: 63 | X-Midt-Server: 64 | - tr1d1um 65 | X-Midt-Version: 66 | - development 67 | metrics: 68 | address: :6101 69 | disableHTTPKeepAlives: true 70 | header: 71 | X-Midt-Server: 72 | - tr1d1um 73 | X-Midt-Version: 74 | - development 75 | health: 76 | address: :6102 77 | disableHTTPKeepAlives: true 78 | header: 79 | X-Midt-Server: 80 | - tr1d1um 81 | X-Midt-Version: 82 | - development 83 | pprof: 84 | address: :6103 85 | 86 | ######################################## 87 | # Logging Related Configuration 88 | ######################################## 89 | 90 | # log configures the logging details 91 | logging: 92 | level: (( grab $LOG_LEVEL || "debug" )) 93 | development: (( grab $LOG_DEVELOPMENT || true )) 94 | encoderConfig: 95 | messageKey: msg 96 | levelKey: level 97 | # reducedLoggingResponseCodes allows disabling verbose transaction logs for 98 | # benign responses from the target server given HTTP status codes. 99 | # (Optional) 100 | # reducedLoggingResponseCodes: [200, 504] 101 | 102 | ############################################################################## 103 | # Webhooks Related configuration 104 | ############################################################################## 105 | # webhookStore provides configuration for storing and obtaining webhook 106 | # information using argus. 107 | webhook: 108 | # disablePartnerIDs, if true, will allow webhooks to register without 109 | # checking the validity of the partnerIDs in the request 110 | # Defaults to 'false'. 111 | disablePartnerIDs: false 112 | 113 | # validation provides options for validating the webhook's URL and TTL 114 | # related fields. Some validation happens regardless of the configuration: 115 | # URLs must be a valid URL structure, the Matcher.DeviceID values must 116 | # compile into regular expressions, and the Events field must have at 117 | # least one value and all values must compile into regular expressions. 118 | validation: 119 | # url provides options for additional validation of the webhook's 120 | # Config.URL, FailureURL, and Config.AlternativeURLs fields. 121 | url: 122 | # httpsOnly will allow only URLs with https schemes through if true. 123 | # (Optional). Defaults to 'false'. 124 | httpsOnly: false 125 | 126 | # allowLoopback will allow any canonical or IP loopback address if 127 | # true. Otherwise, loopback addresses are considered invalid. 128 | # (Optional). Defaults to 'false'. 129 | allowLoopback: true 130 | 131 | # allowIP allows the different webhook URLs to have IP hostnames if set to true. 132 | # (Optional). Defaults to 'false'. 133 | allowIP: true 134 | 135 | # allowSpecialUseHosts allows URLs that include reserved domains if set to true. 136 | # Read more here: https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains 137 | # (Optional). Defaults to 'false'. 138 | allowSpecialUseHosts: true 139 | 140 | # allowSpecialUseIPs, if set to true, allows URLs that contain or route to IPs that have 141 | # been marked as reserved through various RFCs: rfc6761, rfc6890, rfc8190. 142 | # (Optional). Defaults to 'false'. 143 | allowSpecialUseIPs: true 144 | 145 | # invalidHosts is a slice that contains strings that we do not want 146 | # allowed in URLs, providing a way to deny certain domains or hostnames. 147 | # (Optional). Defaults to an empty slice. 148 | invalidHosts: [] 149 | 150 | # invalidSubnets is a list of IP subnets. If a URL contains an 151 | # IP or resolves to an IP in one of these subnets, the webhook is 152 | # considered invalid. 153 | # (Optional). Defaults to an empty slice. 154 | invalidSubnets: [] 155 | 156 | # ttl provides information for what is considered valid for time-related 157 | # fields (Duration and Until) in the webhook. A webhook set to expire 158 | # too far in the future is considered invalid, while a time in the past 159 | # is considered equivalent to a request to delete the webhook. 160 | # Regardless of this configuration, either Until or Duration must have a 161 | # non-zero value. 162 | ttl: 163 | # max is the length of time a webhook is allowed to live. The Duration 164 | # cannot be larger than this value, and the Until value cannot be set 165 | # later than the current time + max + jitter. 166 | max: (( grab $WEBHOOK_MAX_TTL || "1m" )) 167 | 168 | # jitter is the buffer time added when checking that the Until value is 169 | # valid. If there is a slight clock skew between servers or some delay 170 | # in the http request, jitter should help account for that when ensuring 171 | # that Until is not a time too far in the future. 172 | jitter: (( grab $WEBHOOK_TTL_JITTER || "10s" )) 173 | 174 | # JWTParserType establishes which parser type will be used by the JWT token 175 | # acquirer used by Argus. Options include 'simple' and 'raw'. 176 | # Simple: parser assumes token payloads have the following structure: https://github.com/xmidt-org/bascule/blob/c011b128d6b95fa8358228535c63d1945347adaa/acquire/bearer.go#L77 177 | # Raw: parser assumes all of the token payload == JWT token 178 | # (Optional). Defaults to 'simple'. 179 | JWTParserType: (( grab $WEBHOOK_JWT_PARSER_TYPE || "raw" )) 180 | BasicClientConfig: 181 | # listen is the subsection that configures the listening feature of the argus client 182 | # (Optional) 183 | listen: 184 | # pullInterval provides how often the current webhooks list gets refreshed. 185 | pullInterval: (( grab $ARGUS_PULL_INTERVAL || "5s" )) 186 | 187 | # bucket is the partition name where webhooks will be stored. 188 | bucket: (( grab $ARGUS_BUCKET || "webhooks" )) 189 | 190 | # address is Argus' network location. 191 | address: (( grab $ARGUS_ENDPOINT || "http://argus:6600" )) 192 | 193 | # auth the authentication method for argus. 194 | auth: 195 | # basic configures basic authentication for argus. 196 | # Must be of form: 'Basic xyz==' 197 | basic: (( concat "Basic " authToken )) 198 | 199 | # # jwt configures jwt style authentication for argus. 200 | jwt: 201 | # requestHeaders are added to the request for the token. 202 | # (Optional) 203 | # requestHeaders: 204 | # "": "" 205 | 206 | # authURL is the URL to access for the token. 207 | authURL: (( grab $ARGUS_ACQUIRE_JWT_URL || "http://themis:6501/issue" )) 208 | 209 | # timeout is how long the request to get the token will take before 210 | # timing out. 211 | timeout: (( grab $ARGUS_ACQUIRE_TIMEOUT || "1m" )) 212 | 213 | # buffer is the length of time before a token expires to get a new token. 214 | buffer: (( grab $ARGUS_ACQUIRE_BUFFER || "2m" )) 215 | 216 | ############################################################################## 217 | # Authorization Credentials 218 | ############################################################################## 219 | # jwtValidator provides Bearer auth configuration 220 | jwtValidator: 221 | Config: 222 | Resolve: 223 | # Template is a URI template used to fetch keys. This template may 224 | # use a single parameter named keyID, e.g. http://keys.com/{keyID}. 225 | # This field is required and has no default. 226 | 227 | Template: "http://themis:6500/keys/{keyID}" 228 | 229 | Refresh: 230 | Sources: 231 | # URI is the location where keys are served. By default, clortho supports 232 | # file://, http://, and https:// URIs, as well as standard file system paths 233 | # such as /etc/foo/bar.jwk. 234 | # 235 | # This field is required and has no default. 236 | - URI: "http://themis:6500/keys/available" 237 | 238 | 239 | authx: 240 | inbound: 241 | # basic is a list of Basic Auth credentials intended to be used for local testing purposes 242 | # WARNING! Be sure to remove this from your production config 243 | basic: ["dXNlcjpwYXNz"] 244 | 245 | # capabilityCheck provides the details needed for checking an incoming JWT's 246 | # capabilities. If the type of check isn't provided, no checking is done. The 247 | # type can be "monitor" or "enforce". If it is empty or a different value, no 248 | # checking is done. If "monitor" is provided, the capabilities are checked but 249 | # the request isn't rejected when there isn't a valid capability for the 250 | # request. Instead, a message is logged. When "enforce" is provided, a request 251 | # that doesn't have the needed capability is rejected. 252 | # 253 | # The capability is expected to have the format: 254 | # 255 | # {prefix}{endpoint}:{method} 256 | # 257 | # The prefix can be a regular expression. If it's empty, no capability check 258 | # is done. The endpoint is a regular expression that should match the endpoint 259 | # the request was sent to. The method is usually the method of the request, such as 260 | # GET. The accept all method is a catchall string that indicates the capability 261 | # is approved for all methods. 262 | # (Optional) 263 | # capabilityCheck: 264 | # # type provides the mode for capability checking. 265 | # type: "enforce" 266 | # # prefix provides the regex to match the capability before the endpoint. 267 | # prefix: "prefix Here" 268 | # # acceptAllMethod provides a way to have a capability that allows all 269 | # # methods for a specific endpoint. 270 | # acceptAllMethod: "all" 271 | # # endpointBuckets provides regular expressions to use against the request 272 | # # endpoint in order to group requests for a metric label. 273 | # endpointBuckets: 274 | # - "hook\\b" 275 | # - "hooks\\b" 276 | # - "device/.*/stat\\b" 277 | # - "device/.*/config\\b" 278 | 279 | 280 | ############################################################################## 281 | # WRP and XMiDT Cloud configurations 282 | ############################################################################## 283 | 284 | # targetURL is the base URL of the XMiDT cluster 285 | targetURL: (( grab $XMIDT_CLUSTER || "http://scytale:6300/api/v3" )) 286 | 287 | # WRPSource is used as 'source' field for all outgoing WRP Messages 288 | WRPSource: "dns:tr1d1um.example.com" 289 | 290 | # supportedServices is a list of endpoints we support for the WRP producing endpoints 291 | # we will soon drop this configuration 292 | supportedServices: 293 | - "config" 294 | 295 | 296 | ############################################################################## 297 | # HTTP Transaction Configurations 298 | ############################################################################## 299 | # timeouts that apply to the Argus HTTP client. 300 | # (Optional) By default, the values below will be used. 301 | argusClientTimeout: 302 | # clientTimeout is the timeout for requests made through this 303 | # HTTP client. This timeout includes connection time, any 304 | # redirects, and reading the response body. 305 | clientTimeout: 50s 306 | 307 | # netDialerTimeout is the maximum amount of time the HTTP Client Dialer will 308 | # wait for a connect to complete. 309 | netDialerTimeout: 5s 310 | 311 | # timeouts that apply to the XMiDT HTTP client. 312 | # (Optional) By default, the values below will be used. 313 | xmidtClientTimeout: 314 | # clientTimeout is the timeout for the requests made through this 315 | # HTTP client. This timeout includes connection time, any 316 | # redirects, and reading the response body. 317 | clientTimeout: 135s 318 | 319 | # requestTimeout is the timeout imposed on requests made by this client 320 | # through context cancellation. 321 | # TODO since clientTimeouts are implemented through context cancellations, 322 | # we might not need this. 323 | requestTimeout: 129s 324 | 325 | # netDialerTimeout is the maximum amount of time the HTTP Client Dialer will 326 | # wait for a connect to complete. 327 | netDialerTimeout: 5s 328 | 329 | 330 | # requestRetryInterval is the time between HTTP request retries against XMiDT 331 | requestRetryInterval: "2s" 332 | 333 | # requestMaxRetries is the max number of times an HTTP request is retried against XMiDT in 334 | # case of ephemeral errors 335 | requestMaxRetries: 2 336 | 337 | #authtoken used to make spruce work better for authAcquirer 338 | authToken: (( grab $AUTH_TOKEN || "dXNlcjpwYXNz" )) 339 | 340 | # authAcquirer enables configuring the JWT or Basic auth header value factory for outgoing 341 | # requests to XMiDT. If both types are configured, JWT will be preferred. 342 | # (Optional) 343 | authAcquirer: 344 | # JWT: 345 | # # requestHeaders are added to the request for the token. 346 | # # (Optional) 347 | # # requestHeaders: 348 | # # "": "" 349 | 350 | # # authURL is the URL to access for the token. 351 | # authURL: "" 352 | 353 | # # timeout is how long the request to get the token will take before 354 | # # timing out. 355 | # timeout: "1m" 356 | 357 | # # buffer is the length of time before a token expires to get a new token. 358 | # buffer: "2m" 359 | 360 | Basic: (( concat "Basic " authToken )) 361 | 362 | # tracing provides configuration around traces using OpenTelemetry. 363 | # (Optional). By default, a 'noop' tracer provider is used and tracing is disabled. 364 | tracing: 365 | # provider is the provider name. Currently, stdout, jaegar and zipkin are supported. 366 | # 'noop' can also be used as provider to explicitly disable tracing. 367 | provider: (( grab $TRACING_PROVIDER_NAME || "noop" )) 368 | 369 | # skipTraceExport only applies when provider is stdout. Set skipTraceExport to true 370 | # so that trace information is not written to stdout. 371 | # skipTraceExport: true 372 | 373 | # endpoint is where trace information should be routed. Applies to zipkin and jaegar. 374 | endpoint: (( grab $TRACING_PROVIDER_ENDPOINT || "http://localhost:9411/api/v2/spans" )) 375 | 376 | # previousVersionSupport allows us to support two different major versions of 377 | # the API at the same time from the same application. When this is true, 378 | # tr1d1um will support both "/v2" and "/v3" endpoints. When false, only "/v3" 379 | # endpoints will be supported. 380 | previousVersionSupport: (( grab $PREV_VERSION_SUPPORT || true )) 381 | -------------------------------------------------------------------------------- /.release/helm/tr1d1um/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /.release/helm/tr1d1um/Chart.yaml: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | ## SPDX-License-Identifier: Apache-2.0 3 | apiVersion: v2 4 | name: tr1d1um 5 | description: A Helm chart to deploy tr1d1um to Kubernetes 6 | 7 | # A chart can be either an 'application' or a 'library' chart. 8 | # 9 | # Application charts are a collection of templates that can be packaged into versioned archives 10 | # to be deployed. 11 | # 12 | # Library charts provide useful utilities or functions for the chart developer. They're included as 13 | # a dependency of application charts to inject those utilities and functions into the rendering 14 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 15 | type: application 16 | 17 | # This is the chart version. This version number should be incremented each time you make changes 18 | # to the chart and its templates, including the app version. 19 | version: 0.1.5 20 | 21 | # This is the version number of the application being deployed. This version number should be 22 | # incremented each time you make changes to the application. 23 | appVersion: 0.1.5 24 | 25 | dependencies: 26 | -------------------------------------------------------------------------------- /.release/helm/tr1d1um/templates/tr1d1um.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | apiVersion: v1 5 | data: 6 | tr1d1um.yaml: | 7 | ######################################## 8 | # Labeling/Tracing via HTTP Headers Configuration 9 | ######################################## 10 | 11 | # The unique fully-qualified-domain-name of the server. It is provided to 12 | # the X-Tr1d1um-Server header for showing what server fulfilled the request 13 | # sent. 14 | # (Optional) 15 | server: "tr1d1um-local-instance-123.example.com" 16 | 17 | # Provides this build number to the X-Tr1d1um-Build header for 18 | # showing machine version information. The build number SHOULD 19 | # match the scheme `version-build` but there is not a strict requirement. 20 | # (Optional) 21 | build: "0.1.3-434" 22 | 23 | # Provides the region information to the X-Tr1d1um-Region header 24 | # for showing what region this machine is located in. The region 25 | # is arbitrary and optional. 26 | # (Optional) 27 | region: "east" 28 | 29 | # Provides the flavor information to the X-Tr1d1um-Flavor header 30 | # for showing what flavor this machine is associated with. The flavor 31 | # is arbitrary and optional. 32 | # (Optional) 33 | flavor: "mint" 34 | 35 | 36 | ############################################################################## 37 | # WebPA Service configuration 38 | ############################################################################## 39 | 40 | # For a complete view of the service config structure, 41 | # checkout https://godoc.org/github.com/xmidt-org/webpa-common/server#WebPA 42 | 43 | ######################################## 44 | # Primary Endpoint Configuration 45 | ######################################## 46 | 47 | # primary provides the configuration for the main server for this application 48 | primary: 49 | address: "{{ .Values.tr1d1um.address.host }}:{{ .Values.tr1d1um.address.port }}" 50 | 51 | ######################################## 52 | # Health Endpoint Configuration 53 | ######################################## 54 | 55 | # health defines the details needed for the health check endpoint. The 56 | # health check endpoint is generally used by services (like AWS Route53 57 | # or consul) to determine if this particular machine is healthy or not. 58 | health: 59 | address: "{{ .Values.health.address.host }}:{{ .Values.health.address.port }}" 60 | 61 | ######################################## 62 | # Debugging/Pprof Configuration 63 | ######################################## 64 | 65 | # pprof defines the details needed for the pprof debug endpoint. 66 | # (Optional) 67 | pprof: 68 | address: "{{ .Values.pprof.address.host }}:{{ .Values.pprof.address.port }}" 69 | 70 | ######################################## 71 | # Metrics Configuration 72 | ######################################## 73 | 74 | # metric defines the details needed for the prometheus metrics endpoint 75 | # (Optional) 76 | metric: 77 | address: "{{ .Values.metric.address.host }}:{{ .Values.metric.address.port }}" 78 | metricsOptions: 79 | # namespace is the namespace of the metrics provided 80 | # (Optional) 81 | namespace: "webpa" 82 | 83 | # subsystem is the subsystem of the metrics provided 84 | # (Optional) 85 | subsystem: "tr1d1um" 86 | 87 | ######################################## 88 | # Logging Related Configuration 89 | ######################################## 90 | 91 | # log configures the logging subsystem details 92 | log: 93 | # file is the name of the most recent log file. If set to "stdout" this 94 | # will log to os.Stdout. 95 | # (Optional) defaults to os.TempDir() 96 | file: "stdout" 97 | 98 | # level is the logging level to use - INFO, DEBUG, WARN, ERROR 99 | # (Optional) defaults to ERROR 100 | level: "DEBUG" 101 | 102 | # maxsize is the maximum log file size in MB 103 | # (Optional) defaults to max 100MB 104 | maxsize: 50 105 | 106 | # maxage is the maximum number of days to retain old log files 107 | # (Optional) defaults to ignore age limit (0) 108 | maxage: 30 109 | 110 | # maxbackups is the maximum number of old log files to retain 111 | # (Optional) defaults to retain all (0) 112 | maxbackups: 10 113 | 114 | # json is a flag indicating whether JSON logging output should be used. 115 | # (Optional) defaults to false 116 | json: true 117 | 118 | 119 | ############################################################################## 120 | # Webhooks Related configuration 121 | ############################################################################## 122 | 123 | # webhooksEnabled indicates whether or not the webhooks server should be started 124 | # It is disabled for local testing 125 | webhooksEnabled: false 126 | 127 | # The unique fully-qualified-domain-name of the server. The webhooks library uses it 128 | # to know which host to use to confirm this service is ready to receive events 129 | # (Optional if not running webhooks) 130 | fqdn: "tr1d1um-local-instance-123.example.com" 131 | 132 | # start contains configuration for the logic by which Tr1d1um can 133 | # fetch the current WebPA webhooks without having to wait for SNS 134 | # It does so by pinging the rest of the cluter at the specified apiPath 135 | # More details at https://godoc.org/github.com/xmidt-org/webpa-common/webhook#StartConfig 136 | start: 137 | # duration is the max amount of time allowed to wait for webhooks data to be retrieved 138 | duration: "20s" 139 | 140 | # path used to query the existing webhooks 141 | apiPath: http://tr1d1um:6100/hooks 142 | 143 | ######################################## 144 | # Webhooks DNS readiness Configuration 145 | ######################################## 146 | 147 | # WaitForDns is the duration the webhooks library will wait for this server's DNS record to be 148 | # propagated. This waiting logic is important so AWS SNS webhook confirmations are not missed 149 | waitForDns: "30s" 150 | 151 | #soa stands for Start of Authority and it's a type of record in a DNS 152 | soa: 153 | # provider is the SOA provider used to verify DNS record readiness of this service 154 | provider: "example-123.awsdns-00.com:17" 155 | 156 | ######################################## 157 | # Webhooks AWS SNS Configuration 158 | ######################################## 159 | 160 | # aws provides the AWS SNS configurations the webhooks library needs 161 | aws: 162 | #AWS access key 163 | accessKey: "" 164 | 165 | #AWS secret key 166 | secretKey: "" 167 | 168 | env: local-dev 169 | 170 | sns: 171 | # awsEndpoint is the AWS endpoint 172 | # this must be left out in produ 173 | awsEndpoint: http://goaws:4100 174 | 175 | #region is the AWS SNS region 176 | region: "us-east-1" 177 | 178 | # topicArn describes the SNS topic this server needs to subscribe to 179 | topicArn: arn:aws:sns:us-east-1:000000000000:xmidt-local-caduceus 180 | 181 | #urlPath is the URL path SNS will use to confirm a subscription with this server 182 | urlPath: "/api/v2/aws/sns" 183 | 184 | 185 | ############################################################################## 186 | # Testing Authorization Credentials 187 | ############################################################################## 188 | 189 | # authHeader is a list of Basic Auth credentials intended to be used for local testing purposes 190 | # WARNING! Be sure to remove this from your production config 191 | authHeader: ["dXNlcjpwYXNz"] 192 | 193 | # jwtValidator provides Bearer auth configuration 194 | jwtValidator: 195 | keys: 196 | factory: 197 | uri: "http://sample-jwt-validator-uri/{keyId}" 198 | purpose: 0 199 | updateInterval: 604800000000000 200 | 201 | ############################################################################## 202 | # WRP and XMiDT Cloud configurations 203 | ############################################################################## 204 | 205 | # targetURL is the base URL of the XMiDT cluster 206 | targetURL: http://scytale:6300 207 | 208 | # WRPSource is used as 'source' field for all outgoing WRP Messages 209 | WRPSource: "dns:tr1d1um.example.com" 210 | 211 | # supportedServices is a list of endpoints we support for the WRP producing endpoints 212 | # we will soon drop this configuration 213 | supportedServices: 214 | - "config" 215 | 216 | 217 | ############################################################################## 218 | # HTTP Transaction Configurations 219 | ############################################################################## 220 | 221 | # clientTimeout is the timeout for the HTTP clients used to contact the XMiDT cloud 222 | clientTimeout: "135s" 223 | 224 | # respWaitTimeout is the max time Tr1d1um will wait for responses from the XMiDT cloud 225 | respWaitTimeout: "129s" 226 | 227 | # netDialerTimeout is the timeout used for the net dialer used within HTTP clients 228 | netDialerTimeout: "5s" 229 | 230 | # requestRetryInterval is the time between HTTP request retries against XMiDT 231 | requestRetryInterval: "2s" 232 | 233 | # requestMaxRetries is the max number of times an HTTP request is retried against XMiDT in 234 | # case of ephemeral errors 235 | requestMaxRetries: 2 236 | kind: ConfigMap 237 | metadata: 238 | labels: 239 | app: xmidt-app 240 | name: tr1d1um-config 241 | --- 242 | apiVersion: v1 243 | kind: Service 244 | metadata: 245 | annotations: 246 | service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" 247 | labels: 248 | component: tr1d1um 249 | release: tr1d1um 250 | name: tr1d1um 251 | spec: 252 | clusterIP: None 253 | ports: 254 | - name: primary 255 | port: {{ .Values.tr1d1um.address.port }} 256 | protocol: TCP 257 | - name: health 258 | port: {{ .Values.health.address.port }} 259 | protocol: TCP 260 | - name: pprof 261 | port: {{ .Values.pprof.address.port }} 262 | protocol: TCP 263 | - name: metric 264 | port: {{ .Values.metric.address.port }} 265 | protocol: TCP 266 | selector: 267 | app: xmidt-app-tr1d1um 268 | --- 269 | apiVersion: apps/v1 270 | kind: StatefulSet 271 | metadata: 272 | name: tr1d1um 273 | labels: 274 | app: xmidt-app-tr1d1um 275 | spec: 276 | selector: 277 | matchLabels: 278 | app: xmidt-app-tr1d1um 279 | updateStrategy: 280 | type: RollingUpdate 281 | replicas: 1 282 | serviceName: xmidt-app 283 | template: 284 | metadata: 285 | labels: 286 | app: xmidt-app-tr1d1um 287 | spec: 288 | affinity: 289 | podAntiAffinity: 290 | requiredDuringSchedulingIgnoredDuringExecution: 291 | - topologyKey: "kubernetes.io/hostname" 292 | labelSelector: 293 | matchExpressions: 294 | - key: app 295 | operator: In 296 | values: 297 | - xmidt-app-tr1d1um 298 | volumes: 299 | - name: tr1d1um-config 300 | projected: 301 | sources: 302 | - configMap: 303 | name: tr1d1um-config 304 | items: 305 | - key: tr1d1um.yaml 306 | path: tr1d1um.yaml 307 | mode: 0755 308 | securityContext: 309 | runAsNonRoot: false 310 | runAsUser: 999 311 | supplementalGroups: [999] 312 | containers: 313 | - image: {{ .Values.tr1d1um.image }} 314 | name: tr1d1um 315 | ports: 316 | - containerPort: {{ .Values.tr1d1um.address.port }} 317 | protocol: TCP 318 | - containerPort: {{ .Values.health.address.port }} 319 | protocol: TCP 320 | - containerPort: {{ .Values.pprof.address.port }} 321 | protocol: TCP 322 | - containerPort: {{ .Values.metric.address.port }} 323 | protocol: TCP 324 | volumeMounts: 325 | - name: tr1d1um-config 326 | mountPath: "/etc/tr1d1um" 327 | readOnly: true 328 | {{ if (.Values.imagePullSecretName) }} 329 | imagePullSecrets: 330 | - name: {{ .Values.imagePullSecretName }}} 331 | {{ end }} 332 | -------------------------------------------------------------------------------- /.release/helm/tr1d1um/values.yaml: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | ## SPDX-License-Identifier: Apache-2.0 3 | # Default values for xmitd-talaria. 4 | --- 5 | tr1d1um: 6 | # docker image used 7 | image: xmidt/tr1d1um 8 | address: 9 | host: "" 10 | port: "6100" 11 | 12 | health: 13 | address: 14 | host: "" 15 | port: "6101" 16 | 17 | pprof: 18 | address: 19 | host: "" 20 | port: "6102" 21 | 22 | control: 23 | address: 24 | host: "" 25 | port: "6103" 26 | 27 | metric: 28 | address: 29 | host: "" 30 | port: "6104" 31 | 32 | # Pull secret used when images are stored in a private repository 33 | # imagePullSecretName: 34 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: tr1d1um 3 | Upstream-Contact: John Bass 4 | Source: https://github.com/xmidt-org/tr1d1um 5 | 6 | Files: NOTICE 7 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 8 | License: Apache-2.0 9 | 10 | Files: .gitignore 11 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 12 | License: Apache-2.0 13 | 14 | Files: .whitesource 15 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 16 | License: Apache-2.0 17 | 18 | Files: CONTRIBUTING.md 19 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 20 | License: Apache-2.0 21 | 22 | Files: MAINTAINERS.md 23 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 24 | License: Apache-2.0 25 | 26 | Files: README.md 27 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 28 | License: Apache-2.0 29 | 30 | Files: go.mod 31 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 32 | License: Apache-2.0 33 | 34 | Files: go.sum 35 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 36 | License: Apache-2.0 37 | 38 | Files: CHANGELOG.md 39 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 40 | License: Apache-2.0 41 | 42 | Files: .dockerignore 43 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 44 | License: Apache-2.0 45 | 46 | Files: .release/helm/tr1d1um/.helmignore 47 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 48 | License: Apache-2.0 49 | 50 | Files: Makefile 51 | Copyright: SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 52 | License: Apache-2.0 53 | 54 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW" 11 | } 12 | } -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | 5 | extends: default 6 | 7 | ignore: 8 | - .release/helm/tr1d1um/templates/tr1d1um.yaml 9 | 10 | rules: 11 | braces: 12 | level: warning 13 | max-spaces-inside: 1 14 | brackets: 15 | level: warning 16 | max-spaces-inside: 1 17 | colons: 18 | level: warning 19 | max-spaces-after: -1 20 | commas: 21 | level: warning 22 | comments: disable 23 | comments-indentation: disable 24 | document-start: 25 | present: true 26 | empty-lines: 27 | max: 2 28 | hyphens: 29 | max-spaces-after: 1 30 | indentation: 31 | level: error 32 | indent-sequences: consistent 33 | line-length: 34 | level: warning 35 | max: 90 36 | allow-non-breakable-words: true 37 | allow-non-breakable-inline-mappings: true 38 | truthy: disable 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [v0.9.5] 10 | - Update tr1d1um config for docker so themis can be used for jwt auth. 11 | 12 | ## [v0.9.4] 13 | - Disable arm64 builds (temporary) 14 | 15 | ## [v0.9.3] 16 | - First release under the new release infrastructure 17 | - Patch [Device level 403 errors are not properly propagated to HTTP response codes](https://github.com/xmidt-org/tr1d1um/issues/397) 18 | 19 | ## [v0.9.2] 20 | - Updated sallust to v0.2.2 to support custom log file permissions 21 | 22 | ## [v0.9.1] 23 | - [Add a structured log field for just the normalized device-id. #391](https://github.com/xmidt-org/tr1d1um/pull/391) 24 | 25 | ## [v0.9.0] 26 | - [Tracing working, Correct tracestate spelling, pass on empty tracestate if none is provided #389](https://github.com/xmidt-org/tr1d1um/pull/389) 27 | 28 | ## [v0.8.5] 29 | - [Add Trace info to WRP Message Header #385](https://github.com/xmidt-org/tr1d1um/pull/385) 30 | - [Update Tracing Configurations #386](https://github.com/xmidt-org/tr1d1um/pull/386) 31 | 32 | ## [v0.8.4] 33 | - Update dependencies 34 | 35 | ## [v0.8.3] 36 | - [Bug: Event related metrics fail to update #361](https://github.com/xmidt-org/tr1d1um/issues/361) 37 | - [Tr1d1um Panic: device status code used as the response status code can cause an unrecoverable panic #354](https://github.com/xmidt-org/tr1d1um/issues/354) 38 | 39 | ## [v0.8.2] 40 | - [Fix jwtValidator & capabilityCheck config unpacking #359](https://github.com/xmidt-org/tr1d1um/issues/359) 41 | 42 | ## [v0.8.1] 43 | - [Replace go-kit/kit/log & go-kit/log with zap #356](https://github.com/xmidt-org/tr1d1um/pull/356) 44 | 45 | ## [v0.8.0] 46 | - Updated tracing configuration documentation in tr1d1um.yaml to reflect changes in Candlelight [#346](https://github.com/xmidt-org/tr1d1um/pull/346) 47 | - [https://github.com/xmidt-org/tr1d1um/issues/340](https://github.com/xmidt-org/tr1d1um/issues/340) 48 | - [v2 & v3 endpoints are not enforcing capabilities xmidt-org/tr1d1um#342](https://github.com/xmidt-org/tr1d1um/issues/342) 49 | - [v3 endpoint is not validating webhooks #341](https://github.com/xmidt-org/tr1d1um/issues/341) 50 | - [8090 support has been removed #343](https://github.com/xmidt-org/tr1d1um/issues/343) 51 | - [Remove Deprecated webpa-common #304](https://github.com/xmidt-org/tr1d1um/issues/304) 52 | 53 | ## [v0.7.12] 54 | - [`/api/v2/device/` 500 EOF Error #328](https://github.com/xmidt-org/tr1d1um/issues/328) 55 | - [Remove nonstandard charset for media type JSON encodings #336](https://github.com/xmidt-org/tr1d1um/issues/336) 56 | - [Create TransactionUUID if not provided #334](https://github.com/xmidt-org/tr1d1um/issues/334) 57 | 58 | ## [v0.7.11] 59 | - [No Prom Metrics Being Produced #329](https://github.com/xmidt-org/tr1d1um/issues/329) 60 | 61 | ## [v0.7.10] 62 | - Remove several unused build files and update the docker images to work. [#325](https://github.com/xmidt-org/tr1d1um/pull/325) 63 | 64 | ## [v0.7.9] 65 | - Patch [#320](https://github.com/xmidt-org/tr1d1um/issues/320) 66 | - [CVE-2022-32149 (High) detected in golang.org/x/text-v0.3.7](https://github.com/xmidt-org/tr1d1um/issues/317) 67 | 68 | ## [v0.7.8] 69 | - Add support for an alternate server https://github.com/xmidt-org/tr1d1um/pull/297 70 | 71 | ## [v0.7.7] 72 | - Uber fx integration 73 | - https://github.com/xmidt-org/tr1d1um/issues/288 74 | - https://github.com/xmidt-org/tr1d1um/issues/291 75 | - hub.com/xmidt-org/tr1d1um/issues/246 76 | - Major changes to server config, sections changed 77 | - `primary` => `servers.primary` 78 | - `alternate ` => `servers.alternate` 79 | - `health ` => `servers.health` 80 | - `pprof ` => `servers.pprof` 81 | - `alternate` => `servers.alternate` 82 | - Sections `log` and `zap` were replaced with `logging` 83 | - Section `touchstone ` was replaced with `prometheus` 84 | 85 | ## [v0.7.6] 86 | - Dependency update 87 | - guardrails says github.com/gorilla/websocket v1.5.0 has a high vulnerability but no vulnerabilities have been filed 88 | - [github.com/gorilla/sessions v1.2.1 cwe-613 no patch available](https://ossindex.sonatype.org/vulnerability/sonatype-2021-4899) 89 | - JWT Migration #289 90 | - update to use clortho `Resolver` & `Refresher` 91 | - update to use clortho `metrics` & `logging` 92 | - Update ancla client initialization 93 | - Update Config 94 | - Use [uber/zap](https://github.com/uber-go/zap) for clortho logging 95 | - Use [xmidt-org/sallust](https://github.com/xmidt-org/sallust) for the zap config unmarshalling 96 | - Update auth config for clortho 97 | - Update ancla config 98 | 99 | ## [v0.7.4] 100 | - Updated v2 hook endpoint to only enforce loopback validation (when configured). [#277](https://github.com/xmidt-org/tr1d1um/pull/277) 101 | 102 | ## [v0.7.3] 103 | - Bumped ancla to v0.3.9 to fix Duration bug in webhook registration - Duration should be an int in seconds. It will also accept strings such as "5m". [#270](https://github.com/xmidt-org/tr1d1um/pull/270) 104 | - Updated v2 webhook registration to allow for no Duration or Until set. [#270](https://github.com/xmidt-org/tr1d1um/pull/270) 105 | 106 | ## [v0.7.2] 107 | - Fixed v2 endpoint to allow for invalid duration or until fields. When they are invalid, the duration of the webhook is set to the configured maximum. [#266](https://github.com/xmidt-org/tr1d1um/pull/266) 108 | 109 | ## [v0.7.1] 110 | - Renamed common folder and reallocated util.go functions. [#235](https://github.com/xmidt-org/tr1d1um/pull/235) 111 | - Separated main.go into main.go and primaryHandler.go. [#239](https://github.com/xmidt-org/tr1d1um/pull/239) 112 | - Updated spec file and rpkg version macro to be able to choose when the 'v' is included in the version. [#242](https://github.com/xmidt-org/tr1d1um/pull/242) 113 | - Added configurable support for v2 endpoints with current v3 ones from the same application. [#249](https://github.com/xmidt-org/tr1d1um/pull/249) 114 | - Added configurable support for v2 endpoints with current v3 ones from the same application. [#249](https://github.com/xmidt-org/tr1d1um/pull/249) 115 | 116 | ## [v0.7.0] 117 | - Bumped argus to v0.6.0, bumped ancla to v0.3.5, and changed errorEncoder to pull logger from context.[#233](https://github.com/xmidt-org/tr1d1um/pull/233) 118 | - Updated api version in url to v3 to indicate breaking changes in response codes when an invalid auth is sent. This change was made in an earlier release (v0.5.10). [#234](https://github.com/xmidt-org/tr1d1um/pull/234) 119 | - Updated target URL to not have an api base hard coded onto it. Instead, the base should be provided as a part of the configuration value. [#234](https://github.com/xmidt-org/tr1d1um/pull/234) 120 | 121 | ## [v0.6.4] 122 | - Bumped ancla to v0.3.4: 123 | - Changed server log source address field. [#231](https://github.com/xmidt-org/tr1d1um/pull/231) 124 | - Fixes a problem with wiring together configuration for the Duration and Until webhook validations. [#232](https://github.com/xmidt-org/tr1d1um/pull/232) 125 | - Improves logging. [#232](https://github.com/xmidt-org/tr1d1um/pull/232) 126 | 127 | ## [v0.6.3] 128 | - Added configuration for partnerID check. [#229](https://github.com/xmidt-org/tr1d1um/pull/229) 129 | - Bumped ancla to v0.3.2 [#229](https://github.com/xmidt-org/tr1d1um/pull/229) 130 | 131 | ## [v0.6.2] 132 | - Bumped ancla to fix http bug. [#228](https://github.com/xmidt-org/tr1d1um/pull/228) 133 | 134 | ## [v0.6.1] 135 | - Fixed the webhook endpoint to return 400 instead of 500 for webhook validation. [#225](https://github.com/xmidt-org/tr1d1um/pull/225) 136 | 137 | ## [v0.6.0] 138 | - Integrated webhook validator and added documentation and configuration for it. [#224](https://github.com/xmidt-org/tr1d1um/pull/224) 139 | - Bump bascule version which includes a security vulnerability fix. [#223](https://github.com/xmidt-org/tr1d1um/pull/223) 140 | 141 | ## [v0.5.10] 142 | - Keep setter and getter unexported. [#219](https://github.com/xmidt-org/tr1d1um/pull/219) 143 | - Prevent Authorization header from getting logged. [#218](https://github.com/xmidt-org/tr1d1um/pull/218) 144 | - Bumped ancla, webpa-common versions. [#222](https://github.com/xmidt-org/tr1d1um/pull/222) 145 | 146 | ## [v0.5.9] 147 | - Add support for acquiring Themis tokens through Ancla. [#215](https://github.com/xmidt-org/tr1d1um/pull/215) 148 | 149 | ## [v0.5.8] 150 | - Use official ancla release and include bascule updates. [#213](https://github.com/xmidt-org/tr1d1um/pull/213) 151 | 152 | 153 | ## [v0.5.7] 154 | - Fix bug where OTEL trace context was not propagated from server to outgoing client requests [#211](https://github.com/xmidt-org/tr1d1um/pull/211) 155 | 156 | ## [v0.5.6] 157 | - Make OpenTelemetry tracing an optional feature. [#207](https://github.com/xmidt-org/tr1d1um/pull/207) 158 | 159 | ## [v0.5.5] 160 | - Initial OpenTelemetry integration. [#197](https://github.com/xmidt-org/tr1d1um/pull/197) thanks to @Sachin4403 161 | - OpenTelemetry integration in webhook endpoints which was skipped in earlier PR. [#201](https://github.com/xmidt-org/tr1d1um/pull/201) thanks to @Sachin4403 162 | 163 | ## [v0.5.4] 164 | ### Changed 165 | - Migrate to github actions, normalize analysis tools, Dockerfiles and Makefiles. [#186](https://github.com/xmidt-org/tr1d1um/pull/186) 166 | - Bump webpa-common version with xwebhook item ID format update. [#192](https://github.com/xmidt-org/tr1d1um/pull/192) 167 | - Update webhook logic library to xmidt-org/ancla. [#194](https://github.com/xmidt-org/tr1d1um/pull/194) 168 | 169 | ### Fixed 170 | 171 | - Fix bug in which Tr1d1um was not capturing partnerIDs correctly due to casting error. [#182](https://github.com/xmidt-org/tr1d1um/pull/182) 172 | 173 | ### Changed 174 | - Update buildtime format in Makefile to match RPM spec file. [#185](https://github.com/xmidt-org/tr1d1um/pull/185) 175 | 176 | ## [v0.5.3] 177 | ### Fixed 178 | - Bug in which only mTLS was allowed as valid config for a webpa server. [#181](https://github.com/xmidt-org/tr1d1um/pull/181) 179 | 180 | ## [v0.5.2] 181 | ### Changed 182 | - Update Argus integration. [#175](https://github.com/xmidt-org/tr1d1um/pull/175) 183 | - Switched SNS to argus. [#168](https://github.com/xmidt-org/tr1d1um/pull/168) 184 | - Update references to the main branch. [#144](https://github.com/xmidt-org/talaria/pull/144) 185 | - Bumped bascule, webpa-common, and wrp-go versions. [#173](https://github.com/xmidt-org/tr1d1um/pull/173) 186 | 187 | ## [v0.5.1] 188 | ### Fixed 189 | - Specify allowed methods for webhook endpoints. [#163](https://github.com/xmidt-org/tr1d1um/pull/163) 190 | - Revert to default http mux routeNotFound handler for consistency. [#163](https://github.com/xmidt-org/tr1d1um/pull/163) 191 | - Json content type header should only be specified in 200 OK responses for stat endpoint. [#166](https://github.com/xmidt-org/tr1d1um/pull/166) 192 | - Add special field in spruce config yml. [#159](https://github.com/xmidt-org/tr1d1um/pull/159) 193 | 194 | ### Added 195 | - Add docker entrypoint. [154](https://github.com/xmidt-org/tr1d1um/pull/154) 196 | 197 | ### Changed 198 | - Register for specific OS signals. [#162](https://github.com/xmidt-org/tr1d1um/pull/162) 199 | 200 | ## [v0.5.0] 201 | - Add optional config for tr1d1um to use its own authentication tokens (basic and jwt supported). [#148](https://github.com/xmidt-org/tr1d1um/pull/148) 202 | - Remove mention of XPC team in error message. [#150](https://github.com/xmidt-org/tr1d1um/pull/150) 203 | - Bump golang version. [#152](https://github.com/xmidt-org/tr1d1um/pull/152) 204 | - Use scratch as docker base image instead of alpine. [#152](https://github.com/xmidt-org/tr1d1um/pull/152) 205 | - Add docker automation. [#152](https://github.com/xmidt-org/tr1d1um/pull/152) 206 | 207 | ## [v0.4.0] 208 | - Fix a bug in which tr1d1um was returning 500 for user error requests. [#146](https://github.com/xmidt-org/tr1d1um/pull/146) 209 | - Added endpoint regex configuration for capabilityCheck metric. [#147](https://github.com/xmidt-org/tr1d1um/pull/147) 210 | 211 | ## [v0.3.0] 212 | - Add feature to disable verbose transaction logger. [#145](https://github.com/xmidt-org/tr1d1um/pull/145) 213 | - Changed WRP message source. [#144](https://github.com/xmidt-org/tr1d1um/pull/144) 214 | 215 | ## [v0.2.1] 216 | - Moving partnerIDs to tr1d1um. 217 | - Added fix to correctly parse URL for capability checking. [#142](https://github.com/xmidt-org/tr1d1um/pull/142) 218 | 219 | ## [v0.2.0] 220 | - Bumped bascule, webpa-common, and wrp-go. 221 | - Removed temporary `/iot` endpoint. 222 | - Updated release pipeline to use travis. [#135](https://github.com/xmidt-org/tr1d1um/pull/135) 223 | - Added configurable way to check capabilities and put results into metrics, without rejecting requests. [#137](https://github.com/xmidt-org/tr1d1um/pull/137) 224 | 225 | ## [v0.1.5] 226 | - Migrated from glide to go modules. 227 | - Bumped bascule version and removed any dependencies on webpa-common secure package. 228 | 229 | ## [v0.1.4] 230 | - Add logging of WDMP parameters. 231 | 232 | ## [v0.1.2] 233 | - Switching to new build process. 234 | 235 | ## [0.1.1] - 2018-04-06 236 | ### Added 237 | - Initial creation. 238 | 239 | [Unreleased]: https://github.com/xmidt-org/tr1d1um/compare/v0.9.5...HEAD 240 | [v0.9.5]: https://github.com/xmidt-org/tr1d1um/compare/v0.9.4...v0.9.5 241 | [v0.9.4]: https://github.com/xmidt-org/tr1d1um/compare/v0.9.3...v0.9.4 242 | [v0.9.3]: https://github.com/xmidt-org/tr1d1um/compare/v0.9.2...v0.9.3 243 | [v0.9.2]: https://github.com/xmidt-org/tr1d1um/compare/v0.9.1...v0.9.2 244 | [v0.9.1]: https://github.com/xmidt-org/tr1d1um/compare/v0.9.0...v0.9.1 245 | [v0.9.0]: https://github.com/xmidt-org/tr1d1um/compare/v0.8.5...v0.9.0 246 | [v0.8.5]: https://github.com/xmidt-org/tr1d1um/compare/v0.8.4...v0.8.5 247 | [v0.8.4]: https://github.com/xmidt-org/tr1d1um/compare/v0.8.3...v0.8.4 248 | [v0.8.3]: https://github.com/xmidt-org/tr1d1um/compare/v0.8.2...v0.8.3 249 | [v0.8.2]: https://github.com/xmidt-org/tr1d1um/compare/v0.8.1...v0.8.2 250 | [v0.8.1]: https://github.com/xmidt-org/tr1d1um/compare/v0.8.0...v0.8.1 251 | [v0.8.0]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.12...v0.8.0 252 | [v0.7.12]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.11...v0.7.12 253 | [v0.7.11]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.10...v0.7.11 254 | [v0.7.10]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.9...v0.7.10 255 | [v0.7.9]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.8...v0.7.9 256 | [v0.7.8]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.7...v0.7.8 257 | [v0.7.7]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.6...v0.7.7 258 | [v0.7.6]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.4...v0.7.6 259 | [v0.7.4]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.3...v0.7.4 260 | [v0.7.3]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.2...v0.7.3 261 | [v0.7.2]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.1...v0.7.2 262 | [v0.7.1]: https://github.com/xmidt-org/tr1d1um/compare/v0.7.0...v0.7.1 263 | [v0.7.0]: https://github.com/xmidt-org/tr1d1um/compare/v0.6.4...v0.7.0 264 | [v0.6.4]: https://github.com/xmidt-org/tr1d1um/compare/v0.6.3...v0.6.4 265 | [v0.6.3]: https://github.com/xmidt-org/tr1d1um/compare/v0.6.2...v0.6.3 266 | [v0.6.2]: https://github.com/xmidt-org/tr1d1um/compare/v0.6.1...v0.6.2 267 | [v0.6.1]: https://github.com/xmidt-org/tr1d1um/compare/v0.6.0...v0.6.1 268 | [v0.6.0]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.10...v0.6.0 269 | [v0.5.10]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.9...v0.5.10 270 | [v0.5.9]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.8...v0.5.9 271 | [v0.5.8]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.7...v0.5.8 272 | [v0.5.7]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.6...v0.5.7 273 | [v0.5.6]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.5...v0.5.6 274 | [v0.5.5]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.4...v0.5.5 275 | [v0.5.4]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.3...v0.5.4 276 | [v0.5.3]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.2...v0.5.3 277 | [v0.5.2]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.1...v0.5.2 278 | [v0.5.1]: https://github.com/xmidt-org/tr1d1um/compare/v0.5.0...v0.5.1 279 | [v0.5.0]: https://github.com/xmidt-org/tr1d1um/compare/v0.4.0...v0.5.0 280 | [v0.4.0]: https://github.com/xmidt-org/tr1d1um/compare/v0.3.0...v0.4.0 281 | [v0.3.0]: https://github.com/xmidt-org/tr1d1um/compare/v0.2.1...v0.3.0 282 | [v0.2.1]: https://github.com/xmidt-org/tr1d1um/compare/v0.2.0...v0.2.1 283 | [v0.2.0]: https://github.com/xmidt-org/tr1d1um/compare/v0.1.5...v0.2.0 284 | [v0.1.5]: https://github.com/xmidt-org/tr1d1um/compare/v0.1.4...v0.1.5 285 | [v0.1.4]: https://github.com/xmidt-org/tr1d1um/compare/v0.1.2...v0.1.4 286 | [v0.1.2]: https://github.com/xmidt-org/tr1d1um/compare/v0.1.1...v0.1.2 287 | [0.1.1]: https://github.com/xmidt-org/tr1d1um/compare/e34399980ec8f7716633c8b8bc5d72727c79b184...0.1.1 288 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contribution Guidelines 2 | ======================= 3 | 4 | We love to see contributions to the project and have tried to make it easy to do so. If you would like to contribute code to this project you can do so through GitHub by forking the repository and sending a pull request. 5 | 6 | Before Comcast merges your code into the project you must sign the [Comcast Contributor License Agreement (CLA)](https://gist.github.com/ComcastOSS/a7b8933dd8e368535378cda25c92d19a). 7 | 8 | If you haven't previously signed a Comcast CLA, you'll automatically be asked to when you open a pull request. Alternatively, we can e-mail you a PDF that you can sign and scan back to us. Please send us an e-mail or create a new GitHub issue to request a PDF version of the CLA. 9 | 10 | For more details about contributing to GitHub projects see 11 | http://gun.io/blog/how-to-github-fork-branch-and-pull-request/ 12 | 13 | Documentation 14 | ------------- 15 | 16 | If you contribute anything that changes the behavior of the 17 | application, document it in the [README](https://github.com/Comcast/tr1d1um/blob/main/README.md) or [wiki](https://github.com/Comcast/tr1d1um/wiki)! This includes new features, additional variants of behavior and breaking changes. 18 | 19 | Testing 20 | ------- 21 | 22 | Tests are written using golang's standard testing tools, and are run prior to the PR being accepted. 23 | 24 | Pull Requests 25 | ------------- 26 | 27 | * should be narrowly focused with no more than 3 or 4 logical commits 28 | * when possible, address no more than one issue 29 | * should be reviewable in the GitHub code review tool 30 | * should be linked to any issues it relates to (i.e. issue number after (#) in commit messages or pull request message) 31 | * should conform to idiomatic golang code formatting 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | ## SPDX-License-Identifier: Apache-2.0 3 | 4 | ########################## 5 | # Build an image to download spruce and the ca-certificates. 6 | ########################## 7 | 8 | FROM docker.io/library/golang:1.23.2-alpine AS builder 9 | WORKDIR /src 10 | 11 | RUN apk update && \ 12 | apk add --no-cache --no-progress \ 13 | ca-certificates \ 14 | curl 15 | 16 | # Download spruce here to eliminate the need for curl in the final image 17 | RUN mkdir -p /go/bin 18 | RUN curl -Lo /go/bin/spruce https://github.com/geofffranks/spruce/releases/download/v1.31.1/spruce-linux-$(go env GOARCH) 19 | RUN chmod +x /go/bin/spruce 20 | # Error out if spruce download fails by checking the version 21 | RUN /go/bin/spruce --version 22 | 23 | COPY . . 24 | 25 | ########################## 26 | # Build the final image. 27 | ########################## 28 | 29 | FROM alpine:latest 30 | 31 | # Copy over the standard things you'd expect. 32 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 33 | COPY tr1d1um / 34 | COPY .release/docker/entrypoint.sh / 35 | 36 | # Copy over spruce and the spruce template file used to make the actual configuration file. 37 | COPY .release/docker/tr1d1um_spruce.yaml /tmp/tr1d1um_spruce.yaml 38 | COPY --from=builder /go/bin/spruce /bin/ 39 | 40 | # Include compliance details about the container and what it contains. 41 | COPY Dockerfile / 42 | COPY NOTICE / 43 | COPY LICENSE / 44 | 45 | # Make the location for the configuration file that will be used. 46 | RUN mkdir /etc/tr1d1um/ \ 47 | && touch /etc/tr1d1um/tr1d1um.yaml \ 48 | && chmod 666 /etc/tr1d1um/tr1d1um.yaml 49 | 50 | USER nobody 51 | 52 | ENTRYPOINT ["/entrypoint.sh"] 53 | 54 | EXPOSE 6100 55 | EXPOSE 6101 56 | EXPOSE 6102 57 | EXPOSE 6103 58 | 59 | CMD ["/tr1d1um"] 60 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | ## SPDX-License-Identifier: Apache-2.0 3 | FROM docker.io/library/golang:1.23.2-alpine as builder 4 | 5 | WORKDIR /src 6 | 7 | RUN apk add --no-cache --no-progress \ 8 | ca-certificates \ 9 | curl 10 | 11 | # Download spruce here to eliminate the need for curl in the final image 12 | RUN mkdir -p /go/bin && \ 13 | curl -L -o /go/bin/spruce https://github.com/geofffranks/spruce/releases/download/v1.29.0/spruce-linux-amd64 && \ 14 | chmod +x /go/bin/spruce 15 | 16 | COPY . . 17 | RUN go build -o /go/bin/tr1d1um . 18 | 19 | ########################## 20 | # Build the final image. 21 | ########################## 22 | 23 | FROM alpine:latest 24 | 25 | # Copy over the standard things you'd expect. 26 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 27 | COPY --from=builder /go/bin/tr1d1um / 28 | COPY .release/docker/entrypoint.sh / 29 | 30 | # Copy over spruce and the spruce template file used to make the actual configuration file. 31 | COPY .release/docker/tr1d1um_spruce.yaml /tmp/tr1d1um_spruce.yaml 32 | COPY --from=builder /go/bin/spruce /bin/ 33 | 34 | # Include compliance details about the container and what it contains. 35 | COPY Dockerfile / 36 | COPY NOTICE / 37 | COPY LICENSE / 38 | 39 | # Make the location for the configuration file that will be used. 40 | RUN mkdir /etc/tr1d1um/ \ 41 | && touch /etc/tr1d1um/tr1d1um.yaml \ 42 | && chmod 666 /etc/tr1d1um/tr1d1um.yaml 43 | 44 | USER nobody 45 | 46 | ENTRYPOINT ["/entrypoint.sh"] 47 | 48 | EXPOSE 6100 49 | EXPOSE 6101 50 | EXPOSE 6102 51 | EXPOSE 6103 52 | 53 | CMD ["/tr1d1um"] 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSES/Apache-2.0.txt: -------------------------------------------------------------------------------- 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, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 20 | 21 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 22 | 23 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 24 | 25 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 26 | 27 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 28 | 29 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 30 | 31 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 32 | 33 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 34 | 35 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 36 | 37 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 38 | 39 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | 41 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 42 | 43 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 44 | 45 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 46 | 47 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 48 | 49 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 50 | 51 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 52 | 53 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 54 | 55 | END OF TERMS AND CONDITIONS 56 | 57 | APPENDIX: How to apply the Apache License to your work. 58 | 59 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 60 | 61 | Copyright [yyyy] [name of copyright owner] 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); 64 | you may not use this file except in compliance with the License. 65 | You may obtain a copy of the License at 66 | 67 | http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Unless required by applicable law or agreed to in writing, software 70 | distributed under the License is distributed on an "AS IS" BASIS, 71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 72 | See the License for the specific language governing permissions and 73 | limitations under the License. 74 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Maintainers of this repository: 2 | 3 | * Weston Schmidt @schmidtw 4 | * Joel Unzain @joe94 5 | * John Bass @johnabass 6 | * Nick Harter @njharter 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default build test style docker binaries clean 2 | 3 | 4 | DOCKER ?= docker 5 | APP := tr1d1um 6 | DOCKER_ORG := ghcr.io/xmidt-org 7 | 8 | VERSION ?= $(shell git describe --tag --always --dirty) 9 | PROGVER ?= $(shell git describe --tags `git rev-list --tags --max-count=1` | tail -1 | sed 's/v\(.*\)/\1/') 10 | BUILDTIME = $(shell date -u '+%c') 11 | GITCOMMIT = $(shell git rev-parse --short HEAD) 12 | GOBUILDFLAGS = -a -ldflags "-w -s -X 'main.Date=$(BUILDTIME)' -X main.Commit=$(GITCOMMIT) -X main.Version=$(VERSION)" -o $(APP) 13 | 14 | default: build 15 | 16 | generate: 17 | go generate ./... 18 | go install ./... 19 | 20 | test: 21 | go test -v -race -coverprofile=coverage.txt ./... 22 | go test -v -race -json ./... > report.json 23 | 24 | style: 25 | ! gofmt -d $$(find . -path ./vendor -prune -o -name '*.go' -print) | grep '^' 26 | 27 | check: 28 | golangci-lint run -n | tee errors.txt 29 | 30 | build: 31 | CGO_ENABLED=0 go build $(GOBUILDFLAGS) 32 | 33 | release: build 34 | upx $(APP) 35 | 36 | docker: 37 | -$(DOCKER) rmi "$(DOCKER_ORG)/$(APP):$(VERSION)" 38 | -$(DOCKER) rmi "$(DOCKER_ORG)/$(APP):latest" 39 | $(DOCKER) build -t "$(DOCKER_ORG)/$(APP):$(VERSION)" -t "$(DOCKER_ORG)/$(APP):latest" . 40 | 41 | binaries: generate 42 | mkdir -p ./.ignore 43 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o ./.ignore/$(APP)-$(PROGVER).darwin-amd64 -ldflags "-X 'main.Date=$(BUILDTIME)' -X main.Commit=$(GITCOMMIT) -X main.Version=$(VERSION)" 44 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./.ignore/$(APP)-$(PROGVER).linux-amd64 -ldflags "-X 'main.Date=$(BUILDTIME)' -X main.Commit=$(GITCOMMIT) -X main.Version=$(VERSION)" 45 | 46 | upx ./.ignore/$(APP)-$(PROGVER).darwin-amd64 47 | upx ./.ignore/$(APP)-$(PROGVER).linux-amd64 48 | 49 | clean: 50 | -rm -r .ignore/ $(APP) errors.txt report.json coverage.txt 51 | 52 | 53 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | tr1d1um 2 | Copyright 2017 Comcast Cable Communications Management, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | This product includes software developed at Comcast (http://www.comcast.com/). 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tr1d1um 2 | 3 | [![Build Status](https://github.com/xmidt-org/tr1d1um/actions/workflows/ci.yml/badge.svg)](https://github.com/xmidt-org/tr1d1um/actions/workflows/ci.yml) 4 | [![codecov.io](http://codecov.io/github/xmidt-org/tr1d1um/coverage.svg?branch=main)](http://codecov.io/github/xmidt-org/tr1d1um?branch=main) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/xmidt-org/tr1d1um)](https://goreportcard.com/report/github.com/xmidt-org/tr1d1um) 6 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=xmidt-org_tr1d1um&metric=alert_status)](https://sonarcloud.io/dashboard?id=xmidt-org_tr1d1um) 7 | [![Apache V2 License](http://img.shields.io/badge/license-Apache%20V2-blue.svg)](https://github.com/xmidt-org/tr1d1um/blob/main/LICENSE) 8 | [![GitHub Release](https://img.shields.io/github/release/xmidt-org/tr1d1um.svg)](CHANGELOG.md) 9 | 10 | 11 | ## Summary 12 | An implementation of the WebPA API which enables communication with TR-181 data model devices connected to the [XMiDT](https://github.com/xmidt-org/xmidt) cloud as well as subscription capabilities to device events. 13 | 14 | ## Table of Contents 15 | 16 | - [Code of Conduct](#code-of-conduct) 17 | - [Details](#details) 18 | - [Build](#build) 19 | - [Deploy](#deploy) 20 | - [Contributing](#contributing) 21 | 22 | ## Code of Conduct 23 | 24 | This project and everyone participating in it are governed by the [XMiDT Code Of Conduct](https://xmidt.io/code_of_conduct/). 25 | By participating, you agree to this Code. 26 | 27 | 28 | ## Details 29 | The WebPA API operations can be divided into the following categories: 30 | 31 | ### Device Statistics - `/stat` endpoint 32 | 33 | Fetch the statistics (i.e. uptime) for a given device connected to the XMiDT cluster. This endpoint is a simple shadow of its counterpart on the `XMiDT` API. That is, `Tr1d1um` simply passes through the incoming request to `XMiDT` as it comes and returns whatever response `XMiDT` provided. 34 | 35 | ### CRUD operations - `/config` endpoints 36 | 37 | Tr1d1um validates the incoming request, injects it into the payload of a SimpleRequestResponse [WRP](https://github.com/xmidt-org/wrp-c/wiki/Web-Routing-Protocol) message and sends it to XMiDT. It is worth mentioning that Tr1d1um encodes the outgoing `WRP` message in `msgpack` as it is the encoding XMiDT ultimately uses to communicate with devices. 38 | 39 | ### Event listener registration - `/hook(s)` endpoints 40 | Devices connected to the XMiDT Cluster generate events (i.e. going offline). The webhooks library used by Tr1d1um leverages AWS SNS to publish these events. These endpoints then allow API users to both setup listeners of desired events and fetch the current list of configured listeners in the system. 41 | 42 | 43 | ## Build 44 | 45 | ### Source 46 | 47 | In order to build from source, you need a working 1.x Go environment. 48 | Find more information on the [Go website](https://golang.org/doc/install). 49 | 50 | Then, clone the repository and build using make: 51 | 52 | ```bash 53 | git clone git@github.com:xmidt-org/tr1d1um.git 54 | cd tr1d1um 55 | make build 56 | ``` 57 | 58 | ### Makefile 59 | 60 | The Makefile has the following options you may find helpful: 61 | * `make build`: builds the Tr1d1um binary in the tr1d1um/src/tr1d1um folder 62 | * `make docker`: fetches all dependencies from source and builds a Tr1d1um 63 | docker image 64 | * `make test`: runs unit tests with coverage for Tr1d1um 65 | * `make clean`: deletes previously-built binaries and object files 66 | 67 | ### RPM 68 | 69 | First have a local clone of the source and go into the root directory of the 70 | repository. Then use rpkg to build the rpm: 71 | ```bash 72 | rpkg srpm --spec / 73 | rpkg -C /.config/rpkg.conf sources --outdir ' 74 | ``` 75 | 76 | ### Docker 77 | 78 | The docker image can be built either with the Makefile or by running a docker 79 | command. Either option requires first getting the source code. 80 | 81 | See [Makefile](#Makefile) on specifics of how to build the image that way. 82 | 83 | If you'd like to build it without make, follow these instructions based on your use case: 84 | 85 | - Local testing 86 | ```bash 87 | docker build -t tr1d1um:local -f Dockerfile.local . 88 | ``` 89 | ``` 90 | # OR build for am arm64 architecture 91 | bash 92 | docker build -t tr1d1um:local --build-arg arm64=true -f Dockerfile.local . 93 | ``` 94 | This allows you to test local changes to a dependency. For example, you can build 95 | a tr1d1um image with the changes to an upcoming changes to [webpa-common](https://github.com/xmidt-org/webpa-common) by using the [replace](https://golang.org/ref/mod#go) directive in your go.mod file like so: 96 | ``` 97 | replace github.com/xmidt-org/webpa-common v1.10.2 => ../webpa-common 98 | ``` 99 | **Note:** if you omit `go mod vendor`, your build will fail as the path `../webpa-common` does not exist on the builder container. 100 | 101 | - Building a specific version 102 | ```bash 103 | git checkout v0.5.1 104 | docker build -t tr1d1um:v0.5.1 -f Dockerfile.local . 105 | ``` 106 | 107 | **Additional Info:** If you'd like to stand up a XMiDT docker-compose cluster, read [this](https://github.com/xmidt-org/xmidt/blob/master/deploy/docker-compose/README.md). 108 | 109 | ## Deploy 110 | 111 | If you'd like to stand up `Tr1d1um` and the XMiDT cluster on Docker for local testing, refer to the [deploy README](https://github.com/xmidt-org/xmidt/tree/main/deploy/README.md). 112 | 113 | You can also run the standalone `tr1d1um` binary with the default `tr1d1um.yaml` config file: 114 | ```bash 115 | ./tr1d1um 116 | ``` 117 | 118 | ### Kubernetes 119 | 120 | A helm chart can be used to deploy tr1d1um to kubernetes 121 | ``` 122 | helm install xmidt-tr1d1um deploy/helm/tr1d1um 123 | ``` 124 | 125 | ## Contributing 126 | 127 | Refer to [CONTRIBUTING.md](CONTRIBUTING.md). 128 | 129 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "errors" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/xmidt-org/arrange" 12 | "github.com/xmidt-org/bascule" 13 | "github.com/xmidt-org/bascule/basculechecks" 14 | "github.com/xmidt-org/bascule/basculehttp" 15 | "github.com/xmidt-org/clortho" 16 | "go.uber.org/fx" 17 | ) 18 | 19 | var possiblePrefixURLs = []string{ 20 | "/" + apiBase, 21 | "/" + prevAPIBase, 22 | } 23 | 24 | // JWTValidator provides a convenient way to define jwt validator through config files 25 | type JWTValidator struct { 26 | // Config is used to create the clortho Resolver & Refresher for JWT verification keys 27 | Config clortho.Config 28 | 29 | // Leeway is used to set the amount of time buffer should be given to JWT 30 | // time values, such as nbf 31 | Leeway bascule.Leeway 32 | } 33 | 34 | func provideAuthChain(configKey string) fx.Option { 35 | return fx.Options( 36 | basculehttp.ProvideMetrics(), 37 | basculechecks.ProvideMetrics(), 38 | fx.Provide( 39 | func() basculehttp.ParseURL { 40 | return createRemovePrefixURLFuncLegacy(possiblePrefixURLs) 41 | }, 42 | arrange.UnmarshalKey("jwtValidator", JWTValidator{}), 43 | func(c JWTValidator) clortho.Config { 44 | return c.Config 45 | }, 46 | ), 47 | basculehttp.ProvideBasicAuth(configKey), 48 | basculehttp.ProvideBearerTokenFactory("jwtValidator", false), 49 | basculechecks.ProvideRegexCapabilitiesValidator("capabilityCheck"), 50 | basculehttp.ProvideBearerValidator(), 51 | basculehttp.ProvideServerChain(), 52 | ) 53 | } 54 | 55 | func createRemovePrefixURLFuncLegacy(prefixes []string) basculehttp.ParseURL { 56 | return func(u *url.URL) (*url.URL, error) { 57 | escapedPath := u.EscapedPath() 58 | var prefix string 59 | for _, p := range prefixes { 60 | if strings.HasPrefix(escapedPath, p) { 61 | prefix = p 62 | break 63 | } 64 | } 65 | if prefix == "" { 66 | return nil, errors.New("unexpected URL, did not start with expected prefix") 67 | } 68 | u.Path = escapedPath[len(prefix):] 69 | u.RawPath = escapedPath[len(prefix):] 70 | return u, nil 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /basculeLogging.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | func sanitizeHeaders(headers http.Header) (filtered http.Header) { 12 | filtered = headers.Clone() 13 | if authHeader := filtered.Get("Authorization"); authHeader != "" { 14 | filtered.Del("Authorization") 15 | parts := strings.Split(authHeader, " ") 16 | if len(parts) == 2 { 17 | filtered.Set("Authorization-Type", parts[0]) 18 | } 19 | } 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /basculeLogging_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestSanitizeHeaders(t *testing.T) { 14 | testCases := []struct { 15 | Description string 16 | Input http.Header 17 | Expected http.Header 18 | }{ 19 | { 20 | Description: "Filtered", 21 | Input: http.Header{"Authorization": []string{"Basic xyz"}, "HeaderA": []string{"x"}}, 22 | Expected: http.Header{"HeaderA": []string{"x"}, "Authorization-Type": []string{"Basic"}}, 23 | }, 24 | { 25 | Description: "Handled human error", 26 | Input: http.Header{"Authorization": []string{"BasicXYZ"}, "HeaderB": []string{"y"}}, 27 | Expected: http.Header{"HeaderB": []string{"y"}}, 28 | }, 29 | { 30 | Description: "Not a perfect system", 31 | Input: http.Header{"Authorization": []string{"MySecret IWantToLeakIt"}}, 32 | Expected: http.Header{"Authorization-Type": []string{"MySecret"}}, 33 | }, 34 | } 35 | 36 | for _, tc := range testCases { 37 | t.Run(tc.Description, func(t *testing.T) { 38 | assert := assert.New(t) 39 | actual := sanitizeHeaders(tc.Input) 40 | assert.Equal(tc.Expected, actual) 41 | }) 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xmidt-org/tr1d1um 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/go-kit/kit v0.13.0 7 | github.com/goph/emperror v0.17.3-0.20190703203600-60a8d9faa17b 8 | github.com/gorilla/mux v1.8.1 9 | github.com/justinas/alice v1.2.0 10 | github.com/prometheus/client_golang v1.22.0 11 | github.com/spf13/cast v1.8.0 12 | github.com/spf13/pflag v1.0.6 13 | github.com/spf13/viper v1.19.0 14 | github.com/stretchr/testify v1.10.0 15 | github.com/xmidt-org/ancla v0.3.12 16 | github.com/xmidt-org/arrange v0.4.0 17 | github.com/xmidt-org/bascule v0.11.6 18 | github.com/xmidt-org/candlelight v0.1.23 19 | github.com/xmidt-org/clortho v0.0.4 20 | github.com/xmidt-org/httpaux v0.4.2 21 | github.com/xmidt-org/sallust v0.2.4 22 | github.com/xmidt-org/touchstone v0.1.7 23 | github.com/xmidt-org/webpa-common/v2 v2.6.0 24 | github.com/xmidt-org/wrp-go/v3 v3.7.0 25 | go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.61.0 26 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 27 | go.uber.org/fx v1.24.0 28 | go.uber.org/zap v1.27.0 29 | ) 30 | 31 | require ( 32 | emperror.dev/emperror v0.33.0 // indirect 33 | emperror.dev/errors v0.8.1 // indirect 34 | github.com/VividCortex/gohistogram v1.0.0 // indirect 35 | github.com/beorn7/perks v1.0.1 // indirect 36 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 37 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 38 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 39 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect 40 | github.com/felixge/httpsnoop v1.0.4 // indirect 41 | github.com/fsnotify/fsnotify v1.7.0 // indirect 42 | github.com/go-kit/log v0.2.1 // indirect 43 | github.com/go-logfmt/logfmt v0.6.0 // indirect 44 | github.com/go-logr/logr v1.4.2 // indirect 45 | github.com/go-logr/stdr v1.2.2 // indirect 46 | github.com/goccy/go-json v0.10.3 // indirect 47 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 48 | github.com/google/uuid v1.6.0 // indirect 49 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect 50 | github.com/hashicorp/hcl v1.0.0 // indirect 51 | github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c // indirect 52 | github.com/jtacoma/uritemplates v1.0.0 // indirect 53 | github.com/lestrrat-go/blackmagic v1.0.2 // indirect 54 | github.com/lestrrat-go/httpcc v1.0.1 // indirect 55 | github.com/lestrrat-go/httprc v1.0.5 // indirect 56 | github.com/lestrrat-go/iter v1.0.2 // indirect 57 | github.com/lestrrat-go/jwx/v2 v2.0.21 // indirect 58 | github.com/lestrrat-go/option v1.0.1 // indirect 59 | github.com/magiconair/properties v1.8.7 // indirect 60 | github.com/mitchellh/mapstructure v1.5.0 // indirect 61 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 62 | github.com/openzipkin/zipkin-go v0.4.3 // indirect 63 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 64 | github.com/pkg/errors v0.9.1 // indirect 65 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 66 | github.com/prometheus/client_model v0.6.1 // indirect 67 | github.com/prometheus/common v0.62.0 // indirect 68 | github.com/prometheus/procfs v0.15.1 // indirect 69 | github.com/sagikazarmark/locafero v0.4.0 // indirect 70 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 71 | github.com/segmentio/asm v1.2.0 // indirect 72 | github.com/sourcegraph/conc v0.3.0 // indirect 73 | github.com/spf13/afero v1.11.0 // indirect 74 | github.com/stretchr/objx v0.5.2 // indirect 75 | github.com/subosito/gotenv v1.6.0 // indirect 76 | github.com/ugorji/go/codec v1.2.12 // indirect 77 | github.com/xmidt-org/argus v0.10.18 // indirect 78 | github.com/xmidt-org/chronon v0.1.1 // indirect 79 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 80 | go.opentelemetry.io/otel v1.36.0 // indirect 81 | go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect 82 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect 83 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect 84 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 // indirect 85 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect 86 | go.opentelemetry.io/otel/exporters/zipkin v1.29.0 // indirect 87 | go.opentelemetry.io/otel/metric v1.36.0 // indirect 88 | go.opentelemetry.io/otel/sdk v1.36.0 // indirect 89 | go.opentelemetry.io/otel/trace v1.36.0 // indirect 90 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 91 | go.uber.org/dig v1.19.0 // indirect 92 | go.uber.org/multierr v1.11.0 // indirect 93 | golang.org/x/crypto v0.36.0 // indirect 94 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 95 | golang.org/x/net v0.38.0 // indirect 96 | golang.org/x/sys v0.33.0 // indirect 97 | golang.org/x/text v0.23.0 // indirect 98 | google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect 99 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect 100 | google.golang.org/grpc v1.65.0 // indirect 101 | google.golang.org/protobuf v1.36.5 // indirect 102 | gopkg.in/ini.v1 v1.67.0 // indirect 103 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 104 | gopkg.in/yaml.v3 v3.0.1 // indirect 105 | ) 106 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io" 10 | _ "net/http/pprof" 11 | "os" 12 | "runtime" 13 | "time" 14 | 15 | "github.com/xmidt-org/ancla" 16 | "github.com/xmidt-org/arrange" 17 | "github.com/xmidt-org/arrange/arrangepprof" 18 | "github.com/xmidt-org/touchstone" 19 | "github.com/xmidt-org/touchstone/touchhttp" 20 | "go.uber.org/fx" 21 | "go.uber.org/zap" 22 | 23 | "github.com/goph/emperror" 24 | "github.com/spf13/pflag" 25 | "github.com/xmidt-org/candlelight" 26 | ) 27 | 28 | // convenient global values 29 | const ( 30 | DefaultKeyID = "current" 31 | apiVersion = "v3" 32 | prevAPIVersion = "v2" 33 | applicationName = "tr1d1um" 34 | apiBase = "api/" + apiVersion 35 | prevAPIBase = "api/" + prevAPIVersion 36 | apiBaseDualVersion = "api/{version:" + apiVersion + "|" + prevAPIVersion + "}" 37 | ) 38 | 39 | const ( 40 | translationServicesKey = "supportedServices" 41 | targetURLKey = "targetURL" 42 | netDialerTimeoutKey = "netDialerTimeout" 43 | clientTimeoutKey = "clientTimeout" 44 | reqTimeoutKey = "respWaitTimeout" 45 | reqRetryIntervalKey = "requestRetryInterval" 46 | reqMaxRetriesKey = "requestMaxRetries" 47 | wrpSourceKey = "WRPSource" 48 | hooksSchemeKey = "hooksScheme" 49 | reducedTransactionLoggingCodesKey = "logging.reducedLoggingResponseCodes" 50 | authAcquirerKey = "authAcquirer" 51 | webhookConfigKey = "webhook" 52 | tracingConfigKey = "tracing" 53 | ) 54 | 55 | var ( 56 | // dynamic versioning 57 | Version string 58 | Date string 59 | Commit string 60 | ) 61 | 62 | var defaults = map[string]interface{}{ 63 | translationServicesKey: []string{}, // no services allowed by the default 64 | targetURLKey: "localhost:6000", 65 | netDialerTimeoutKey: "5s", 66 | clientTimeoutKey: "50s", 67 | reqTimeoutKey: "40s", 68 | reqRetryIntervalKey: "2s", 69 | reqMaxRetriesKey: 2, 70 | wrpSourceKey: "dns:localhost", 71 | hooksSchemeKey: "https", 72 | } 73 | 74 | type XmidtClientTimeoutConfigIn struct { 75 | fx.In 76 | XmidtClientTimeout httpClientTimeout `name:"xmidtClientTimeout"` 77 | } 78 | 79 | type ArgusClientTimeoutConfigIn struct { 80 | fx.In 81 | ArgusClientTimeout httpClientTimeout `name:"argusClientTimeout"` 82 | } 83 | 84 | type TracingConfigIn struct { 85 | fx.In 86 | TracingConfig candlelight.Config 87 | Logger *zap.Logger 88 | } 89 | 90 | type ConstOut struct { 91 | fx.Out 92 | DefaultKeyID string `name:"default_key_id"` 93 | } 94 | 95 | func consts() ConstOut { 96 | return ConstOut{ 97 | DefaultKeyID: DefaultKeyID, 98 | } 99 | } 100 | 101 | func configureXmidtClientTimeout(in XmidtClientTimeoutConfigIn) httpClientTimeout { 102 | xct := in.XmidtClientTimeout 103 | 104 | if xct.ClientTimeout == 0 { 105 | xct.ClientTimeout = time.Second * 135 106 | } 107 | if xct.NetDialerTimeout == 0 { 108 | xct.NetDialerTimeout = time.Second * 5 109 | } 110 | if xct.RequestTimeout == 0 { 111 | xct.RequestTimeout = time.Second * 129 112 | } 113 | return xct 114 | } 115 | 116 | func configureArgusClientTimeout(in ArgusClientTimeoutConfigIn) httpClientTimeout { 117 | act := in.ArgusClientTimeout 118 | 119 | if act.ClientTimeout == 0 { 120 | act.ClientTimeout = time.Second * 50 121 | } 122 | if act.NetDialerTimeout == 0 { 123 | act.NetDialerTimeout = time.Second * 5 124 | } 125 | return act 126 | } 127 | 128 | func loadTracing(in TracingConfigIn) (candlelight.Tracing, error) { 129 | traceConfig := in.TracingConfig 130 | traceConfig.ApplicationName = applicationName 131 | tracing, err := candlelight.New(traceConfig) 132 | if err != nil { 133 | return candlelight.Tracing{}, err 134 | } 135 | in.Logger.Info("tracing status", zap.Bool("enabled", !tracing.IsNoop())) 136 | return tracing, nil 137 | } 138 | 139 | func printVersion(f *pflag.FlagSet, arguments []string) (bool, error) { 140 | if err := f.Parse(arguments); err != nil { 141 | return true, err 142 | } 143 | 144 | if pVersion, _ := f.GetBool("version"); pVersion { 145 | printVersionInfo(os.Stdout) 146 | return true, nil 147 | } 148 | return false, nil 149 | } 150 | 151 | func printVersionInfo(writer io.Writer) { 152 | fmt.Fprintf(writer, "%s:\n", applicationName) 153 | fmt.Fprintf(writer, " version: \t%s\n", Version) 154 | fmt.Fprintf(writer, " go version: \t%s\n", runtime.Version()) 155 | fmt.Fprintf(writer, " built time: \t%s\n", Date) 156 | fmt.Fprintf(writer, " git commit: \t%s\n", Commit) 157 | fmt.Fprintf(writer, " os/arch: \t%s/%s\n", runtime.GOOS, runtime.GOARCH) 158 | } 159 | 160 | func exitIfError(logger *zap.Logger, err error) { 161 | if err != nil { 162 | if logger != nil { 163 | logger.Error("failed to parse arguments", zap.Error(err)) 164 | } 165 | fmt.Fprintf(os.Stderr, "Error: %#v\n", err.Error()) 166 | os.Exit(1) 167 | } 168 | } 169 | 170 | //nolint:funlen 171 | func tr1d1um(arguments []string) (exitCode int) { 172 | v, l, f, err := setup(arguments) 173 | if err != nil { 174 | fmt.Fprintln(os.Stderr, err) 175 | return 1 176 | } 177 | 178 | // This allows us to communicate the version of the binary upon request. 179 | if done, parseErr := printVersion(f, arguments); done { 180 | // if we're done, we're exiting no matter what 181 | exitIfError(l, emperror.Wrap(parseErr, "failed to parse arguments")) 182 | os.Exit(0) 183 | } 184 | 185 | app := fx.New( 186 | arrange.LoggerFunc(l.Sugar().Infof), 187 | fx.Supply(l), 188 | fx.Supply(v), 189 | arrange.ForViper(v), 190 | arrange.ProvideKey("xmidtClientTimeout", httpClientTimeout{}), 191 | arrange.ProvideKey("argusClientTimeout", httpClientTimeout{}), 192 | touchstone.Provide(), 193 | touchhttp.Provide(), 194 | provideMetrics(), 195 | ancla.ProvideMetrics(), 196 | arrangepprof.HTTP{ 197 | RouterName: "server_pprof", 198 | }.Provide(), 199 | fx.Provide( 200 | consts, 201 | arrange.UnmarshalKey(tracingConfigKey, candlelight.Config{}), 202 | fx.Annotated{ 203 | Name: "xmidt_client_timeout", 204 | Target: configureXmidtClientTimeout, 205 | }, 206 | fx.Annotated{ 207 | Name: "argus_client_timeout", 208 | Target: configureArgusClientTimeout, 209 | }, 210 | loadTracing, 211 | newHTTPClient, 212 | ), 213 | provideAuthChain("authx.inbound"), 214 | provideServers(), 215 | provideHandlers(), 216 | ) 217 | 218 | switch err := app.Err(); { 219 | case errors.Is(err, pflag.ErrHelp): 220 | return 221 | case err == nil: 222 | app.Run() 223 | default: 224 | fmt.Fprintln(os.Stderr, err) 225 | return 2 226 | } 227 | 228 | return 0 229 | } 230 | 231 | func main() { 232 | os.Exit(tr1d1um(os.Args)) 233 | } 234 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2025 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/xmidt-org/touchstone" 9 | "go.uber.org/fx" 10 | ) 11 | 12 | const ( 13 | // metric names 14 | serviceConfigsRetriesCounter = "service_configs_retries" 15 | 16 | // metric labels 17 | apiLabel = "api" 18 | 19 | // metric label values 20 | // api 21 | stat_api = "stat" 22 | device_api = "device" 23 | ) 24 | 25 | func provideMetrics() fx.Option { 26 | return touchstone.CounterVec( 27 | prometheus.CounterOpts{ 28 | Name: serviceConfigsRetriesCounter, 29 | Help: "Count of retries for xmidt service configs api calls.", 30 | }, 31 | []string{apiLabel}..., 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /primaryHandler.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/http" 12 | "time" 13 | 14 | gokitprometheus "github.com/go-kit/kit/metrics/prometheus" 15 | "github.com/prometheus/client_golang/prometheus" 16 | "github.com/spf13/viper" 17 | "github.com/xmidt-org/ancla" 18 | "github.com/xmidt-org/arrange" 19 | "github.com/xmidt-org/bascule/acquire" 20 | "github.com/xmidt-org/candlelight" 21 | "github.com/xmidt-org/sallust" 22 | "github.com/xmidt-org/touchstone" 23 | "github.com/xmidt-org/touchstone/touchhttp" 24 | "github.com/xmidt-org/tr1d1um/stat" 25 | "github.com/xmidt-org/tr1d1um/transaction" 26 | "github.com/xmidt-org/tr1d1um/translation" 27 | "github.com/xmidt-org/webpa-common/v2/xhttp" 28 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 29 | "go.uber.org/fx" 30 | "go.uber.org/zap" 31 | ) 32 | 33 | // httpClientTimeout contains timeouts for an HTTP client and its requests. 34 | type httpClientTimeout struct { 35 | // ClientTimeout is HTTP Client Timeout. 36 | ClientTimeout time.Duration 37 | 38 | // RequestTimeout can be imposed as an additional timeout on the request 39 | // using context cancellation. 40 | RequestTimeout time.Duration 41 | 42 | // NetDialerTimeout is the net dialer timeout 43 | NetDialerTimeout time.Duration 44 | } 45 | 46 | type authAcquirerConfig struct { 47 | JWT acquire.RemoteBearerTokenAcquirerOptions 48 | Basic string 49 | } 50 | 51 | type provideWebhookHandlersIn struct { 52 | fx.In 53 | Lifecycle fx.Lifecycle 54 | V *viper.Viper 55 | WebhookConfig ancla.Config 56 | ArgusClientTimeout httpClientTimeout `name:"argus_client_timeout"` 57 | Logger *zap.Logger 58 | Measures *ancla.Measures 59 | Tracing candlelight.Tracing 60 | Tf *touchstone.Factory 61 | } 62 | 63 | type provideWebhookHandlersOut struct { 64 | fx.Out 65 | AddWebhookHandler http.Handler `name:"add_webhook_handler"` 66 | V2AddWebhookHandler http.Handler `name:"v2_add_webhook_handler"` 67 | GetAllWebhooksHandler http.Handler `name:"get_all_webhooks_handler"` 68 | } 69 | 70 | type ServiceOptionsIn struct { 71 | fx.In 72 | Logger *zap.Logger 73 | XmidtClientTimeout httpClientTimeout `name:"xmidt_client_timeout"` 74 | RequestMaxRetries int `name:"requestMaxRetries"` 75 | RequestRetryInterval time.Duration `name:"requestRetryInterval"` 76 | TargetURL string `name:"targetURL"` 77 | WRPSource string `name:"WRPSource"` 78 | ServiceConfigsRetries *prometheus.CounterVec `name:"service_configs_retries"` 79 | 80 | Tracing candlelight.Tracing 81 | } 82 | 83 | type ServiceOptionsOut struct { 84 | fx.Out 85 | StatServiceOptions *stat.ServiceOptions 86 | TranslationServiceOptions *translation.ServiceOptions 87 | } 88 | 89 | func newHTTPClient(timeouts httpClientTimeout, tracing candlelight.Tracing) *http.Client { 90 | var transport http.RoundTripper = &http.Transport{ 91 | Dial: (&net.Dialer{ 92 | Timeout: timeouts.NetDialerTimeout, 93 | }).Dial, 94 | } 95 | transport = otelhttp.NewTransport(transport, 96 | otelhttp.WithPropagators(tracing.Propagator()), 97 | otelhttp.WithTracerProvider(tracing.TracerProvider()), 98 | ) 99 | 100 | return &http.Client{ 101 | Timeout: timeouts.ClientTimeout, 102 | Transport: transport, 103 | } 104 | } 105 | 106 | func createAuthAcquirer(config authAcquirerConfig) (acquire.Acquirer, error) { 107 | if config.JWT.AuthURL != "" && config.JWT.Buffer != 0 && config.JWT.Timeout != 0 { 108 | return acquire.NewRemoteBearerTokenAcquirer(config.JWT) 109 | } 110 | 111 | if config.Basic != "" { 112 | return acquire.NewFixedAuthAcquirer(config.Basic) 113 | } 114 | 115 | return nil, errors.New("auth acquirer not configured properly") 116 | } 117 | 118 | func v2WebhookValidators(c ancla.Config) (ancla.Validators, error) { 119 | //build validators and webhook handler for previous version that only check loopback. 120 | v, err := ancla.BuildValidators(ancla.ValidatorConfig{ 121 | URL: ancla.URLVConfig{ 122 | AllowLoopback: c.Validation.URL.AllowLoopback, 123 | AllowIP: true, 124 | AllowSpecialUseHosts: true, 125 | AllowSpecialUseIPs: true, 126 | }, 127 | TTL: c.Validation.TTL, 128 | }) 129 | if err != nil { 130 | return ancla.Validators{}, err 131 | } 132 | 133 | return v, nil 134 | } 135 | 136 | func provideWebhookHandlers(in provideWebhookHandlersIn) (out provideWebhookHandlersOut, err error) { 137 | // Webhooks (if not configured, handlers are not set up) 138 | if !in.V.IsSet(webhookConfigKey) { 139 | in.Logger.Info("Webhook service disabled") 140 | return 141 | } 142 | 143 | webhookConfig := in.WebhookConfig 144 | webhookConfig.Logger = in.Logger 145 | listenerMeasures := ancla.ListenerConfig{ 146 | Measures: *in.Measures, 147 | } 148 | webhookConfig.BasicClientConfig.HTTPClient = newHTTPClient(in.ArgusClientTimeout, in.Tracing) 149 | 150 | svc, err := ancla.NewService(webhookConfig, sallust.Get) 151 | if err != nil { 152 | return out, fmt.Errorf("failed to initialize webhook service: %s", err) 153 | } 154 | 155 | stopWatches, err := svc.StartListener(listenerMeasures, sallust.With) 156 | if err != nil { 157 | return out, fmt.Errorf("webhook service start listener error: %s", err) 158 | } 159 | in.Logger.Info("Webhook service enabled") 160 | 161 | in.Lifecycle.Append(fx.Hook{ 162 | OnStop: func(_ context.Context) error { 163 | stopWatches() 164 | return nil 165 | }, 166 | }) 167 | 168 | out.GetAllWebhooksHandler = ancla.NewGetAllWebhooksHandler(svc, ancla.HandlerConfig{ 169 | GetLogger: sallust.Get, 170 | }) 171 | 172 | builtValidators, err := ancla.BuildValidators(webhookConfig.Validation) 173 | if err != nil { 174 | return out, fmt.Errorf("failed to initialize webhook validators: %s", err) 175 | } 176 | 177 | out.AddWebhookHandler = ancla.NewAddWebhookHandler(svc, ancla.HandlerConfig{ 178 | V: builtValidators, 179 | DisablePartnerIDs: webhookConfig.DisablePartnerIDs, 180 | GetLogger: sallust.Get, 181 | }) 182 | 183 | v2Validators, err := v2WebhookValidators(webhookConfig) 184 | if err != nil { 185 | return out, fmt.Errorf("failed to setup v2 webhook validators: %s", err) 186 | } 187 | 188 | out.V2AddWebhookHandler = ancla.NewAddWebhookHandler(svc, ancla.HandlerConfig{ 189 | V: v2Validators, 190 | DisablePartnerIDs: webhookConfig.DisablePartnerIDs, 191 | GetLogger: sallust.Get, 192 | }) 193 | 194 | in.Logger.Info("Webhook service enabled") 195 | return 196 | } 197 | 198 | func provideHandlers() fx.Option { 199 | return fx.Options( 200 | arrange.ProvideKey(authAcquirerKey, authAcquirerConfig{}), 201 | fx.Provide( 202 | arrange.UnmarshalKey(webhookConfigKey, ancla.Config{}), 203 | arrange.UnmarshalKey("prometheus", touchstone.Config{}), 204 | arrange.UnmarshalKey("prometheus.handler", touchhttp.Config{}), 205 | provideWebhookHandlers, 206 | ), 207 | ) 208 | } 209 | 210 | func provideServiceOptions(in ServiceOptionsIn) (ServiceOptionsOut, error) { 211 | var errs error 212 | 213 | xmidtHTTPClient := newHTTPClient(in.XmidtClientTimeout, in.Tracing) 214 | stat_retries_counter, err := in.ServiceConfigsRetries.CurryWith(prometheus.Labels{apiLabel: stat_api}) 215 | errs = errors.Join(errs, err) 216 | // Stat Service configs 217 | statOptions := &stat.ServiceOptions{ 218 | HTTPTransactor: transaction.New( 219 | &transaction.Options{ 220 | Do: xhttp.RetryTransactor( //nolint:bodyclose 221 | xhttp.RetryOptions{ 222 | Logger: in.Logger, 223 | Retries: in.RequestMaxRetries, 224 | Interval: in.RequestRetryInterval, 225 | Counter: gokitprometheus.NewCounter(stat_retries_counter), 226 | }, 227 | xmidtHTTPClient.Do), 228 | RequestTimeout: in.XmidtClientTimeout.RequestTimeout, 229 | }), 230 | XmidtStatURL: fmt.Sprintf("%s/device/${device}/stat", in.TargetURL), 231 | } 232 | 233 | device_retries_counter, err := in.ServiceConfigsRetries.CurryWith(prometheus.Labels{apiLabel: device_api}) 234 | errs = errors.Join(errs, err) 235 | // WRP Service configs 236 | translationOptions := &translation.ServiceOptions{ 237 | XmidtWrpURL: fmt.Sprintf("%s/device", in.TargetURL), 238 | WRPSource: in.WRPSource, 239 | T: transaction.New( 240 | &transaction.Options{ 241 | RequestTimeout: in.XmidtClientTimeout.RequestTimeout, 242 | Do: xhttp.RetryTransactor( //nolint:bodyclose 243 | xhttp.RetryOptions{ 244 | Logger: in.Logger, 245 | Retries: in.RequestMaxRetries, 246 | Interval: in.RequestRetryInterval, 247 | Counter: gokitprometheus.NewCounter(device_retries_counter), 248 | }, 249 | xmidtHTTPClient.Do), 250 | }), 251 | } 252 | 253 | return ServiceOptionsOut{ 254 | StatServiceOptions: statOptions, 255 | TranslationServiceOptions: translationOptions, 256 | }, errs 257 | } 258 | -------------------------------------------------------------------------------- /routes.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | "os" 15 | "time" 16 | 17 | "github.com/gorilla/mux" 18 | "github.com/justinas/alice" 19 | "github.com/spf13/viper" 20 | "github.com/xmidt-org/ancla" 21 | "github.com/xmidt-org/arrange" 22 | "github.com/xmidt-org/arrange/arrangehttp" 23 | "github.com/xmidt-org/candlelight" 24 | "github.com/xmidt-org/httpaux" 25 | "github.com/xmidt-org/sallust" 26 | "github.com/xmidt-org/sallust/sallusthttp" 27 | "github.com/xmidt-org/touchstone" 28 | "github.com/xmidt-org/touchstone/touchhttp" 29 | "github.com/xmidt-org/tr1d1um/stat" 30 | "github.com/xmidt-org/tr1d1um/translation" 31 | "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" 32 | "go.uber.org/fx" 33 | "go.uber.org/zap" 34 | ) 35 | 36 | var ( 37 | errFailedWebhookUnmarshal = errors.New("failed to JSON unmarshal webhook") 38 | 39 | v2WarningHeader = "X-Xmidt-Warning" 40 | ) 41 | 42 | type primaryEndpointIn struct { 43 | fx.In 44 | V *viper.Viper 45 | Router *mux.Router `name:"server_primary"` 46 | APIRouter *mux.Router `name:"api_router"` 47 | AuthChain alice.Chain `name:"auth_chain"` 48 | Tracing candlelight.Tracing 49 | Logger *zap.Logger 50 | StatServiceOptions *stat.ServiceOptions 51 | TranslationOptions *translation.ServiceOptions 52 | AuthAcquirer authAcquirerConfig `name:"authAcquirer"` 53 | ReducedLoggingResponseCodes []int `name:"reducedLoggingResponseCodes"` 54 | TranslationServices []string `name:"supportedServices"` 55 | } 56 | 57 | type handleWebhookRoutesIn struct { 58 | fx.In 59 | Logger *zap.Logger 60 | Tracing candlelight.Tracing 61 | 62 | APIRouter *mux.Router `name:"api_router"` 63 | AuthChain alice.Chain `name:"auth_chain"` 64 | AddWebhookHandler http.Handler `name:"add_webhook_handler"` 65 | V2AddWebhookHandler http.Handler `name:"v2_add_webhook_handler"` 66 | GetAllWebhooksHandler http.Handler `name:"get_all_webhooks_handler"` 67 | WebhookConfig ancla.Config 68 | } 69 | 70 | type apiAltRouterIn struct { 71 | fx.In 72 | APIRouter *mux.Router `name:"api_router"` 73 | AlternateRouter *mux.Router `name:"server_alternate"` 74 | URLPrefix string `name:"url_prefix"` 75 | } 76 | 77 | type apiRouterIn struct { 78 | fx.In 79 | PrimaryRouter *mux.Router `name:"server_primary"` 80 | URLPrefix string `name:"url_prefix"` 81 | } 82 | 83 | type provideURLPrefixIn struct { 84 | fx.In 85 | PrevVerSupport bool `name:"previousVersionSupport"` 86 | } 87 | 88 | type primaryMetricMiddlewareIn struct { 89 | fx.In 90 | Primary alice.Chain `name:"middleware_primary_metrics"` 91 | } 92 | 93 | type alternateMetricMiddlewareIn struct { 94 | fx.In 95 | Alternate alice.Chain `name:"middleware_alternate_metrics"` 96 | } 97 | 98 | type healthMetricMiddlewareIn struct { 99 | fx.In 100 | Health alice.Chain `name:"middleware_health_metrics"` 101 | } 102 | 103 | type metricMiddlewareOut struct { 104 | fx.Out 105 | Primary alice.Chain `name:"middleware_primary_metrics"` 106 | Alternate alice.Chain `name:"middleware_alternate_metrics"` 107 | Health alice.Chain `name:"middleware_health_metrics"` 108 | } 109 | 110 | type metricsRoutesIn struct { 111 | fx.In 112 | Router *mux.Router `name:"server_metrics"` 113 | Handler touchhttp.Handler 114 | } 115 | 116 | func provideServers() fx.Option { 117 | return fx.Options( 118 | arrange.ProvideKey(reqMaxRetriesKey, 0), 119 | arrange.ProvideKey(reqRetryIntervalKey, time.Duration(0)), 120 | arrange.ProvideKey("previousVersionSupport", true), 121 | arrange.ProvideKey("targetURL", ""), 122 | arrange.ProvideKey("WRPSource", ""), 123 | arrange.ProvideKey(translationServicesKey, []string{}), 124 | fx.Provide(metricMiddleware), 125 | fx.Provide( 126 | fx.Annotated{ 127 | Name: "reducedLoggingResponseCodes", 128 | Target: arrange.UnmarshalKey(reducedTransactionLoggingCodesKey, []int{}), 129 | }, 130 | fx.Annotated{ 131 | Name: "api_router", 132 | Target: provideAPIRouter, 133 | }, 134 | fx.Annotated{ 135 | Name: "url_prefix", 136 | Target: provideURLPrefix, 137 | }, 138 | provideServiceOptions, 139 | ), 140 | arrangehttp.Server{ 141 | Name: "server_primary", 142 | Key: "servers.primary", 143 | Inject: arrange.Inject{ 144 | primaryMetricMiddlewareIn{}, 145 | }, 146 | }.Provide(), 147 | arrangehttp.Server{ 148 | Name: "server_alternate", 149 | Key: "servers.alternate", 150 | Inject: arrange.Inject{ 151 | alternateMetricMiddlewareIn{}, 152 | }, 153 | }.Provide(), 154 | arrangehttp.Server{ 155 | Name: "server_health", 156 | Key: "servers.health", 157 | Inject: arrange.Inject{ 158 | healthMetricMiddlewareIn{}, 159 | }, 160 | Invoke: arrange.Invoke{ 161 | func(r *mux.Router) { 162 | r.Handle("/health", httpaux.ConstantHandler{ 163 | StatusCode: http.StatusOK, 164 | }).Methods("GET") 165 | }, 166 | }, 167 | }.Provide(), 168 | arrangehttp.Server{ 169 | Name: "server_pprof", 170 | Key: "servers.pprof", 171 | }.Provide(), 172 | arrangehttp.Server{ 173 | Name: "server_metrics", 174 | Key: "servers.metrics", 175 | }.Provide(), 176 | fx.Invoke( 177 | handlePrimaryEndpoint, 178 | handleWebhookRoutes, 179 | buildMetricsRoutes, 180 | buildAPIAltRouter, 181 | ), 182 | ) 183 | } 184 | 185 | func handlePrimaryEndpoint(in primaryEndpointIn) { 186 | otelMuxOptions := []otelmux.Option{ 187 | otelmux.WithTracerProvider(in.Tracing.TracerProvider()), 188 | otelmux.WithPropagators(in.Tracing.Propagator()), 189 | } 190 | 191 | in.Router.Use( 192 | otelmux.Middleware("mainSpan", otelMuxOptions...), 193 | ) 194 | 195 | if in.V.IsSet(authAcquirerKey) { 196 | acquirer, err := createAuthAcquirer(in.AuthAcquirer) 197 | if err != nil { 198 | in.Logger.Error("Could not configure auth acquirer", zap.Error(err)) 199 | } else { 200 | in.TranslationOptions.AuthAcquirer = acquirer 201 | in.StatServiceOptions.AuthAcquirer = acquirer 202 | in.Logger.Info("Outbound request authentication token acquirer enabled") 203 | } 204 | } 205 | ss := stat.NewService(in.StatServiceOptions) 206 | ts := translation.NewService(in.TranslationOptions) 207 | 208 | // Must be called before translation.ConfigHandler due to mux path specificity (https://github.com/gorilla/mux#matching-routes). 209 | stat.ConfigHandler(&stat.Options{ 210 | S: ss, 211 | APIRouter: in.APIRouter, 212 | Authenticate: &in.AuthChain, 213 | Log: in.Logger, 214 | ReducedLoggingResponseCodes: in.ReducedLoggingResponseCodes, 215 | }) 216 | translation.ConfigHandler(&translation.Options{ 217 | S: ts, 218 | APIRouter: in.APIRouter, 219 | Authenticate: &in.AuthChain, 220 | Log: in.Logger, 221 | ValidServices: in.TranslationServices, 222 | ReducedLoggingResponseCodes: in.ReducedLoggingResponseCodes, 223 | }) 224 | } 225 | 226 | func handleWebhookRoutes(in handleWebhookRoutesIn) error { 227 | if in.AddWebhookHandler != nil && in.GetAllWebhooksHandler != nil { 228 | fixV2Middleware, err := fixV2Duration(sallust.Get, in.WebhookConfig.Validation.TTL, in.V2AddWebhookHandler) 229 | if err != nil { 230 | fmt.Fprintf(os.Stderr, "Failed to initialize v2 endpoint middleware: %v\n", err) 231 | return err 232 | } 233 | in.APIRouter.Handle("/hook", in.AuthChain.Then(fixV2Middleware(candlelight.EchoFirstTraceNodeInfo(in.Tracing.Propagator(), false)(in.AddWebhookHandler)))).Methods(http.MethodPost) 234 | in.APIRouter.Handle("/hooks", in.AuthChain.Then(candlelight.EchoFirstTraceNodeInfo(in.Tracing.Propagator(), false)(in.GetAllWebhooksHandler))) 235 | } 236 | return nil 237 | } 238 | 239 | func metricMiddleware(f *touchstone.Factory) (out metricMiddlewareOut) { 240 | var bundle touchhttp.ServerBundle 241 | 242 | primary, err1 := bundle.NewInstrumenter( 243 | touchhttp.ServerLabel, "server_primary", 244 | )(f) 245 | alternate, err2 := bundle.NewInstrumenter( 246 | touchhttp.ServerLabel, "server_alternate", 247 | )(f) 248 | health, err3 := bundle.NewInstrumenter( 249 | touchhttp.ServerLabel, "server_health", 250 | )(f) 251 | 252 | if err1 != nil || err2 != nil || err3 != nil { 253 | return 254 | } 255 | 256 | out.Primary = alice.New(primary.Then) 257 | out.Alternate = alice.New(alternate.Then) 258 | out.Health = alice.New(health.Then) 259 | 260 | return 261 | } 262 | 263 | func provideAPIRouter(in apiRouterIn) *mux.Router { 264 | return in.PrimaryRouter.PathPrefix(in.URLPrefix).Subrouter() 265 | } 266 | 267 | func buildAPIAltRouter(in apiAltRouterIn) { 268 | apiAltRouter := in.AlternateRouter.PathPrefix(in.URLPrefix).Subrouter() 269 | apiAltRouter.Handle("/device/{deviceid}/{service}", in.APIRouter) 270 | apiAltRouter.Handle("/device/{deviceid}/{service}/{parameter}", in.APIRouter) 271 | apiAltRouter.Handle("/device/{deviceid}/stat", in.APIRouter) 272 | apiAltRouter.Handle("/hook", in.APIRouter) 273 | apiAltRouter.Handle("/hooks", in.APIRouter) 274 | } 275 | 276 | func provideURLPrefix(in provideURLPrefixIn) string { 277 | // if we want to support the previous API version, then include it in the 278 | // api base. 279 | urlPrefix := fmt.Sprintf("/%s", apiBase) 280 | if in.PrevVerSupport { 281 | urlPrefix = fmt.Sprintf("/%s", apiBaseDualVersion) 282 | } 283 | return urlPrefix 284 | } 285 | 286 | //nolint:funlen 287 | func fixV2Duration(getLogger func(context.Context) *zap.Logger, config ancla.TTLVConfig, v2Handler http.Handler) (alice.Constructor, error) { 288 | if config.Now == nil { 289 | config.Now = time.Now 290 | } 291 | 292 | durationCheck, err := ancla.CheckDuration(config.Max) 293 | if err != nil { 294 | return nil, fmt.Errorf("failed to create duration check: %v", err) 295 | } 296 | 297 | untilCheck, err := ancla.CheckUntil(config.Jitter, config.Max, config.Now) 298 | if err != nil { 299 | return nil, fmt.Errorf("failed to create until check: %v", err) 300 | } 301 | 302 | return func(next http.Handler) http.Handler { 303 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 304 | vars := mux.Vars(r) 305 | // if this isn't v2, do nothing. 306 | if vars == nil || vars["version"] != prevAPIVersion { 307 | next.ServeHTTP(w, r) 308 | return 309 | } 310 | 311 | // if this is v2, we need to unmarshal and check the duration. If 312 | // the duration is bad, change it to 5m and add a header. Then use 313 | // the v2 handler. 314 | logger := sallusthttp.Get(r) 315 | 316 | requestPayload, err := ioutil.ReadAll(r.Body) 317 | if err != nil { 318 | v2ErrEncode(w, logger, err, 0) 319 | return 320 | } 321 | 322 | var wr ancla.WebhookRegistration 323 | err = json.Unmarshal(requestPayload, &wr) 324 | if err != nil { 325 | var e *json.UnmarshalTypeError 326 | if errors.As(err, &e) { 327 | v2ErrEncode(w, logger, 328 | fmt.Errorf("%w: %v must be of type %v", errFailedWebhookUnmarshal, e.Field, e.Type), 329 | http.StatusBadRequest) 330 | return 331 | } 332 | v2ErrEncode(w, logger, fmt.Errorf("%w: %v", errFailedWebhookUnmarshal, err), 333 | http.StatusBadRequest) 334 | return 335 | } 336 | 337 | // check to see if the Webhook has a valid until/duration. 338 | // If not, set the WebhookRegistration duration to 5m. 339 | webhook := wr.ToWebhook() 340 | if webhook.Until.IsZero() { 341 | if webhook.Duration == 0 { 342 | wr.Duration = ancla.CustomDuration(config.Max) 343 | w.Header().Add(v2WarningHeader, 344 | fmt.Sprintf("Unset duration and until fields will not be accepted in v3, webhook duration defaulted to %v", config.Max)) 345 | } else { 346 | durationErr := durationCheck(webhook) 347 | if durationErr != nil { 348 | wr.Duration = ancla.CustomDuration(config.Max) 349 | w.Header().Add(v2WarningHeader, 350 | fmt.Sprintf("Invalid duration will not be accepted in v3: %v, webhook duration defaulted to %v", durationErr, config.Max)) 351 | } 352 | } 353 | } else { 354 | untilErr := untilCheck(webhook) 355 | if untilErr != nil { 356 | wr.Until = time.Time{} 357 | wr.Duration = ancla.CustomDuration(config.Max) 358 | w.Header().Add(v2WarningHeader, 359 | fmt.Sprintf("Invalid until value will not be accepted in v3: %v, webhook duration defaulted to 5m", untilErr)) 360 | } 361 | } 362 | 363 | // put the body back in the request 364 | body, err := json.Marshal(wr) 365 | if err != nil { 366 | v2ErrEncode(w, logger, fmt.Errorf("failed to recreate request body: %v", err), 0) 367 | } 368 | r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 369 | 370 | if v2Handler == nil { 371 | v2Handler = next 372 | } 373 | v2Handler.ServeHTTP(w, r) 374 | }) 375 | }, nil 376 | } 377 | 378 | func v2ErrEncode(w http.ResponseWriter, logger *zap.Logger, err error, code int) { 379 | if code == 0 { 380 | code = http.StatusInternalServerError 381 | } 382 | logger.Error("sending non-200, non-404 response", 383 | zap.Error(err), zap.Int("code", code)) 384 | 385 | w.WriteHeader(code) 386 | 387 | json.NewEncoder(w).Encode( 388 | map[string]interface{}{ 389 | "message": err.Error(), 390 | }) 391 | } 392 | 393 | func buildMetricsRoutes(in metricsRoutesIn) { 394 | if in.Router != nil && in.Handler != nil { 395 | in.Router.Handle("/metrics", in.Handler).Methods("GET") 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /setup.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/spf13/pflag" 10 | "github.com/spf13/viper" 11 | "github.com/xmidt-org/sallust" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func setupFlagSet(fs *pflag.FlagSet) { 16 | fs.StringP("file", "f", "", "the configuration file to use. Overrides the search path.") 17 | fs.BoolP("debug", "d", false, "enables debug logging. Overrides configuration.") 18 | fs.BoolP("version", "v", false, "print version and exit") 19 | } 20 | 21 | func setup(args []string) (*viper.Viper, *zap.Logger, *pflag.FlagSet, error) { 22 | fs := pflag.NewFlagSet(applicationName, pflag.ContinueOnError) 23 | setupFlagSet(fs) 24 | err := fs.Parse(args) 25 | if err != nil { 26 | return nil, nil, fs, fmt.Errorf("failed to create parse args: %w", err) 27 | } 28 | 29 | v := viper.New() 30 | for k, va := range defaults { 31 | v.SetDefault(k, va) 32 | } 33 | 34 | if file, _ := fs.GetString("file"); len(file) > 0 { 35 | v.SetConfigFile(file) 36 | err = v.ReadInConfig() 37 | } else { 38 | v.SetConfigName(applicationName) 39 | v.AddConfigPath(fmt.Sprintf("/etc/%s", applicationName)) 40 | v.AddConfigPath(fmt.Sprintf("$HOME/.%s", applicationName)) 41 | v.AddConfigPath(".") 42 | err = v.ReadInConfig() 43 | } 44 | if err != nil { 45 | return v, nil, fs, fmt.Errorf("failed to read config file: %w", err) 46 | } 47 | 48 | if debug, _ := fs.GetBool("debug"); debug { 49 | v.Set("log.level", "DEBUG") 50 | } 51 | 52 | var c sallust.Config 53 | v.UnmarshalKey("logging", &c) 54 | l := zap.Must(c.Build()) 55 | return v, l, fs, err 56 | } 57 | -------------------------------------------------------------------------------- /stat/endpoint.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package stat 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/go-kit/kit/endpoint" 10 | ) 11 | 12 | type statRequest struct { 13 | DeviceID string 14 | AuthHeaderValue string 15 | } 16 | 17 | func makeStatEndpoint(s Service) endpoint.Endpoint { 18 | return func(ctx context.Context, r interface{}) (interface{}, error) { 19 | statReq := (r).(*statRequest) 20 | return s.RequestStat(ctx, statReq.AuthHeaderValue, statReq.DeviceID) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /stat/endpoint_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package stat 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | ) 10 | 11 | func TestMakeStatEndpoint(t *testing.T) { 12 | s := new(MockService) 13 | endpoint := makeStatEndpoint(s) 14 | 15 | sr := &statRequest{ 16 | DeviceID: "mac:1122334455", 17 | AuthHeaderValue: "a0", 18 | } 19 | 20 | s.On("RequestStat", context.TODO(), "a0", "mac:1122334455").Return(nil, nil) 21 | 22 | endpoint(context.TODO(), sr) 23 | s.AssertExpectations(t) 24 | } 25 | -------------------------------------------------------------------------------- /stat/mocks_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package stat 5 | 6 | import ( 7 | context "context" 8 | "net/http" 9 | 10 | "github.com/xmidt-org/tr1d1um/transaction" 11 | 12 | mock "github.com/stretchr/testify/mock" 13 | ) 14 | 15 | // MockService is an autogenerated mock type for the Service type 16 | type MockService struct { 17 | mock.Mock 18 | } 19 | 20 | // RequestStat provides a mock function with given fields: ctx, authHeaderValue, deviceID 21 | func (_m *MockService) RequestStat(ctx context.Context, authHeaderValue string, deviceID string) (*transaction.XmidtResponse, error) { 22 | ret := _m.Called(ctx, authHeaderValue, deviceID) 23 | 24 | var r0 *transaction.XmidtResponse 25 | if rf, ok := ret.Get(0).(func(context.Context, string, string) *transaction.XmidtResponse); ok { 26 | r0 = rf(ctx, authHeaderValue, deviceID) 27 | } else { 28 | if ret.Get(0) != nil { 29 | r0 = ret.Get(0).(*transaction.XmidtResponse) 30 | } 31 | } 32 | 33 | var r1 error 34 | if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { 35 | r1 = rf(ctx, authHeaderValue, deviceID) 36 | } else { 37 | r1 = ret.Error(1) 38 | } 39 | 40 | return r0, r1 41 | } 42 | 43 | // MockTr1d1umTransactor is an autogenerated mock type for the Tr1d1umTransactor type 44 | type MockTr1d1umTransactor struct { 45 | mock.Mock 46 | } 47 | 48 | // Transact provides a mock function with given fields: _a0 49 | func (_m *MockTr1d1umTransactor) Transact(_a0 *http.Request) (*transaction.XmidtResponse, error) { 50 | ret := _m.Called(_a0) 51 | 52 | var r0 *transaction.XmidtResponse 53 | if rf, ok := ret.Get(0).(func(*http.Request) *transaction.XmidtResponse); ok { 54 | r0 = rf(_a0) 55 | } else { 56 | if ret.Get(0) != nil { 57 | r0 = ret.Get(0).(*transaction.XmidtResponse) 58 | } 59 | } 60 | 61 | var r1 error 62 | if rf, ok := ret.Get(1).(func(*http.Request) error); ok { 63 | r1 = rf(_a0) 64 | } else { 65 | r1 = ret.Error(1) 66 | } 67 | 68 | return r0, r1 69 | } 70 | 71 | type mockAcquirer struct { 72 | mock.Mock 73 | } 74 | 75 | func (m *mockAcquirer) Acquire() (string, error) { 76 | args := m.Called() 77 | return args.String(0), args.Error(1) 78 | } 79 | -------------------------------------------------------------------------------- /stat/service.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package stat 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/xmidt-org/bascule/acquire" 12 | 13 | "github.com/xmidt-org/tr1d1um/transaction" 14 | ) 15 | 16 | // Service defines the behavior of the device statistics Tr1d1um Service. 17 | type Service interface { 18 | RequestStat(ctx context.Context, authHeaderValue, deviceID string) (*transaction.XmidtResponse, error) 19 | } 20 | 21 | // NewService constructs a new stat service instance given some options. 22 | func NewService(o *ServiceOptions) Service { 23 | return &service{ 24 | transactor: o.HTTPTransactor, 25 | authAcquirer: o.AuthAcquirer, 26 | xmidtStatURL: o.XmidtStatURL, 27 | } 28 | } 29 | 30 | // ServiceOptions defines the options needed to build a new stat service. 31 | type ServiceOptions struct { 32 | //Base Endpoint URL for device stats from the XMiDT API. 33 | //It's expected to have the "${device}" substring to perform device ID substitution. 34 | XmidtStatURL string 35 | 36 | //AuthAcquirer provides a mechanism to fetch auth tokens to complete the HTTP transaction 37 | //with the remote server. 38 | //(Optional) 39 | AuthAcquirer acquire.Acquirer 40 | 41 | //HTTPTransactor is the component that's responsible to make the HTTP 42 | //request to the XMiDT API and return only data we care about. 43 | HTTPTransactor transaction.T 44 | } 45 | 46 | type service struct { 47 | transactor transaction.T 48 | 49 | authAcquirer acquire.Acquirer 50 | 51 | xmidtStatURL string 52 | } 53 | 54 | // RequestStat contacts the XMiDT cluster for device statistics. 55 | func (s *service) RequestStat(ctx context.Context, authHeaderValue, deviceID string) (*transaction.XmidtResponse, error) { 56 | r, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.Replace(s.xmidtStatURL, "${device}", deviceID, 1), nil) 57 | 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | if s.authAcquirer != nil { 63 | authHeaderValue, err = s.authAcquirer.Acquire() 64 | if err != nil { 65 | return nil, err 66 | } 67 | } 68 | 69 | r.Header.Set("Authorization", authHeaderValue) 70 | return s.transactor.Transact(r) 71 | } 72 | -------------------------------------------------------------------------------- /stat/service_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package stat 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "net/http" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | "github.com/xmidt-org/tr1d1um/transaction" 15 | ) 16 | 17 | func TestRequestStat(t *testing.T) { 18 | testCases := []struct { 19 | Name string 20 | ExpectedRequestAuth string 21 | EnableAcquirer bool 22 | AcquirerReturnString string 23 | AcquirerReturnError error 24 | }{ 25 | { 26 | Name: "No auth acquirer", 27 | ExpectedRequestAuth: "pass-through-token", 28 | }, 29 | 30 | { 31 | Name: "Auth acquirer enabled - success", 32 | EnableAcquirer: true, 33 | ExpectedRequestAuth: "acquired-token", 34 | AcquirerReturnString: "acquired-token", 35 | }, 36 | 37 | { 38 | Name: "Auth acquirer enabled - error", 39 | EnableAcquirer: true, 40 | AcquirerReturnError: errors.New("error retrieving token"), 41 | }, 42 | } 43 | 44 | for _, testCase := range testCases { 45 | t.Run(testCase.Name, func(t *testing.T) { 46 | assert := assert.New(t) 47 | m := new(MockTr1d1umTransactor) 48 | var a *mockAcquirer 49 | 50 | options := &ServiceOptions{ 51 | XmidtStatURL: "http://localhost/stat/${device}", 52 | HTTPTransactor: m, 53 | } 54 | 55 | if testCase.EnableAcquirer { 56 | a = new(mockAcquirer) 57 | options.AuthAcquirer = a 58 | 59 | err := testCase.AcquirerReturnError 60 | a.On("Acquire").Return(testCase.AcquirerReturnString, err) 61 | } 62 | 63 | s := NewService(options) 64 | 65 | var requestMatcher = func(r *http.Request) bool { 66 | return r.URL.String() == "http://localhost/stat/mac:112233445566" && 67 | r.Header.Get("Authorization") == testCase.ExpectedRequestAuth 68 | } 69 | 70 | if testCase.AcquirerReturnError != nil { 71 | m.AssertNotCalled(t, "Transact", mock.Anything) 72 | } else { 73 | m.On("Transact", mock.MatchedBy(requestMatcher)).Return(&transaction.XmidtResponse{}, nil) 74 | } 75 | 76 | _, e := s.RequestStat(context.TODO(), "pass-through-token", "mac:112233445566") 77 | 78 | m.AssertExpectations(t) 79 | if testCase.EnableAcquirer { 80 | a.AssertExpectations(t) 81 | if testCase.AcquirerReturnError != nil { 82 | assert.Equal(testCase.AcquirerReturnError, e) 83 | } 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /stat/transport.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package stat 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "errors" 10 | "net/http" 11 | 12 | "github.com/xmidt-org/candlelight" 13 | "github.com/xmidt-org/sallust" 14 | "github.com/xmidt-org/tr1d1um/transaction" 15 | "github.com/xmidt-org/wrp-go/v3" 16 | "go.uber.org/zap" 17 | 18 | kithttp "github.com/go-kit/kit/transport/http" 19 | "github.com/gorilla/mux" 20 | "github.com/justinas/alice" 21 | ) 22 | 23 | const ( 24 | contentTypeHeaderKey = "Content-Type" 25 | authHeaderKey = "Authorization" 26 | ) 27 | 28 | var ( 29 | errResponseIsNil = errors.New("response is nil") 30 | ) 31 | 32 | // Options wraps the properties needed to set up the stat server 33 | type Options struct { 34 | S Service 35 | 36 | //APIRouter is assumed to be a subrouter with the API prefix path (i.e. 'api/v2') 37 | APIRouter *mux.Router 38 | Authenticate *alice.Chain 39 | Log *zap.Logger 40 | ReducedLoggingResponseCodes []int 41 | } 42 | 43 | // ConfigHandler sets up the server that powers the stat service 44 | // That is, it configures the mux paths to access the service 45 | func ConfigHandler(c *Options) { 46 | opts := []kithttp.ServerOption{ 47 | kithttp.ServerErrorEncoder(transaction.ErrorLogEncoder(sallust.Get, encodeError)), 48 | kithttp.ServerFinalizer(transaction.Log(c.ReducedLoggingResponseCodes)), 49 | } 50 | 51 | statHandler := kithttp.NewServer( 52 | makeStatEndpoint(c.S), 53 | decodeRequest, 54 | encodeResponse, 55 | opts..., 56 | ) 57 | 58 | c.APIRouter.Handle("/device/{deviceid}/stat", c.Authenticate.Then(candlelight.EchoFirstTraceNodeInfo(candlelight.Tracing{}.Propagator(), false)(transaction.Welcome(statHandler)))). 59 | Methods(http.MethodGet) 60 | } 61 | 62 | func decodeRequest(_ context.Context, r *http.Request) (req interface{}, err error) { 63 | var deviceID wrp.DeviceID 64 | if deviceID, err = wrp.ParseDeviceID(mux.Vars(r)["deviceid"]); err == nil { 65 | req = &statRequest{ 66 | AuthHeaderValue: r.Header.Get(authHeaderKey), 67 | DeviceID: string(deviceID), 68 | } 69 | } else { 70 | err = transaction.NewBadRequestError(err) 71 | } 72 | 73 | return 74 | } 75 | 76 | func encodeError(ctx context.Context, err error, w http.ResponseWriter) { 77 | w.Header().Set(contentTypeHeaderKey, "application/json") 78 | var ctxKeyReqTID string 79 | c := ctx.Value(transaction.ContextKeyRequestTID) 80 | if c != nil { 81 | ctxKeyReqTID = c.(string) 82 | } 83 | 84 | w.Header().Set(candlelight.HeaderWPATIDKeyName, ctxKeyReqTID) 85 | var ce transaction.CodedError 86 | if errors.As(err, &ce) { 87 | w.WriteHeader(ce.StatusCode()) 88 | } else { 89 | w.WriteHeader(http.StatusInternalServerError) 90 | 91 | //the real error is logged into our system before encodeError() is called 92 | //the idea behind masking it is to not send the external API consumer internal error messages 93 | err = transaction.ErrTr1d1umInternal 94 | } 95 | 96 | json.NewEncoder(w).Encode(map[string]string{ 97 | "message": err.Error(), 98 | }) 99 | } 100 | 101 | // encodeResponse simply forwards the response Tr1d1um got from the XMiDT API 102 | // TODO: What about if XMiDT cluster reports 500. There would be ambiguity 103 | // about which machine is actually having the error (Tr1d1um or the Xmidt API) 104 | // do we care to make that distinction? 105 | func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) (err error) { 106 | resp := response.(*transaction.XmidtResponse) 107 | 108 | if resp == nil || resp.Body == nil { 109 | err = errResponseIsNil 110 | return 111 | } 112 | 113 | if resp.Code == http.StatusOK { 114 | w.Header().Set("Content-Type", "application/json") 115 | } else { 116 | w.Header().Del("Content-Type") 117 | } 118 | 119 | var ctxKeyReqTID string 120 | c := ctx.Value(transaction.ContextKeyRequestTID) 121 | if c != nil { 122 | ctxKeyReqTID = c.(string) 123 | } 124 | 125 | w.Header().Set(candlelight.HeaderWPATIDKeyName, ctxKeyReqTID) 126 | 127 | transaction.ForwardHeadersByPrefix("", resp.ForwardedHeaders, w.Header()) 128 | 129 | w.WriteHeader(resp.Code) 130 | 131 | _, err = w.Write(resp.Body) 132 | return 133 | } 134 | -------------------------------------------------------------------------------- /stat/transport_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package stat 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "net/http" 13 | "net/http/httptest" 14 | "testing" 15 | 16 | "github.com/xmidt-org/tr1d1um/transaction" 17 | "github.com/xmidt-org/wrp-go/v3" 18 | 19 | "github.com/gorilla/mux" 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | var ctxTID = context.WithValue(context.Background(), transaction.ContextKeyRequestTID, "testTID") 24 | 25 | func TestDecodeRequest(t *testing.T) { 26 | 27 | t.Run("InvalidDeviceName", func(t *testing.T) { 28 | var assert = assert.New(t) 29 | 30 | var r = httptest.NewRequest(http.MethodGet, "http://localhost:8090", nil) 31 | 32 | r = mux.SetURLVars(r, map[string]string{"deviceid": "mac:1122@#8!!"}) 33 | 34 | resp, err := decodeRequest(ctxTID, r) 35 | 36 | assert.Nil(resp) 37 | assert.Equal(wrp.ErrorInvalidDeviceName.Error(), err.Error()) 38 | 39 | }) 40 | 41 | t.Run("NormalFlow", func(t *testing.T) { 42 | var assert = assert.New(t) 43 | 44 | var r = httptest.NewRequest(http.MethodGet, "http://localhost:8090/api/stat", nil) 45 | 46 | r = mux.SetURLVars(r, map[string]string{"deviceid": "mac:112233445566"}) 47 | r.Header.Set("Authorization", "a0") 48 | 49 | resp, err := decodeRequest(ctxTID, r) 50 | 51 | assert.Nil(err) 52 | 53 | assert.Equal(&statRequest{ 54 | AuthHeaderValue: "a0", 55 | DeviceID: "mac:112233445566", 56 | }, resp.(*statRequest)) 57 | }) 58 | } 59 | 60 | func TestEncodeError(t *testing.T) { 61 | t.Run("Timeouts", func(t *testing.T) { 62 | testErrorEncode(t, http.StatusServiceUnavailable, []error{ 63 | transaction.NewCodedError(errors.New("some bad network timeout error"), http.StatusServiceUnavailable), 64 | }) 65 | }) 66 | 67 | t.Run("BadRequest", func(t *testing.T) { 68 | testErrorEncode(t, http.StatusBadRequest, []error{ 69 | transaction.NewBadRequestError(wrp.ErrorInvalidDeviceName), 70 | }) 71 | }) 72 | 73 | t.Run("Internal", func(t *testing.T) { 74 | assert := assert.New(t) 75 | expected := bytes.NewBufferString("") 76 | 77 | json.NewEncoder(expected).Encode( 78 | map[string]string{ 79 | "message": transaction.ErrTr1d1umInternal.Error(), 80 | }, 81 | ) 82 | 83 | w := httptest.NewRecorder() 84 | encodeError(ctxTID, errors.New("tremendously unexpected internal error"), w) 85 | 86 | assert.EqualValues(http.StatusInternalServerError, w.Code) 87 | assert.EqualValues(expected.String(), w.Body.String()) 88 | }) 89 | } 90 | 91 | func testErrorEncode(t *testing.T, expectedCode int, es []error) { 92 | assert := assert.New(t) 93 | 94 | for _, e := range es { 95 | expected := bytes.NewBufferString("") 96 | json.NewEncoder(expected).Encode( 97 | map[string]string{ 98 | "message": e.Error(), 99 | }, 100 | ) 101 | 102 | w := httptest.NewRecorder() 103 | encodeError(ctxTID, e, w) 104 | 105 | assert.EqualValues(expectedCode, w.Code) 106 | assert.EqualValues(expected.String(), w.Body.String()) 107 | } 108 | } 109 | 110 | func TestEncodeResponse(t *testing.T) { 111 | 112 | tcs := []struct { 113 | desc string 114 | expectedErr error 115 | expectedContentTypeHeader string 116 | resp *transaction.XmidtResponse 117 | respRecorder *httptest.ResponseRecorder 118 | }{ 119 | { 120 | desc: "Base Success", 121 | resp: &transaction.XmidtResponse{ 122 | Code: http.StatusOK, 123 | ForwardedHeaders: http.Header{}, 124 | Body: []byte(`{"dBytesSent": "1024"}`), 125 | }, 126 | expectedContentTypeHeader: "application/json", 127 | respRecorder: httptest.NewRecorder(), 128 | }, 129 | { 130 | desc: "Response Body is Nil Failure", 131 | resp: &transaction.XmidtResponse{ 132 | Code: http.StatusOK, 133 | ForwardedHeaders: http.Header{}, 134 | }, 135 | expectedErr: errResponseIsNil, 136 | }, 137 | { 138 | desc: "Response is Nil Failure", 139 | expectedErr: errResponseIsNil, 140 | }, 141 | } 142 | for _, tc := range tcs { 143 | t.Run(tc.desc, func(t *testing.T) { 144 | assert := assert.New(t) 145 | //Tr1d1um just forwards the response 146 | var e = encodeResponse(ctxTID, tc.respRecorder, tc.resp) 147 | if tc.expectedErr != nil { 148 | assert.True(errors.Is(e, tc.expectedErr), 149 | fmt.Errorf("error [%v] doesn't contain error [%v] in its err chain", 150 | e, tc.expectedErr)) 151 | return 152 | } 153 | assert.Nil(e) 154 | assert.EqualValues(tc.expectedContentTypeHeader, tc.respRecorder.Header().Get("Content-Type")) 155 | assert.EqualValues(tc.resp.Body, tc.respRecorder.Body.String()) 156 | assert.EqualValues(tc.resp.Code, tc.respRecorder.Code) 157 | }) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tr1d1um.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ## SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 3 | ## SPDX-License-Identifier: Apache-2.0 4 | ######################################## 5 | # Labeling/Tracing via HTTP Headers Configuration 6 | ######################################## 7 | 8 | # The unique fully-qualified-domain-name of the server. It is provided to 9 | # the X-Tr1d1um-Server header for showing what server fulfilled the request 10 | # sent. 11 | # (Optional) 12 | server: "tr1d1um-local-instance-123.example.com" 13 | 14 | # Provides this build number to the X-Tr1d1um-Build header for 15 | # showing machine version information. The build number SHOULD 16 | # match the scheme `version-build` but there is not a strict requirement. 17 | # (Optional) 18 | build: "0.1.3-434" 19 | 20 | # Provides the region information to the X-Tr1d1um-Region header 21 | # for showing what region this machine is located in. The region 22 | # is arbitrary and optional. 23 | # (Optional) 24 | region: "east" 25 | 26 | # Provides the flavor information to the X-Tr1d1um-Flavor header 27 | # for showing what flavor this machine is associated with. The flavor 28 | # is arbitrary and optional. 29 | # (Optional) 30 | flavor: "mint" 31 | 32 | prometheus: 33 | defaultNamespace: webpa 34 | defaultSubsystem: tr1d1um 35 | constLabels: 36 | development: "true" 37 | handler: 38 | maxRequestsInFlight: 5 39 | timeout: 5s 40 | instrumentMetricHandler: true 41 | 42 | health: 43 | disableLogging: false 44 | custom: 45 | server: development 46 | 47 | ######################################## 48 | # Primary Endpoint Configuration 49 | ######################################## 50 | 51 | servers: 52 | primary: 53 | address: :6100 54 | disableHTTPKeepAlives: true 55 | header: 56 | X-Midt-Server: 57 | - tr1d1um 58 | X-Midt-Version: 59 | - development 60 | alternate: 61 | address: :8090 62 | header: 63 | X-Midt-Server: 64 | - tr1d1um 65 | X-Midt-Version: 66 | - development 67 | metrics: 68 | address: :6101 69 | disableHTTPKeepAlives: true 70 | header: 71 | X-Midt-Server: 72 | - tr1d1um 73 | X-Midt-Version: 74 | - development 75 | health: 76 | address: :6102 77 | disableHTTPKeepAlives: true 78 | header: 79 | X-Midt-Server: 80 | - tr1d1um 81 | X-Midt-Version: 82 | - development 83 | pprof: 84 | address: :6103 85 | 86 | ######################################## 87 | # Logging Related Configuration 88 | ######################################## 89 | 90 | logging: 91 | # OutputPaths is a list of URLs or file paths to write logging output to. 92 | outputPaths: 93 | - stdout 94 | # - /var/log/tr1d1um/tr1d1um.log 95 | 96 | # Level is the minimum enabled logging level. Note that this is a dynamic 97 | # level, so calling Config.Level.SetLevel will atomically change the log 98 | # level of all loggers descended from this config. 99 | level: debug 100 | 101 | # EncoderConfig sets options for the chosen encoder. See 102 | # zapcore.EncoderConfig for details. 103 | errorOutputPaths: 104 | - stderr 105 | - denopink-tr1d1um.log 106 | 107 | # EncoderConfig sets options for the chosen encoder. See 108 | # zapcore.EncoderConfig for details. 109 | encoderConfig: 110 | messageKey: message 111 | levelKey: key 112 | levelEncoder: lowercase 113 | # reducedLoggingResponseCodes allows disabling verbose transaction logs for 114 | # benign responses from the target server given HTTP status codes. 115 | # (Optional) 116 | # reducedLoggingResponseCodes: [200, 504] 117 | 118 | # Encoding sets the logger's encoding. Valid values are "json" and 119 | # "console", as well as any third-party encodings registered via 120 | # RegisterEncoder. 121 | encoding: json 122 | 123 | ############################################################################## 124 | # Webhooks Related Configuration 125 | ############################################################################## 126 | # webhook provides configuration for storing and obtaining webhook 127 | # information using Argus. 128 | # Optional: if key is not supplied, webhooks would be disabled. 129 | webhook: 130 | 131 | # disablePartnerIDs, if true, will allow webhooks to register without 132 | # checking the validity of the partnerIDs in the request 133 | # Defaults to 'false'. 134 | disablePartnerIDs: false 135 | 136 | # validation provides options for validating the webhook's URL and TTL 137 | # related fields. Some validation happens regardless of the configuration: 138 | # URLs must be a valid URL structure, the Matcher.DeviceID values must 139 | # compile into regular expressions, and the Events field must have at 140 | # least one value and all values must compile into regular expressions. 141 | validation: 142 | 143 | # url provides options for additional validation of the webhook's 144 | # Config.URL, FailureURL, and Config.AlternativeURLs fields. 145 | url: 146 | # httpsOnly will allow only URLs with https schemes through if true. 147 | # (Optional). Defaults to 'false'. 148 | httpsOnly: false 149 | 150 | # allowLoopback will allow any canonical or IP loopback address if 151 | # true. Otherwise, loopback addresses are considered invalid. 152 | # (Optional). Defaults to 'false'. 153 | allowLoopback: true 154 | 155 | # allowIP allows the different webhook URLs to have IP hostnames if set to true. 156 | # (Optional). Defaults to 'false'. 157 | allowIP: true 158 | 159 | # allowSpecialUseHosts allows URLs that include reserved domains if set to true. 160 | # Read more here: https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains 161 | # (Optional). Defaults to 'false'. 162 | allowSpecialUseHosts: true 163 | 164 | # allowSpecialUseIPs, if set to true, allows URLs that contain or route to IPs that have 165 | # been marked as reserved through various RFCs: rfc6761, rfc6890, rfc8190. 166 | # (Optional). Defaults to 'false'. 167 | allowSpecialUseIPs: true 168 | 169 | # invalidHosts is a slice that contains strings that we do not want 170 | # allowed in URLs, providing a way to deny certain domains or hostnames. 171 | # (Optional). Defaults to an empty slice. 172 | invalidHosts: [] 173 | 174 | # invalidSubnets is a list of IP subnets. If a URL contains an 175 | # IP or resolves to an IP in one of these subnets, the webhook is 176 | # considered invalid. 177 | # (Optional). Defaults to an empty slice. 178 | invalidSubnets: [] 179 | 180 | # ttl provides information for what is considered valid for time-related 181 | # fields (Duration and Until) in the webhook. A webhook set to expire 182 | # too far in the future is considered invalid, while a time in the past 183 | # is considered equivalent to a request to delete the webhook. 184 | # Regardless of this configuration, either Until or Duration must have a 185 | # non-zero value. 186 | ttl: 187 | # max is the length of time a webhook is allowed to live. The Duration 188 | # cannot be larger than this value, and the Until value cannot be set 189 | # later than the current time + max + jitter. 190 | max: 1m 191 | 192 | # jitter is the buffer time added when checking that the Until value is 193 | # valid. If there is a slight clock skew between servers or some delay 194 | # in the http request, jitter should help account for that when ensuring 195 | # that Until is not a time too far in the future. 196 | jitter: 10s 197 | 198 | # JWTParserType establishes which parser type will be used by the JWT token 199 | # acquirer used by Argus. Options include 'simple' and 'raw'. 200 | # Simple: parser assumes token payloads have the following structure: https://github.com/xmidt-org/bascule/blob/c011b128d6b95fa8358228535c63d1945347adaa/acquire/bearer.go#L77 201 | # Raw: parser assumes all of the token payload == JWT token 202 | # (Optional). Defaults to 'simple'. 203 | JWTParserType: "raw" 204 | BasicClientConfig: 205 | # listen is the subsection that configures the listening feature of the argus client 206 | # (Optional) 207 | listen: 208 | # pullInterval provides how often the current webhooks list gets refreshed. 209 | pullInterval: 5s 210 | 211 | # bucket is the partition name where webhooks will be stored. 212 | bucket: "webhooks" 213 | 214 | # address is Argus' network location. 215 | address: "http://localhost:6600" 216 | 217 | # auth the authentication method for argus. 218 | auth: 219 | # basic configures basic authentication for argus. 220 | # Must be of form: 'Basic xyz==' 221 | basic: "Basic dXNlcjpwYXNz" 222 | # 223 | # # jwt configures jwt style authentication for argus. 224 | # JWT: 225 | # # requestHeaders are added to the request for the token. 226 | # # (Optional) 227 | # # requestHeaders: 228 | # # "": "" 229 | # 230 | # # authURL is the URL to access the token. 231 | # authURL: "" 232 | # 233 | # # timeout is how long the request to get the token will take before 234 | # # timing out. 235 | # timeout: "1m" 236 | # 237 | # # buffer is the length of time before a token expires to get a new token. 238 | # buffer: "2m" 239 | 240 | 241 | ############################################################################## 242 | # Authorization Credentials 243 | ############################################################################## 244 | # jwtValidator provides Bearer auth configuration 245 | jwtValidator: 246 | config: 247 | resolve: 248 | # Template is a URI template used to fetch keys. This template may 249 | # use a single parameter named keyID, e.g. http://keys.com/{keyID}. 250 | # This field is required and has no default. 251 | template: "http://localhost/{keyID}" 252 | refresh: 253 | sources: 254 | # URI is the location where keys are served. By default, clortho supports 255 | # file://, http://, and https:// URIs, as well as standard file system paths 256 | # such as /etc/foo/bar.jwk. 257 | # 258 | # This field is required and has no default. 259 | - uri: "http://localhost/available" 260 | authx: 261 | inbound: 262 | # basic is a list of Basic Auth credentials intended to be used for local testing purposes 263 | # WARNING! Be sure to remove this from your production config 264 | basic: ["dXNlcjpwYXNz"] 265 | # capabilityCheck provides the details needed for checking an incoming JWT's 266 | # capabilities. If the type of check isn't provided, no checking is done. The 267 | # type can be "monitor" or "enforce". If it is empty or a different value, no 268 | # checking is done. If "monitor" is provided, the capabilities are checked but 269 | # the request isn't rejected when there isn't a valid capability for the 270 | # request. Instead, a message is logged. When "enforce" is provided, a request 271 | # that doesn't have the needed capability is rejected. 272 | # 273 | # The capability is expected to have the format: 274 | # 275 | # {prefix}{endpoint}:{method} 276 | # 277 | # The prefix can be a regular expression. If it's empty, no capability check 278 | # is done. The endpoint is a regular expression that should match the endpoint 279 | # the request was sent to. The method is usually the method of the request, such as 280 | # GET. The accept all method is a catchall string that indicates the capability 281 | # is approved for all methods. 282 | # (Optional) 283 | # capabilityCheck: 284 | # # type provides the mode for capability checking. 285 | # type: "enforce" 286 | # # prefix provides the regex to match the capability before the endpoint. 287 | # prefix: "prefix Here" 288 | # # acceptAllMethod provides a way to have a capability that allows all 289 | # # methods for a specific endpoint. 290 | # acceptAllMethod: "all" 291 | # # endpointBuckets provides regular expressions to use against the request 292 | # # endpoint in order to group requests for a metric label. 293 | # endpointBuckets: 294 | # - "hook\\b" 295 | # - "hooks\\b" 296 | # - "device/.*/stat\\b" 297 | # - "device/.*/config\\b" 298 | 299 | 300 | ############################################################################## 301 | # WRP and XMiDT Cloud configurations 302 | ############################################################################## 303 | 304 | # targetURL is the base URL of the XMiDT cluster 305 | targetURL: http://scytale:6300/api/v3 306 | 307 | # WRPSource is used as 'source' field for all outgoing WRP Messages 308 | WRPSource: "dns:tr1d1um.example.com" 309 | 310 | # supportedServices is a list of endpoints we support for the WRP producing endpoints 311 | # we will soon drop this configuration 312 | supportedServices: 313 | - "config" 314 | 315 | 316 | ############################################################################## 317 | # HTTP Transaction Configurations 318 | ############################################################################## 319 | # timeouts that apply to the Argus HTTP client. 320 | # (Optional) By default, the values below will be used. 321 | argusClientTimeout: 322 | # clientTimeout is the timeout for requests made through this 323 | # HTTP client. This timeout includes connection time, any 324 | # redirects, and reading the response body. 325 | clientTimeout: 50s 326 | 327 | # netDialerTimeout is the maximum amount of time the HTTP Client Dialer will 328 | # wait for a connect to complete. 329 | netDialerTimeout: 5s 330 | 331 | # timeouts that apply to the XMiDT HTTP client. 332 | # (Optional) By default, the values below will be used. 333 | xmidtClientTimeout: 334 | # clientTimeout is the timeout for the requests made through this 335 | # HTTP client. This timeout includes connection time, any 336 | # redirects, and reading the response body. 337 | clientTimeout: 135s 338 | 339 | # requestTimeout is the timeout imposed on requests made by this client 340 | # through context cancellation. 341 | # TODO since clientTimeouts are implemented through context cancellations, 342 | # we might not need this. 343 | requestTimeout: 129s 344 | 345 | # netDialerTimeout is the maximum amount of time the HTTP Client Dialer will 346 | # wait for a connect to complete. 347 | netDialerTimeout: 5s 348 | 349 | 350 | # requestRetryInterval is the time between HTTP request retries against XMiDT 351 | requestRetryInterval: "2s" 352 | 353 | # requestMaxRetries is the max number of times an HTTP request is retried against XMiDT in 354 | # case of ephemeral errors 355 | requestMaxRetries: 2 356 | 357 | # authAcquirer enables configuring the JWT or Basic auth header value factory for outgoing 358 | # requests to XMiDT. If both types are configured, JWT will be preferred. 359 | # (Optional) 360 | # authAcquirer: 361 | # JWT: 362 | # # requestHeaders are added to the request for the token. 363 | # # (Optional) 364 | # # requestHeaders: 365 | # # "": "" 366 | 367 | # # authURL is the URL to access for the token. 368 | # authURL: "" 369 | 370 | # # timeout is how long the request to get the token will take before 371 | # # timing out. 372 | # timeout: "1m" 373 | 374 | # # buffer is the length of time before a token expires to get a new token. 375 | # buffer: "2m" 376 | 377 | # Basic: "" # Must be of form: 'Basic xyz==' 378 | 379 | 380 | # tracing provides configuration around traces using OpenTelemetry. 381 | # (Optional). By default, a 'noop' tracer provider is used and tracing is disabled. 382 | tracing: 383 | # provider is the name of the trace provider to use. Currently, otlp/grpc, otlp/http, stdout, jaeger and zipkin are supported. 384 | # 'noop' can also be used as provider to explicitly disable tracing. 385 | provider: "noop" 386 | 387 | # skipTraceExport only applies when provider is stdout. Set skipTraceExport to true 388 | # so that trace information is not written to stdout. 389 | # skipTraceExport: true 390 | 391 | # endpoint is where trace information should be routed. Applies to otlp, zipkin, and jaegar. OTLP/gRPC uses port 4317 by default. 392 | # OTLP/HTTP uses port 4318 by default. 393 | # endpoint: "localhost:4317" 394 | 395 | # ParentBased and NoParent dictate if and when new spans should be created. 396 | # ParentBased = "ignore" (default), tracing is effectively turned off and the "NoParent" value is ignored 397 | # ParentBased = "honor", the sampling decision is made by the parent of the span 398 | parentBased: ignore 399 | 400 | # NoParent decides if a root span should be initiated in the case where there is no existing parent 401 | # This value is ignored if ParentBased = "ignore" 402 | # NoParent = "never" (default), root spans are not initiated 403 | # NoParent = "always", roots spans are initiated 404 | noParent: never 405 | 406 | # previousVersionSupport allows us to support two different major versions of 407 | # the API at the same time from the same application. When this is true, 408 | # tr1d1um will support both "/v2" and "/v3" endpoints. When false, only "/v3" 409 | # endpoints will be supported. 410 | previousVersionSupport: true 411 | -------------------------------------------------------------------------------- /transaction/context.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package transaction 5 | 6 | type contextKey int 7 | 8 | // Keys to important context values on incoming requests to TR1D1UM 9 | const ( 10 | ContextKeyRequestArrivalTime contextKey = iota 11 | ContextKeyRequestTID 12 | ) 13 | -------------------------------------------------------------------------------- /transaction/errors.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package transaction 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "net/http" 10 | 11 | kithttp "github.com/go-kit/kit/transport/http" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | // ErrTr1d1umInternal should be the error shown to external API consumers in Internal Server error cases 16 | var ErrTr1d1umInternal = errors.New("oops! Something unexpected went wrong in this service") 17 | 18 | // CodedError describes the behavior of an error that additionally has an HTTP status code used for TR1D1UM business logic 19 | type CodedError interface { 20 | error 21 | StatusCode() int 22 | } 23 | 24 | type codedError struct { 25 | error 26 | statusCode int 27 | } 28 | 29 | func (c *codedError) StatusCode() int { 30 | return c.statusCode 31 | } 32 | 33 | // NewBadRequestError is the constructor for an error returned for bad HTTP requests to tr1d1um 34 | func NewBadRequestError(e error) CodedError { 35 | return NewCodedError(e, http.StatusBadRequest) 36 | } 37 | 38 | // NewCodedError upgrades an Error to a CodedError 39 | // e must not be non-nil to avoid panics 40 | func NewCodedError(e error, code int) CodedError { 41 | return &codedError{ 42 | error: e, 43 | statusCode: code, 44 | } 45 | } 46 | 47 | // ErrorLogEncoder decorates the errorEncoder in such a way that 48 | // errors are logged with their corresponding unique request identifier 49 | func ErrorLogEncoder(getLogger func(context.Context) *zap.Logger, ee kithttp.ErrorEncoder) kithttp.ErrorEncoder { 50 | return func(ctx context.Context, e error, w http.ResponseWriter) { 51 | code := http.StatusInternalServerError 52 | var sc kithttp.StatusCoder 53 | if errors.As(e, &sc) { 54 | code = sc.StatusCode() 55 | } 56 | 57 | if l := getLogger(ctx); l != nil && code != http.StatusNotFound { 58 | l.Error("sending non-200, non-404 response", zap.String("error", e.Error()), 59 | zap.Any("tid", ctx.Value(ContextKeyRequestTID)), 60 | ) 61 | } 62 | 63 | ee(ctx, e, w) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /transaction/errors_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package transaction 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "net/http" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/xmidt-org/sallust" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | func TestNewCodedError(t *testing.T) { 18 | assert := assert.New(t) 19 | var ce = NewCodedError(errors.New("test"), 500) 20 | assert.NotNil(ce) 21 | assert.EqualValues(500, ce.StatusCode()) 22 | assert.EqualValues("test", ce.Error()) 23 | } 24 | 25 | func TestBadRequestError(t *testing.T) { 26 | assert := assert.New(t) 27 | var ce = NewBadRequestError(errors.New("test")) 28 | assert.NotNil(ce) 29 | assert.EqualValues(400, ce.StatusCode()) 30 | assert.EqualValues("test", ce.Error()) 31 | } 32 | 33 | func TestErrorLogEncoder(t *testing.T) { 34 | tcs := []struct { 35 | desc string 36 | getLogger func(context.Context) *zap.Logger 37 | }{ 38 | { 39 | desc: "nil getlogger", 40 | getLogger: nil, 41 | }, 42 | { 43 | desc: "valid getlogger", 44 | getLogger: sallust.Get, 45 | }, 46 | } 47 | 48 | for _, tc := range tcs { 49 | t.Run(tc.desc, func(t *testing.T) { 50 | assert := assert.New(t) 51 | e := func(ctx context.Context, _ error, _ http.ResponseWriter) { 52 | assert.EqualValues("tid00", ctx.Value(ContextKeyRequestTID)) 53 | } 54 | le := ErrorLogEncoder(tc.getLogger, e) 55 | 56 | if tc.getLogger == nil { 57 | assert.Panics(func() { 58 | //assumes TID is context 59 | le(context.WithValue(context.TODO(), ContextKeyRequestTID, "tid00"), errors.New("test"), nil) 60 | }) 61 | } else { 62 | assert.NotPanics(func() { 63 | //assumes TID is context 64 | le(context.WithValue(context.TODO(), ContextKeyRequestTID, "tid00"), errors.New("test"), nil) 65 | }) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /transaction/transactor.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package transaction 5 | 6 | import ( 7 | "context" 8 | "crypto/rand" 9 | "encoding/base64" 10 | "encoding/json" 11 | "io/ioutil" 12 | "net/http" 13 | "strings" 14 | "time" 15 | 16 | kithttp "github.com/go-kit/kit/transport/http" 17 | "github.com/gorilla/mux" 18 | "github.com/xmidt-org/candlelight" 19 | "github.com/xmidt-org/sallust" 20 | "github.com/xmidt-org/wrp-go/v3" 21 | "go.uber.org/zap" 22 | ) 23 | 24 | // XmidtResponse represents the data that a tr1d1um transactor keeps from an HTTP request to 25 | // the XMiDT API 26 | type XmidtResponse struct { 27 | 28 | //Code is the HTTP Status code received from the transaction 29 | Code int 30 | 31 | //ForwardedHeaders contains all the headers tr1d1um keeps from the transaction 32 | ForwardedHeaders http.Header 33 | 34 | //Body represents the full data off the XMiDT http.Response body 35 | Body []byte 36 | } 37 | 38 | // T performs a typical HTTP request but 39 | // enforces some logic onto the HTTP transaction such as 40 | // context-based timeout and header filtering 41 | // this is a common utility for the stat and config tr1d1um services 42 | type T interface { 43 | Transact(*http.Request) (*XmidtResponse, error) 44 | } 45 | 46 | // Options include parameters needed to configure the transactor 47 | type Options struct { 48 | //RequestTimeout is the deadline duration for the HTTP transaction to be completed 49 | RequestTimeout time.Duration 50 | 51 | //Do is the core responsible to perform the actual HTTP request 52 | Do func(*http.Request) (*http.Response, error) 53 | } 54 | 55 | type transactor struct { 56 | RequestTimeout time.Duration 57 | Do func(*http.Request) (*http.Response, error) 58 | } 59 | 60 | type Request struct { 61 | Address string `json:"address,omitempty"` 62 | Path string `json:"path,omitempty"` 63 | Query string `json:"query,omitempty"` 64 | Method string `json:"method,omitempty"` 65 | } 66 | 67 | type response struct { 68 | Code int `json:"code,omitempty"` 69 | Headers interface{} `json:"headers,omitempty"` 70 | } 71 | 72 | func (re *Request) MarshalJSON() ([]byte, error) { 73 | return json.Marshal(re) 74 | } 75 | 76 | func (rs *response) MarshalJSON() ([]byte, error) { 77 | return json.Marshal(rs) 78 | } 79 | 80 | func New(o *Options) T { 81 | return &transactor{ 82 | Do: o.Do, 83 | RequestTimeout: o.RequestTimeout, 84 | } 85 | } 86 | 87 | func (t *transactor) Transact(req *http.Request) (result *XmidtResponse, err error) { 88 | ctx, cancel := context.WithTimeout(req.Context(), t.RequestTimeout) 89 | defer cancel() 90 | 91 | var resp *http.Response 92 | if resp, err = t.Do(req.WithContext(ctx)); err == nil { 93 | result = &XmidtResponse{ 94 | ForwardedHeaders: make(http.Header), 95 | Body: []byte{}, 96 | } 97 | 98 | ForwardHeadersByPrefix("X", resp.Header, result.ForwardedHeaders) 99 | result.Code = resp.StatusCode 100 | 101 | defer resp.Body.Close() 102 | 103 | result.Body, err = ioutil.ReadAll(resp.Body) 104 | return 105 | } 106 | 107 | //Timeout, network errors, etc. 108 | err = NewCodedError(err, http.StatusServiceUnavailable) 109 | return 110 | } 111 | 112 | // Log is used by the different Tr1d1um services to 113 | // keep track of incoming requests and their corresponding responses 114 | func Log(reducedLoggingResponseCodes []int) kithttp.ServerFinalizerFunc { 115 | return func(ctx context.Context, code int, r *http.Request) { 116 | tid, _ := ctx.Value(ContextKeyRequestTID).(string) 117 | logger := sallust.Get(ctx) 118 | requestArrival, ok := ctx.Value(ContextKeyRequestArrivalTime).(time.Time) 119 | 120 | if !ok { 121 | logger = logger.With( 122 | zap.Any("duration", time.Since(requestArrival)), 123 | ) 124 | } else { 125 | traceID, spanID, ok := candlelight.ExtractTraceInfo(ctx) 126 | if !ok { 127 | logger.Error("Request arrival not capture for logger", zap.String("tid", tid)) 128 | } else { 129 | logger.Error("Request arrival not capture for logger", zap.String("tid", tid), zap.String(candlelight.TraceIdLogKeyName, traceID), zap.String(candlelight.SpanIDLogKeyName, spanID)) 130 | } 131 | } 132 | 133 | includeHeaders := true 134 | response := response{Code: code} 135 | 136 | for _, responseCode := range reducedLoggingResponseCodes { 137 | if responseCode == code { 138 | includeHeaders = false 139 | break 140 | } 141 | } 142 | 143 | if includeHeaders { 144 | response.Headers = ctx.Value(kithttp.ContextKeyResponseHeaders) 145 | } 146 | 147 | logger.Info("response", zap.Any("response", response)) 148 | } 149 | } 150 | 151 | // ForwardHeadersByPrefix copies headers h where the source and target are 'from' 152 | // and 'to' respectively such that key(h) has p as prefix 153 | func ForwardHeadersByPrefix(p string, from http.Header, to http.Header) { 154 | for headerKey, headerValues := range from { 155 | if strings.HasPrefix(headerKey, p) { 156 | for _, headerValue := range headerValues { 157 | to.Add(headerKey, headerValue) 158 | } 159 | } 160 | } 161 | } 162 | 163 | // Welcome is an Alice-style constructor that defines necessary request 164 | // context values assumed to exist by the delegate. These values should 165 | // be those expected to be used both in and outside the gokit server flow 166 | func Welcome(delegate http.Handler) http.Handler { 167 | return http.HandlerFunc( 168 | func(w http.ResponseWriter, r *http.Request) { 169 | var tid string 170 | 171 | if tid = r.Header.Get(candlelight.HeaderWPATIDKeyName); tid == "" { 172 | tid = genTID() 173 | } 174 | 175 | ctx := context.WithValue(r.Context(), ContextKeyRequestTID, tid) 176 | ctx = context.WithValue(ctx, ContextKeyRequestArrivalTime, time.Now()) 177 | ctx = addDeviceIdToLog(ctx, r) 178 | delegate.ServeHTTP(w, r.WithContext(ctx)) 179 | }) 180 | } 181 | 182 | // genTID generates a 16-byte long string 183 | // it returns "N/A" in the extreme case the random string could not be generated 184 | func genTID() (tid string) { 185 | buf := make([]byte, 16) 186 | tid = "" 187 | if _, err := rand.Read(buf); err == nil { 188 | tid = base64.RawURLEncoding.EncodeToString(buf) 189 | } 190 | return 191 | } 192 | 193 | // updateLogger updates the logger with a device id field and adds it back into the context. 194 | func addDeviceIdToLog(ctx context.Context, r *http.Request) context.Context { 195 | did := getDeviceId(r) 196 | f := zap.String("deviceid", did) 197 | 198 | logger := sallust.Get(ctx) 199 | logger = logger.With(f) 200 | ctx = sallust.With(ctx, logger) 201 | 202 | return ctx 203 | } 204 | 205 | // getDeviceId extracts device id from the request path params 206 | func getDeviceId(r *http.Request) string { 207 | vars := mux.Vars(r) 208 | id := vars["deviceid"] 209 | if id == "" { 210 | id = "mac:000000000000" 211 | } 212 | 213 | normalized, err := wrp.ParseDeviceID(id) 214 | if err != nil { 215 | id = "invalid:" + id 216 | } else { 217 | id = string(normalized) 218 | } 219 | return id 220 | } 221 | -------------------------------------------------------------------------------- /transaction/transactor_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package transaction 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "errors" 10 | "io/ioutil" 11 | "net/http" 12 | "net/http/httptest" 13 | "testing" 14 | "time" 15 | 16 | "github.com/gorilla/mux" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/require" 19 | "github.com/xmidt-org/candlelight" 20 | "github.com/xmidt-org/sallust" 21 | "go.uber.org/zap" 22 | "go.uber.org/zap/zapcore" 23 | "go.uber.org/zap/zaptest" 24 | "go.uber.org/zap/zaptest/observer" 25 | ) 26 | 27 | func TestTransactError(t *testing.T) { 28 | assert := assert.New(t) 29 | 30 | plainErr := errors.New("network test error") 31 | expectedErr := NewCodedError(plainErr, 503) 32 | 33 | transactor := New(&Options{ 34 | Do: func(_ *http.Request) (*http.Response, error) { 35 | return nil, plainErr 36 | }, 37 | }) 38 | 39 | r := httptest.NewRequest(http.MethodGet, "localhost:6003/test", nil) 40 | _, e := transactor.Transact(r) 41 | 42 | assert.EqualValues(expectedErr, e) 43 | } 44 | 45 | func TestTransactIdeal(t *testing.T) { 46 | assert := assert.New(t) 47 | 48 | expected := &XmidtResponse{ 49 | Code: 404, 50 | Body: []byte("not found"), 51 | ForwardedHeaders: http.Header{"X-A": []string{"a", "b"}}, 52 | } 53 | 54 | rawXmidtResponse := &http.Response{ 55 | StatusCode: 404, 56 | Body: ioutil.NopCloser(bytes.NewBufferString("not found")), 57 | Header: http.Header{ 58 | "X-A": []string{"a", "b"}, //should be forwarded 59 | "Y-A": []string{"c", "d"}, //should be ignored 60 | }, 61 | } 62 | 63 | transactor := New(&Options{ 64 | Do: func(_ *http.Request) (*http.Response, error) { 65 | return rawXmidtResponse, nil 66 | }, 67 | }) 68 | 69 | r := httptest.NewRequest(http.MethodGet, "localhost:6003/test", nil) 70 | actual, e := transactor.Transact(r) 71 | assert.Nil(e) 72 | assert.EqualValues(expected, actual) 73 | } 74 | 75 | func TestForwardHeadersByPrefix(t *testing.T) { 76 | t.Run("NoHeaders", func(t *testing.T) { 77 | assert := assert.New(t) 78 | 79 | var to, from = make(http.Header), make(http.Header) 80 | 81 | ForwardHeadersByPrefix("H", from, to) 82 | assert.Empty(to) 83 | }) 84 | 85 | t.Run("MultipleHeadersFiltered", func(t *testing.T) { 86 | assert := assert.New(t) 87 | var to, from = make(http.Header), make(http.Header) 88 | 89 | from.Add("Helium", "3") 90 | from.Add("Hydrogen", "5") 91 | from.Add("Hydrogen", "6") 92 | 93 | ForwardHeadersByPrefix("He", from, to) 94 | assert.NotEmpty(to) 95 | assert.Len(to, 1) 96 | assert.EqualValues("3", to.Get("Helium")) 97 | }) 98 | 99 | t.Run("MultipleHeadersFilteredFullArray", func(t *testing.T) { 100 | assert := assert.New(t) 101 | 102 | var to, from = make(http.Header), make(http.Header) 103 | 104 | from.Add("Helium", "3") 105 | from.Add("Hydrogen", "5") 106 | from.Add("Hydrogen", "6") 107 | 108 | ForwardHeadersByPrefix("H", from, to) 109 | assert.NotEmpty(to) 110 | assert.Len(to, 2) 111 | assert.EqualValues([]string{"5", "6"}, to["Hydrogen"]) 112 | }) 113 | 114 | t.Run("NilCases", func(t *testing.T) { 115 | var to, from = make(http.Header), make(http.Header) 116 | //none of these should panic 117 | ForwardHeadersByPrefix("", nil, nil) 118 | ForwardHeadersByPrefix("", from, nil) 119 | ForwardHeadersByPrefix("", from, to) 120 | }) 121 | } 122 | 123 | func TestWelcome(t *testing.T) { 124 | tests := []struct { 125 | description string 126 | genReq func() *http.Request 127 | expectedTID string 128 | }{ 129 | { 130 | description: "Generated TID", 131 | genReq: func() (r *http.Request) { 132 | r = httptest.NewRequest(http.MethodGet, "http://localhost", nil) 133 | return 134 | }, 135 | }, 136 | { 137 | description: "Given TID", 138 | genReq: func() (r *http.Request) { 139 | r = httptest.NewRequest(http.MethodGet, "http://localhost", nil) 140 | r.Header.Set(candlelight.HeaderWPATIDKeyName, "tid01") 141 | return 142 | }, 143 | expectedTID: "tid01", 144 | }, 145 | } 146 | 147 | for _, tc := range tests { 148 | t.Run(tc.description, func(t *testing.T) { 149 | assert := assert.New(t) 150 | require := require.New(t) 151 | handler := http.HandlerFunc( 152 | func(_ http.ResponseWriter, r *http.Request) { 153 | assert.NotNil(r.Context().Value(ContextKeyRequestArrivalTime)) 154 | tid := r.Context().Value(ContextKeyRequestTID) 155 | require.NotNil(tid) 156 | tid = tid.(string) 157 | if assert.NotZero(tid) && tc.expectedTID != "" { 158 | assert.Equal(tc.expectedTID, tid) 159 | } 160 | }) 161 | decorated := Welcome(handler) 162 | decorated.ServeHTTP(nil, tc.genReq()) 163 | 164 | }) 165 | } 166 | } 167 | 168 | func TestLog(t *testing.T) { 169 | ctxWithArrivalTime := context.WithValue(context.Background(), ContextKeyRequestArrivalTime, time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)) 170 | tcs := []struct { 171 | desc string 172 | reducedLoggingResponseCodes []int 173 | ctx context.Context 174 | code int 175 | request *http.Request 176 | expectedLogCount int 177 | }{ 178 | { 179 | desc: "Sanity Check", 180 | reducedLoggingResponseCodes: []int{}, 181 | ctx: context.Background(), 182 | code: 200, 183 | request: &http.Request{}, 184 | expectedLogCount: 1, 185 | }, 186 | { 187 | desc: "Arrival Time Present", 188 | reducedLoggingResponseCodes: []int{}, 189 | ctx: ctxWithArrivalTime, 190 | code: 200, 191 | request: &http.Request{}, 192 | expectedLogCount: 2, 193 | }, 194 | { 195 | desc: "IncludeHeaders is False", 196 | reducedLoggingResponseCodes: []int{200}, 197 | ctx: context.Background(), 198 | code: 200, 199 | request: &http.Request{}, 200 | expectedLogCount: 1, 201 | }, 202 | } 203 | 204 | for _, tc := range tcs { 205 | t.Run(tc.desc, func(t *testing.T) { 206 | assert := assert.New(t) 207 | var logCount = 0 208 | logger := zaptest.NewLogger(t, zaptest.WrapOptions(zap.Hooks( 209 | func(e zapcore.Entry) error { 210 | logCount++ 211 | return nil 212 | }))) 213 | ctx := sallust.With(tc.ctx, logger) 214 | s := Log(tc.reducedLoggingResponseCodes) 215 | s(ctx, tc.code, tc.request) 216 | assert.Equal(tc.expectedLogCount, logCount) 217 | }) 218 | } 219 | } 220 | 221 | func TestAddDeviceIdToLog(t *testing.T) { 222 | tests := []struct { 223 | desc string 224 | ctx context.Context 225 | req func() (r *http.Request) 226 | deviceid string 227 | }{ 228 | { 229 | desc: "device id in request", 230 | ctx: context.Background(), 231 | req: func() (r *http.Request) { 232 | r = httptest.NewRequest(http.MethodGet, "http://localhost:6100/api/v2/device/", nil) 233 | r = mux.SetURLVars(r, map[string]string{"deviceid": "mac:112233445577"}) 234 | return 235 | }, 236 | deviceid: "mac:112233445577", 237 | }, 238 | { 239 | desc: "device id added in code", 240 | ctx: context.Background(), 241 | req: func() (r *http.Request) { 242 | r = httptest.NewRequest(http.MethodGet, "http://localhost:6100/api/v2/device/", nil) 243 | return 244 | }, 245 | deviceid: "mac:000000000000", 246 | }, 247 | } 248 | for _, tc := range tests { 249 | t.Run(tc.desc, func(t *testing.T) { 250 | assert := assert.New(t) 251 | observedZapCore, observedLogs := observer.New(zap.DebugLevel) 252 | observedLogger := zap.New(observedZapCore) 253 | ctx := sallust.With(tc.ctx, observedLogger) 254 | ctx = addDeviceIdToLog(ctx, tc.req()) 255 | 256 | logger := sallust.Get(ctx) 257 | logger.Debug("test") 258 | gotLog := observedLogs.All()[0].Context 259 | 260 | assert.Equal("deviceid", gotLog[0].Key) 261 | assert.Equal(tc.deviceid, gotLog[0].String) 262 | 263 | }) 264 | } 265 | } 266 | 267 | func TestGetDeviceId(t *testing.T) { 268 | tests := []struct { 269 | desc string 270 | req func() *http.Request 271 | expected string 272 | }{ 273 | { 274 | desc: "Request has id", 275 | req: func() (r *http.Request) { 276 | r = httptest.NewRequest(http.MethodGet, "http://localhost:6100/api/v2/device/", nil) 277 | r = mux.SetURLVars(r, map[string]string{"deviceid": "mac:11:22:33:44:55:Aa"}) 278 | return 279 | }, 280 | expected: "mac:1122334455aa", 281 | }, 282 | { 283 | desc: "no id", 284 | req: func() (r *http.Request) { 285 | r = httptest.NewRequest(http.MethodGet, "http://localhost:6100/api/v2/device/", nil) 286 | return 287 | }, 288 | expected: "mac:000000000000", 289 | }, 290 | { 291 | desc: "invalid id", 292 | req: func() (r *http.Request) { 293 | r = httptest.NewRequest(http.MethodGet, "http://localhost:6100/api/v2/device/", nil) 294 | r = mux.SetURLVars(r, map[string]string{"deviceid": "unsupported:frog"}) 295 | return 296 | }, 297 | expected: "invalid:unsupported:frog", 298 | }, 299 | } 300 | for _, tc := range tests { 301 | t.Run(tc.desc, func(t *testing.T) { 302 | assert := assert.New(t) 303 | id := getDeviceId(tc.req()) 304 | assert.NotNil(id) 305 | assert.Equal(tc.expected, id) 306 | }) 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /translation/endpoint.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package translation 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/go-kit/kit/endpoint" 10 | "github.com/xmidt-org/wrp-go/v3" 11 | ) 12 | 13 | type wrpRequest struct { 14 | WRPMessage *wrp.Message 15 | AuthHeaderValue string 16 | } 17 | 18 | func makeTranslationEndpoint(s Service) endpoint.Endpoint { 19 | return func(ctx context.Context, request interface{}) (interface{}, error) { 20 | wrpReq := (request).(*wrpRequest) 21 | return s.SendWRP(ctx, wrpReq.WRPMessage, wrpReq.AuthHeaderValue) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /translation/endpoint_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package translation 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/xmidt-org/wrp-go/v3" 11 | ) 12 | 13 | func TestMakeTranslationEndpoint(t *testing.T) { 14 | var s = new(MockService) 15 | 16 | r := &wrpRequest{ 17 | WRPMessage: new(wrp.Message), 18 | AuthHeaderValue: "a0", 19 | } 20 | 21 | s.On("SendWRP", context.TODO(), r.WRPMessage, r.AuthHeaderValue).Return(nil, nil) 22 | 23 | e := makeTranslationEndpoint(s) 24 | e(context.TODO(), r) 25 | s.AssertExpectations(t) 26 | } 27 | -------------------------------------------------------------------------------- /translation/errors.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package translation 5 | 6 | import ( 7 | "errors" 8 | 9 | "github.com/xmidt-org/tr1d1um/transaction" 10 | ) 11 | 12 | // Error values definitions for the translation service 13 | var ( 14 | ErrEmptyNames = transaction.NewBadRequestError(errors.New("names parameter is required")) 15 | ErrInvalidService = transaction.NewBadRequestError(errors.New("unsupported Service")) 16 | ErrUnsupportedMethod = transaction.NewBadRequestError(errors.New("unsupported method. Could not decode request payload")) 17 | 18 | //Set command errors 19 | ErrInvalidSetWDMP = transaction.NewBadRequestError(errors.New("invalid SET message")) 20 | ErrNewCIDRequired = transaction.NewBadRequestError(errors.New("newCid is required for TEST_AND_SET")) 21 | 22 | //Add/Delete command errors 23 | ErrMissingTable = transaction.NewBadRequestError(errors.New("table property is required")) 24 | ErrMissingRow = transaction.NewBadRequestError(errors.New("row property is required")) 25 | ErrInvalidRow = transaction.NewBadRequestError(errors.New("row property is invalid")) 26 | ErrInvalidPayload = transaction.NewBadRequestError(errors.New("payload is invalid")) 27 | 28 | //Replace command error 29 | ErrMissingRows = transaction.NewBadRequestError(errors.New("rows property is required")) 30 | ErrInvalidRows = transaction.NewBadRequestError(errors.New("rows property is invalid")) 31 | ) 32 | -------------------------------------------------------------------------------- /translation/mocks_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package translation 5 | 6 | import ( 7 | context "context" 8 | "net/http" 9 | 10 | transaction "github.com/xmidt-org/tr1d1um/transaction" 11 | 12 | mock "github.com/stretchr/testify/mock" 13 | 14 | wrp "github.com/xmidt-org/wrp-go/v3" 15 | ) 16 | 17 | // MockService is an autogenerated mock type for the Service type 18 | type MockService struct { 19 | mock.Mock 20 | } 21 | 22 | // SendWRP provides a mock function with given fields: _a0, _a1, _a2 23 | func (_m *MockService) SendWRP(_a0 context.Context, _a1 *wrp.Message, _a2 string) (*transaction.XmidtResponse, error) { 24 | ret := _m.Called(_a0, _a1, _a2) 25 | 26 | var r0 *transaction.XmidtResponse 27 | if rf, ok := ret.Get(0).(func(context.Context, *wrp.Message, string) *transaction.XmidtResponse); ok { 28 | r0 = rf(_a0, _a1, _a2) 29 | } else { 30 | if ret.Get(0) != nil { 31 | r0 = ret.Get(0).(*transaction.XmidtResponse) 32 | } 33 | } 34 | 35 | var r1 error 36 | if rf, ok := ret.Get(1).(func(context.Context, *wrp.Message, string) error); ok { 37 | r1 = rf(_a0, _a1, _a2) 38 | } else { 39 | r1 = ret.Error(1) 40 | } 41 | 42 | return r0, r1 43 | } 44 | 45 | // MockTr1d1umTransactor is an autogenerated mock type for the Tr1d1umTransactor type 46 | type MockTr1d1umTransactor struct { 47 | mock.Mock 48 | } 49 | 50 | // Transact provides a mock function with given fields: _a0 51 | func (_m *MockTr1d1umTransactor) Transact(_a0 *http.Request) (*transaction.XmidtResponse, error) { 52 | ret := _m.Called(_a0) 53 | 54 | var r0 *transaction.XmidtResponse 55 | if rf, ok := ret.Get(0).(func(*http.Request) *transaction.XmidtResponse); ok { 56 | r0 = rf(_a0) 57 | } else { 58 | if ret.Get(0) != nil { 59 | r0 = ret.Get(0).(*transaction.XmidtResponse) 60 | } 61 | } 62 | 63 | var r1 error 64 | if rf, ok := ret.Get(1).(func(*http.Request) error); ok { 65 | r1 = rf(_a0) 66 | } else { 67 | r1 = ret.Error(1) 68 | } 69 | 70 | return r0, r1 71 | } 72 | 73 | type mockAcquirer struct { 74 | mock.Mock 75 | } 76 | 77 | func (m *mockAcquirer) Acquire() (string, error) { 78 | args := m.Called() 79 | return args.String(0), args.Error(1) 80 | } 81 | -------------------------------------------------------------------------------- /translation/service.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package translation 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | 10 | "net/http" 11 | 12 | "github.com/xmidt-org/bascule/acquire" 13 | "github.com/xmidt-org/tr1d1um/transaction" 14 | 15 | "github.com/xmidt-org/wrp-go/v3" 16 | ) 17 | 18 | // Service represents the Webpa-Tr1d1um component that translates WDMP data into WRP 19 | // which is compatible with the XMiDT API. 20 | type Service interface { 21 | SendWRP(context.Context, *wrp.Message, string) (*transaction.XmidtResponse, error) 22 | } 23 | 24 | // ServiceOptions defines the options needed to build a new translation WRP service. 25 | type ServiceOptions struct { 26 | //XmidtWrpURL is the URL of the XMiDT API which takes in WRP messages. 27 | XmidtWrpURL string 28 | 29 | //WRPSource is the value set on the WRPSource field of all WRP messages created by Tr1d1um. 30 | WRPSource string 31 | 32 | //Acquirer provides a mechanism to build auth headers for outbound requests. 33 | AuthAcquirer acquire.Acquirer 34 | 35 | //T is the component that's responsible to make the HTTP 36 | //request to the XMiDT API and return only data we care about. 37 | transaction.T 38 | } 39 | 40 | // NewService constructs a new translation service instance given some options. 41 | func NewService(o *ServiceOptions) Service { 42 | return &service{ 43 | xmidtWrpURL: o.XmidtWrpURL, 44 | wrpSource: o.WRPSource, 45 | transactor: o.T, 46 | authAcquirer: o.AuthAcquirer, 47 | } 48 | } 49 | 50 | type service struct { 51 | transactor transaction.T 52 | authAcquirer acquire.Acquirer 53 | xmidtWrpURL string 54 | wrpSource string 55 | } 56 | 57 | // SendWRP sends the given wrpMsg to the XMiDT cluster and returns the response if any. 58 | func (w *service) SendWRP(ctx context.Context, wrpMsg *wrp.Message, authHeaderValue string) (*transaction.XmidtResponse, error) { 59 | wrpMsg.Source = w.wrpSource 60 | 61 | var payload []byte 62 | 63 | err := wrp.NewEncoderBytes(&payload, wrp.Msgpack).Encode(wrpMsg) 64 | 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | r, err := http.NewRequestWithContext(ctx, http.MethodPost, w.xmidtWrpURL, bytes.NewBuffer(payload)) 70 | 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | if w.authAcquirer != nil { 76 | authHeaderValue, err = w.authAcquirer.Acquire() 77 | if err != nil { 78 | return nil, err 79 | } 80 | } 81 | 82 | r.Header.Set("Content-Type", wrp.Msgpack.ContentType()) 83 | r.Header.Set("Authorization", authHeaderValue) 84 | return w.transactor.Transact(r) 85 | } 86 | -------------------------------------------------------------------------------- /translation/service_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package translation 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "errors" 10 | "io/ioutil" 11 | "net/http" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/mock" 16 | "github.com/stretchr/testify/require" 17 | "github.com/xmidt-org/wrp-go/v3" 18 | ) 19 | 20 | func TestSendWRP(t *testing.T) { 21 | testCases := []struct { 22 | Name string 23 | ExpectedRequestAuth string 24 | EnableAcquirer bool 25 | AcquirerReturnString string 26 | AcquirerReturnError error 27 | }{ 28 | { 29 | Name: "No auth acquirer", 30 | ExpectedRequestAuth: "pass-through-token", 31 | }, 32 | 33 | { 34 | Name: "Auth acquirer enabled - success", 35 | EnableAcquirer: true, 36 | ExpectedRequestAuth: "acquired-token", 37 | AcquirerReturnString: "acquired-token", 38 | }, 39 | 40 | { 41 | Name: "Auth acquirer enabled error", 42 | EnableAcquirer: true, 43 | AcquirerReturnError: errors.New("error retrieving token"), 44 | }, 45 | } 46 | 47 | for _, testCase := range testCases { 48 | t.Run(testCase.Name, func(t *testing.T) { 49 | assert := assert.New(t) 50 | require := require.New(t) 51 | 52 | m := new(MockTr1d1umTransactor) 53 | var a *mockAcquirer 54 | 55 | options := &ServiceOptions{ 56 | XmidtWrpURL: "http://localhost/wrp", 57 | WRPSource: "dns:tr1d1um-xyz-example.com", 58 | T: m, 59 | } 60 | 61 | if testCase.EnableAcquirer { 62 | a = new(mockAcquirer) 63 | options.AuthAcquirer = a 64 | 65 | err := testCase.AcquirerReturnError 66 | a.On("Acquire").Return(testCase.AcquirerReturnString, err) 67 | } 68 | 69 | s := NewService(options) 70 | 71 | var expected = wrp.MustEncode(wrp.Message{ 72 | Type: wrp.SimpleRequestResponseMessageType, 73 | Source: "dns:tr1d1um-xyz-example.com", 74 | }, wrp.Msgpack) 75 | 76 | var requestMatcher = func(r *http.Request) bool { 77 | assert.EqualValues("http://localhost/wrp", r.URL.String()) 78 | assert.EqualValues(testCase.ExpectedRequestAuth, r.Header.Get("Authorization")) 79 | assert.EqualValues(wrp.Msgpack.ContentType(), r.Header.Get("Content-Type")) 80 | 81 | data, err := ioutil.ReadAll(r.Body) 82 | require.Nil(err) 83 | r.Body = ioutil.NopCloser(bytes.NewBuffer(data)) 84 | 85 | assert.EqualValues(string(expected), string(data)) 86 | 87 | //MatchedBy is not friendly in explicitly showing what's not matching 88 | //so we use assertions instead in this function 89 | return true 90 | } 91 | 92 | if testCase.AcquirerReturnError != nil { 93 | m.AssertNotCalled(t, "Transact", mock.Anything) 94 | } else { 95 | m.On("Transact", mock.MatchedBy(requestMatcher)).Return(nil, nil) 96 | } 97 | 98 | _, e := s.SendWRP(context.TODO(), &wrp.Message{ 99 | Type: wrp.SimpleRequestResponseMessageType, 100 | }, "pass-through-token") 101 | 102 | m.AssertExpectations(t) 103 | 104 | if testCase.EnableAcquirer { 105 | a.AssertExpectations(t) 106 | assert.Equal(testCase.AcquirerReturnError, e) 107 | } else { 108 | assert.Nil(e) 109 | } 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /translation/transport.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package translation 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "net/http" 14 | "strings" 15 | 16 | kithttp "github.com/go-kit/kit/transport/http" 17 | "github.com/gorilla/mux" 18 | "github.com/justinas/alice" 19 | "github.com/spf13/cast" 20 | "go.uber.org/zap" 21 | 22 | "github.com/xmidt-org/bascule" 23 | "github.com/xmidt-org/bascule/basculechecks" 24 | "github.com/xmidt-org/candlelight" 25 | "github.com/xmidt-org/sallust" 26 | "github.com/xmidt-org/tr1d1um/transaction" 27 | "github.com/xmidt-org/wrp-go/v3" 28 | "github.com/xmidt-org/wrp-go/v3/wrphttp" 29 | ) 30 | 31 | const ( 32 | contentTypeHeaderKey = "Content-Type" 33 | authHeaderKey = "Authorization" 34 | ) 35 | 36 | // Options wraps the properties needed to set up the translation server 37 | type Options struct { 38 | S Service 39 | 40 | //APIRouter is assumed to be a subrouter with the API prefix path (i.e. 'api/v2') 41 | APIRouter *mux.Router 42 | 43 | Authenticate *alice.Chain 44 | Log *zap.Logger 45 | ValidServices []string 46 | ReducedLoggingResponseCodes []int 47 | } 48 | 49 | // ConfigHandler sets up the server that powers the translation service 50 | func ConfigHandler(c *Options) { 51 | opts := []kithttp.ServerOption{ 52 | kithttp.ServerBefore(captureWDMPParameters), 53 | kithttp.ServerErrorEncoder(transaction.ErrorLogEncoder(sallust.Get, encodeError)), 54 | kithttp.ServerFinalizer(transaction.Log(c.ReducedLoggingResponseCodes)), 55 | } 56 | 57 | WRPHandler := kithttp.NewServer( 58 | makeTranslationEndpoint(c.S), 59 | decodeValidServiceRequest(c.ValidServices, decodeRequest), 60 | encodeResponse, 61 | opts..., 62 | ) 63 | 64 | c.APIRouter.Handle("/device/{deviceid}/{service}", c.Authenticate.Then(candlelight.EchoFirstTraceNodeInfo(candlelight.Tracing{}.Propagator(), false)(transaction.Welcome(WRPHandler)))). 65 | Methods(http.MethodGet, http.MethodPatch) 66 | 67 | c.APIRouter.Handle("/device/{deviceid}/{service}/{parameter}", c.Authenticate.Then(candlelight.EchoFirstTraceNodeInfo(candlelight.Tracing{}.Propagator(), false)(transaction.Welcome(WRPHandler)))). 68 | Methods(http.MethodDelete, http.MethodPut, http.MethodPost) 69 | } 70 | 71 | // getPartnerIDs returns the array that represents the partner-ids that were 72 | // passed in as headers. This function handles multiple duplicate headers. 73 | func getPartnerIDs(h http.Header) []string { 74 | headers, ok := h[wrphttp.PartnerIdHeader] 75 | if !ok { 76 | return nil 77 | } 78 | 79 | var partners []string 80 | 81 | for _, value := range headers { 82 | fields := strings.Split(value, ",") 83 | for i := 0; i < len(fields); i++ { 84 | fields[i] = strings.TrimSpace(fields[i]) 85 | } 86 | partners = append(partners, fields...) 87 | } 88 | return partners 89 | } 90 | 91 | // getPartnerIDsDecodeRequest returns array of partnerIDs needed for decodeRequest 92 | func getPartnerIDsDecodeRequest(ctx context.Context, r *http.Request) []string { 93 | auth, ok := bascule.FromContext(ctx) 94 | //if no token 95 | if !ok { 96 | return getPartnerIDs(r.Header) 97 | } 98 | tokenType := auth.Token.Type() 99 | //if not jwt type 100 | if tokenType != "jwt" { 101 | return getPartnerIDs(r.Header) 102 | } 103 | partnerVal, ok := bascule.GetNestedAttribute(auth.Token.Attributes(), basculechecks.PartnerKeys()...) 104 | //if no partner ids 105 | if !ok { 106 | return getPartnerIDs(r.Header) 107 | } 108 | partnerIDs, err := cast.ToStringSliceE(partnerVal) 109 | 110 | if err != nil { 111 | return getPartnerIDs(r.Header) 112 | } 113 | return partnerIDs 114 | } 115 | 116 | func getTID(ctx context.Context) string { 117 | t, ok := ctx.Value(transaction.ContextKeyRequestTID).(string) 118 | if !ok { 119 | sallust.Get(ctx).Warn(fmt.Sprintf("tid not found in header `%s` or generated", candlelight.HeaderWPATIDKeyName)) 120 | return "" 121 | } 122 | 123 | return t 124 | } 125 | 126 | /* Request Decoding */ 127 | func decodeRequest(ctx context.Context, r *http.Request) (decodedRequest interface{}, err error) { 128 | var ( 129 | payload []byte 130 | wrpMsg *wrp.Message 131 | tid string 132 | partnerIDs []string 133 | ) 134 | 135 | if payload, err = requestPayload(r); err == nil { 136 | tid = getTID(ctx) 137 | partnerIDs = getPartnerIDsDecodeRequest(ctx, r) 138 | } 139 | 140 | if err == nil { 141 | var traceHeaders []string 142 | 143 | // If there's a traceparent, add it to traceHeaders array 144 | // Also, add tracestate to the traceHeaders array (can be empty) 145 | // A tracestate will not exist without a traceparent 146 | tp := r.Header.Get("traceparent") 147 | if tp != "" { 148 | tp = "traceparent: " + tp 149 | ts := r.Header.Get("tracestate") 150 | ts = "tracestate: " + ts 151 | traceHeaders = append(traceHeaders, tp, ts) 152 | } 153 | 154 | wrpMsg, err = wrap(payload, tid, mux.Vars(r), partnerIDs, traceHeaders) 155 | 156 | if err == nil { 157 | decodedRequest = &wrpRequest{ 158 | WRPMessage: wrpMsg, 159 | AuthHeaderValue: r.Header.Get(authHeaderKey), 160 | } 161 | } 162 | } 163 | 164 | return 165 | } 166 | 167 | func requestPayload(r *http.Request) (payload []byte, err error) { 168 | 169 | switch r.Method { 170 | case http.MethodGet: 171 | payload, err = requestGetPayload(r.FormValue("names"), r.FormValue("attributes")) 172 | case http.MethodPatch: 173 | payload, err = requestSetPayload(r.Body, r.Header.Get(HeaderWPASyncNewCID), r.Header.Get(HeaderWPASyncOldCID), r.Header.Get(HeaderWPASyncCMC)) 174 | case http.MethodDelete: 175 | payload, err = requestDeletePayload(mux.Vars(r)) 176 | case http.MethodPut: 177 | payload, err = requestReplacePayload(mux.Vars(r), r.Body) 178 | case http.MethodPost: 179 | payload, err = requestAddPayload(mux.Vars(r), r.Body) 180 | default: 181 | //Unwanted methods should be filtered at the mux level. Thus, we "should" never get here 182 | err = ErrUnsupportedMethod 183 | } 184 | 185 | return 186 | } 187 | 188 | /* Response Encoding */ 189 | func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) (err error) { 190 | var resp = response.(*transaction.XmidtResponse) 191 | 192 | //equivalent to forwarding all headers 193 | transaction.ForwardHeadersByPrefix("", resp.ForwardedHeaders, w.Header()) 194 | 195 | // Write TransactionID for all requests 196 | tid := getTID(ctx) 197 | w.Header().Set(candlelight.HeaderWPATIDKeyName, tid) 198 | // just forward the XMiDT cluster response 199 | if len(resp.Body) == 0 && resp.Code == http.StatusOK { 200 | sallust.Get(ctx).Warn("sending 200 with an empty body") 201 | w.WriteHeader(resp.Code) 202 | return 203 | } else if resp.Code != http.StatusOK { 204 | w.WriteHeader(resp.Code) 205 | _, err = w.Write(resp.Body) 206 | return 207 | } 208 | 209 | wrpModel := new(wrp.Message) 210 | 211 | if err = wrp.NewDecoderBytes(resp.Body, wrp.Msgpack).Decode(wrpModel); err == nil { 212 | 213 | // device response model 214 | var d struct { 215 | StatusCode int `json:"statusCode"` 216 | } 217 | 218 | w.Header().Set("Content-Type", "application/json") 219 | // use the device response status code if it's within 520-599 (inclusive) or 403 220 | // https://github.com/xmidt-org/tr1d1um/issues/354 221 | // https://github.com/xmidt-org/tr1d1um/issues/397 222 | if errUnmarshall := json.Unmarshal(wrpModel.Payload, &d); errUnmarshall == nil { 223 | if http.StatusForbidden == d.StatusCode || (520 <= d.StatusCode && d.StatusCode <= 599) { 224 | w.WriteHeader(d.StatusCode) 225 | } 226 | } 227 | 228 | _, err = w.Write(wrpModel.Payload) 229 | } 230 | 231 | return 232 | } 233 | 234 | /* Error Encoding */ 235 | 236 | func encodeError(ctx context.Context, err error, w http.ResponseWriter) { 237 | tid := getTID(ctx) 238 | w.Header().Set(contentTypeHeaderKey, "application/json") 239 | w.Header().Set(candlelight.HeaderWPATIDKeyName, tid) 240 | var ce transaction.CodedError 241 | if errors.As(err, &ce) { 242 | w.WriteHeader(ce.StatusCode()) 243 | } else { 244 | w.WriteHeader(http.StatusInternalServerError) 245 | 246 | //the real error is logged into our system before encodeError() is called 247 | //the idea behind masking it is to not send the external API consumer internal error messages 248 | err = transaction.ErrTr1d1umInternal 249 | } 250 | 251 | json.NewEncoder(w).Encode(map[string]interface{}{ 252 | "message": err.Error(), 253 | }) 254 | 255 | } 256 | 257 | /* Request-type specific decoding functions */ 258 | 259 | func requestSetPayload(in io.Reader, newCID, oldCID, syncCMC string) (p []byte, err error) { 260 | var ( 261 | wdmp = new(setWDMP) 262 | data []byte 263 | ) 264 | 265 | if data, err = ioutil.ReadAll(in); err == nil { 266 | if wdmp, err = loadWDMP(data, newCID, oldCID, syncCMC); err == nil { 267 | return json.Marshal(wdmp) 268 | } 269 | } 270 | 271 | return 272 | } 273 | 274 | func requestGetPayload(names, attributes string) ([]byte, error) { 275 | if len(names) < 1 { 276 | return nil, ErrEmptyNames 277 | } 278 | 279 | wdmp := new(getWDMP) 280 | 281 | //default values at this point 282 | wdmp.Names, wdmp.Command = strings.Split(names, ","), CommandGet 283 | 284 | if attributes != "" { 285 | wdmp.Command, wdmp.Attributes = CommandGetAttrs, attributes 286 | } 287 | 288 | return json.Marshal(wdmp) 289 | } 290 | 291 | func requestAddPayload(m map[string]string, input io.Reader) (p []byte, err error) { 292 | var wdmp = &addRowWDMP{Command: CommandAddRow} 293 | 294 | table := m["parameter"] 295 | 296 | if len(table) < 1 { 297 | return nil, ErrMissingTable 298 | } 299 | 300 | wdmp.Table = table 301 | 302 | payload, err := ioutil.ReadAll(input) 303 | 304 | if err != nil { 305 | return nil, ErrInvalidPayload 306 | } 307 | 308 | if len(payload) < 1 { 309 | return nil, ErrMissingRow 310 | } 311 | 312 | err = json.Unmarshal(payload, &wdmp.Row) 313 | if err != nil { 314 | return nil, ErrInvalidRow 315 | } 316 | return json.Marshal(wdmp) 317 | } 318 | 319 | func requestReplacePayload(m map[string]string, input io.Reader) ([]byte, error) { 320 | var wdmp = &replaceRowsWDMP{Command: CommandReplaceRows} 321 | 322 | table := strings.Trim(m["parameter"], " ") 323 | if len(table) == 0 { 324 | return nil, ErrMissingTable 325 | } 326 | 327 | wdmp.Table = table 328 | 329 | payload, err := ioutil.ReadAll(input) 330 | 331 | if err != nil { 332 | return nil, err 333 | } 334 | 335 | if len(payload) < 1 { 336 | return nil, ErrMissingRows 337 | } 338 | 339 | err = json.Unmarshal(payload, &wdmp.Rows) 340 | if err != nil { 341 | return nil, ErrInvalidRows 342 | } 343 | 344 | return json.Marshal(wdmp) 345 | } 346 | 347 | func requestDeletePayload(m map[string]string) ([]byte, error) { 348 | row := m["parameter"] 349 | if len(row) < 1 { 350 | return nil, ErrMissingRow 351 | } 352 | return json.Marshal(&deleteRowDMP{Command: CommandDeleteRow, Row: row}) 353 | } 354 | -------------------------------------------------------------------------------- /translation/transport_utils.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package translation 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | 14 | "github.com/xmidt-org/sallust" 15 | "github.com/xmidt-org/tr1d1um/transaction" 16 | "go.uber.org/zap" 17 | 18 | kithttp "github.com/go-kit/kit/transport/http" 19 | "github.com/gorilla/mux" 20 | "github.com/xmidt-org/wrp-go/v3" 21 | ) 22 | 23 | /* Functions that help decode a given SET request to TR1D1UM */ 24 | 25 | // deduceSET deduces the command for a given wdmp object 26 | func deduceSET(wdmp *setWDMP, newCID, oldCID, syncCMC string) (err error) { 27 | if newCID == "" && oldCID != "" { 28 | return ErrNewCIDRequired 29 | } else if newCID == "" && oldCID == "" && syncCMC == "" { 30 | wdmp.Command = getCommandForParams(wdmp.Parameters) 31 | } else { 32 | wdmp.Command = CommandTestSet 33 | wdmp.NewCid, wdmp.OldCid, wdmp.SyncCmc = newCID, oldCID, syncCMC 34 | } 35 | 36 | return 37 | } 38 | 39 | // isValidSetWDMP helps verify a given Set WDMP object is valid for its context 40 | func isValidSetWDMP(wdmp *setWDMP) (isValid bool) { 41 | if len(wdmp.Parameters) == 0 { 42 | return wdmp.Command == CommandTestSet //TEST_AND_SET can have empty parameters 43 | } 44 | 45 | var cmdSetAttr, cmdSet = 0, 0 46 | 47 | //validate parameters if it exists, even for TEST_SET 48 | for _, param := range wdmp.Parameters { 49 | if param.Name == nil || *param.Name == "" { 50 | return 51 | } 52 | 53 | if param.Value != nil && (param.DataType == nil || *param.DataType < 0) { 54 | return 55 | } 56 | 57 | if wdmp.Command == CommandSetAttrs && param.Attributes == nil { 58 | return 59 | } 60 | 61 | if param.Attributes != nil && 62 | param.DataType == nil && 63 | param.Value == nil { 64 | 65 | cmdSetAttr++ 66 | } else { 67 | cmdSet++ 68 | } 69 | 70 | // verify that all parameters are correct for either doing a command SET_ATTRIBUTE or SET 71 | if cmdSetAttr > 0 && cmdSet > 0 { 72 | return 73 | } 74 | } 75 | return true 76 | } 77 | 78 | // getCommandForParams decides whether the command for some request is a 'SET' or 'SET_ATTRS' based on a given list of parameters 79 | func getCommandForParams(params []setParam) (command string) { 80 | command = CommandSet 81 | if len(params) < 1 { 82 | return 83 | } 84 | if wdmp := params[0]; wdmp.Attributes != nil && 85 | wdmp.Name != nil && 86 | wdmp.DataType == nil && 87 | wdmp.Value == nil { 88 | command = CommandSetAttrs 89 | } 90 | return 91 | } 92 | 93 | /* Other transport-level helper functions */ 94 | 95 | // wrp merges different values from a WDMP request into a WRP message 96 | func wrap(WDMP []byte, tid string, pathVars map[string]string, partnerIDs []string, traceHeaders []string) (*wrp.Message, error) { 97 | canonicalDeviceID, err := wrp.ParseDeviceID(pathVars["deviceid"]) 98 | if err != nil { 99 | return nil, transaction.NewBadRequestError(err) 100 | } 101 | return &wrp.Message{ 102 | Type: wrp.SimpleRequestResponseMessageType, 103 | Payload: WDMP, 104 | Destination: fmt.Sprintf("%s/%s", string(canonicalDeviceID), pathVars["service"]), 105 | TransactionUUID: tid, 106 | PartnerIDs: partnerIDs, 107 | Headers: traceHeaders, 108 | }, nil 109 | } 110 | 111 | func decodeValidServiceRequest(services []string, decoder kithttp.DecodeRequestFunc) kithttp.DecodeRequestFunc { 112 | return func(c context.Context, r *http.Request) (interface{}, error) { 113 | 114 | if !contains(mux.Vars(r)["service"], services) { 115 | return nil, ErrInvalidService 116 | } 117 | 118 | return decoder(c, r) 119 | } 120 | } 121 | 122 | func loadWDMP(encodedWDMP []byte, newCID, oldCID, syncCMC string) (*setWDMP, error) { 123 | wdmp := new(setWDMP) 124 | 125 | err := json.Unmarshal(encodedWDMP, wdmp) 126 | 127 | if err != nil && len(encodedWDMP) > 0 { //len(encodedWDMP) == 0 is ok as it is used for TEST_SET 128 | return nil, transaction.NewBadRequestError(fmt.Errorf("invalid WDMP structure: %s", err)) 129 | } 130 | 131 | err = deduceSET(wdmp, newCID, oldCID, syncCMC) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | if !isValidSetWDMP(wdmp) { 137 | return nil, ErrInvalidSetWDMP 138 | } 139 | 140 | return wdmp, nil 141 | } 142 | 143 | func captureWDMPParameters(ctx context.Context, r *http.Request) (nctx context.Context) { 144 | nctx = ctx 145 | logger := sallust.Get(ctx) 146 | 147 | if r.Method == http.MethodPatch { 148 | bodyBytes, _ := ioutil.ReadAll(r.Body) 149 | r.Body.Close() 150 | 151 | r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 152 | wdmp, e := loadWDMP(bodyBytes, r.Header.Get(HeaderWPASyncNewCID), r.Header.Get(HeaderWPASyncOldCID), r.Header.Get(HeaderWPASyncCMC)) 153 | if e == nil { 154 | 155 | logger = logger.With( 156 | zap.Any("command", wdmp.Command), 157 | zap.Any("parameters", getParamNames(wdmp.Parameters)), 158 | ) 159 | 160 | nctx = sallust.With(ctx, logger) 161 | } 162 | } 163 | 164 | return 165 | } 166 | 167 | func getParamNames(params []setParam) (paramNames []string) { 168 | paramNames = make([]string, len(params)) 169 | 170 | for i, param := range params { 171 | paramNames[i] = *param.Name 172 | } 173 | 174 | return 175 | } 176 | 177 | func contains(i string, elements []string) bool { 178 | for _, e := range elements { 179 | if e == i { 180 | return true 181 | } 182 | } 183 | return false 184 | } 185 | -------------------------------------------------------------------------------- /translation/transport_utils_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package translation 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/gorilla/mux" 13 | 14 | "github.com/stretchr/testify/assert" 15 | transaction "github.com/xmidt-org/tr1d1um/transaction" 16 | "github.com/xmidt-org/wrp-go/v3" 17 | ) 18 | 19 | func TestValidateAndDeduceSETCommand(t *testing.T) { 20 | 21 | t.Run("newCIDMissing", func(t *testing.T) { 22 | assert := assert.New(t) 23 | wdmp := new(setWDMP) 24 | err := deduceSET(wdmp, "", "old-cid", "sync-cm") 25 | assert.EqualValues(ErrNewCIDRequired, err) 26 | }) 27 | 28 | t.Run("", func(t *testing.T) { 29 | assert := assert.New(t) 30 | wdmp := new(setWDMP) 31 | err := deduceSET(wdmp, "", "", "") 32 | assert.Nil(err) 33 | assert.EqualValues(CommandSet, wdmp.Command) 34 | 35 | }) 36 | 37 | t.Run("TestSetNilValues", func(t *testing.T) { 38 | assert := assert.New(t) 39 | wdmp := new(setWDMP) 40 | 41 | err := deduceSET(wdmp, "newVal", "oldVal", "") 42 | assert.Nil(err) 43 | assert.EqualValues(CommandTestSet, wdmp.Command) 44 | }) 45 | } 46 | 47 | func TestIsValidSetWDMP(t *testing.T) { 48 | t.Run("TestAndSetZeroParams", func(t *testing.T) { 49 | assert := assert.New(t) 50 | 51 | wdmp := &setWDMP{Command: CommandTestSet} //nil parameters 52 | assert.True(isValidSetWDMP(wdmp)) 53 | 54 | wdmp = &setWDMP{Command: CommandTestSet, Parameters: []setParam{}} //empty parameters 55 | assert.True(isValidSetWDMP(wdmp)) 56 | }) 57 | 58 | t.Run("NilNameInParam", func(t *testing.T) { 59 | assert := assert.New(t) 60 | 61 | dataType := int8(0) 62 | nilNameParam := setParam{ 63 | Value: "val", 64 | DataType: &dataType, 65 | // Name is left undefined 66 | } 67 | params := []setParam{nilNameParam} 68 | wdmp := &setWDMP{Command: CommandSet, Parameters: params} 69 | assert.False(isValidSetWDMP(wdmp)) 70 | }) 71 | 72 | t.Run("NilDataTypeNonNilValue", func(t *testing.T) { 73 | assert := assert.New(t) 74 | 75 | name := "nameVal" 76 | param := setParam{ 77 | Name: &name, 78 | Value: 3, 79 | //DataType is left undefined 80 | } 81 | params := []setParam{param} 82 | wdmp := &setWDMP{Command: CommandSet, Parameters: params} 83 | assert.False(isValidSetWDMP(wdmp)) 84 | }) 85 | 86 | t.Run("SetAttrsParamNilAttr", func(t *testing.T) { 87 | assert := assert.New(t) 88 | 89 | name := "nameVal" 90 | param := setParam{ 91 | Name: &name, 92 | } 93 | params := []setParam{param} 94 | wdmp := &setWDMP{Command: CommandSetAttrs, Parameters: params} 95 | assert.False(isValidSetWDMP(wdmp)) 96 | }) 97 | 98 | t.Run("MixedParams", func(t *testing.T) { 99 | assert := assert.New(t) 100 | 101 | name, dataType := "victorious", int8(1) 102 | setAttrParam := setParam{ 103 | Name: &name, 104 | Attributes: map[string]interface{}{"three": 3}, 105 | } 106 | 107 | sp := setParam{ 108 | Name: &name, 109 | Attributes: map[string]interface{}{"two": 2}, 110 | Value: 3, 111 | DataType: &dataType, 112 | } 113 | mixParams := []setParam{setAttrParam, sp} 114 | wdmp := &setWDMP{Command: CommandSetAttrs, Parameters: mixParams} 115 | assert.False(isValidSetWDMP(wdmp)) 116 | }) 117 | 118 | t.Run("IdealSet", func(t *testing.T) { 119 | assert := assert.New(t) 120 | 121 | name := "victorious" 122 | setAttrParam := setParam{ 123 | Name: &name, 124 | Attributes: map[string]interface{}{"three": 3}, 125 | } 126 | params := []setParam{setAttrParam} 127 | wdmp := &setWDMP{Command: CommandSetAttrs, Parameters: params} 128 | assert.True(isValidSetWDMP(wdmp)) 129 | }) 130 | } 131 | 132 | func TestGetCommandForParam(t *testing.T) { 133 | t.Run("EmptyParams", func(t *testing.T) { 134 | assert := assert.New(t) 135 | assert.EqualValues(CommandSet, getCommandForParams(nil)) 136 | assert.EqualValues(CommandSet, getCommandForParams([]setParam{})) 137 | }) 138 | 139 | //Attributes and Name are required properties for SET_ATTRS 140 | t.Run("SetCommandUndefinedAttributes", func(t *testing.T) { 141 | assert := assert.New(t) 142 | name := "setParam" 143 | setCommandParam := setParam{Name: &name} 144 | assert.EqualValues(CommandSet, getCommandForParams([]setParam{setCommandParam})) 145 | }) 146 | 147 | //DataType and Value must be null for SET_ATTRS 148 | t.Run("SetAttrsCommand", func(t *testing.T) { 149 | assert := assert.New(t) 150 | name := "setAttrsParam" 151 | setCommandParam := setParam{ 152 | Name: &name, 153 | Attributes: map[string]interface{}{"zero": 0}, 154 | } 155 | assert.EqualValues(CommandSetAttrs, getCommandForParams([]setParam{setCommandParam})) 156 | }) 157 | } 158 | func TestWrapInWRP(t *testing.T) { 159 | t.Run("EmptyVars", func(t *testing.T) { 160 | assert := assert.New(t) 161 | 162 | w, e := wrap([]byte(""), "", nil, nil, nil) 163 | 164 | assert.Nil(w) 165 | assert.EqualValues(transaction.NewBadRequestError(wrp.ErrorInvalidDeviceName), e) 166 | }) 167 | 168 | t.Run("GivenParameters", func(t *testing.T) { 169 | assert := assert.New(t) 170 | 171 | w, e := wrap([]byte{'t'}, "t0", map[string]string{"deviceid": "mac:112233445566", "service": "s0"}, nil, nil) 172 | 173 | assert.Nil(e) 174 | assert.EqualValues(wrp.SimpleRequestResponseMessageType, w.Type) 175 | assert.EqualValues([]byte{'t'}, w.Payload) 176 | assert.EqualValues("mac:112233445566/s0", w.Destination) 177 | assert.EqualValues("t0", w.TransactionUUID) 178 | }) 179 | } 180 | 181 | func TestDecodeValidServiceRequest(t *testing.T) { 182 | f := decodeValidServiceRequest([]string{"s0"}, func(_ context.Context, _ *http.Request) (interface{}, error) { 183 | return nil, nil 184 | }) 185 | 186 | t.Run("InvalidService", func(t *testing.T) { 187 | assert := assert.New(t) 188 | r := httptest.NewRequest(http.MethodGet, "localhost:8090/api", nil) 189 | i, err := f(context.TODO(), r) 190 | assert.Nil(i) 191 | assert.EqualValues(ErrInvalidService, err) 192 | }) 193 | 194 | t.Run("ValidService", func(t *testing.T) { 195 | assert := assert.New(t) 196 | r := httptest.NewRequest(http.MethodGet, "localhost:8090/api", nil) 197 | r = mux.SetURLVars(r, map[string]string{"service": "s0"}) 198 | 199 | i, err := f(context.TODO(), r) 200 | assert.Nil(i) 201 | assert.Nil(err) 202 | }) 203 | } 204 | 205 | func TestContains(t *testing.T) { 206 | assert := assert.New(t) 207 | assert.False(contains("a", nil)) 208 | assert.False(contains("a", []string{})) 209 | assert.True(contains("a", []string{"a", "b"})) 210 | } 211 | 212 | func TestGetParamNames(t *testing.T) { 213 | j := "Josh" 214 | b := "Brian" 215 | tcs := []struct { 216 | desc string 217 | params []setParam 218 | expectedParamnames []string 219 | }{ 220 | { 221 | desc: "Empty Params", 222 | params: []setParam{}, 223 | expectedParamnames: []string{}, 224 | }, 225 | { 226 | desc: "Pull Params", 227 | params: []setParam{{Name: &j}, {Name: &b}}, 228 | expectedParamnames: []string{"Josh", "Brian"}, 229 | }, 230 | } 231 | 232 | for _, tc := range tcs { 233 | t.Run(tc.desc, func(t *testing.T) { 234 | assert := assert.New(t) 235 | r := getParamNames(tc.params) 236 | assert.Equal(r, tc.expectedParamnames) 237 | }) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /translation/wdmp_type.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package translation 5 | 6 | // All the supported commands, WebPA Headers and misc 7 | const ( 8 | CommandGet = "GET" 9 | CommandGetAttrs = "GET_ATTRIBUTES" 10 | CommandSet = "SET" 11 | CommandSetAttrs = "SET_ATTRIBUTES" 12 | CommandTestSet = "TEST_AND_SET" 13 | CommandAddRow = "ADD_ROW" 14 | CommandDeleteRow = "DELETE_ROW" 15 | CommandReplaceRows = "REPLACE_ROWS" 16 | 17 | HeaderWPASyncOldCID = "X-Webpa-Sync-Old-Cid" 18 | HeaderWPASyncNewCID = "X-Webpa-Sync-New-Cid" 19 | HeaderWPASyncCMC = "X-Webpa-Sync-Cmc" 20 | ) 21 | 22 | type getWDMP struct { 23 | Command string `json:"command"` 24 | Names []string `json:"names"` 25 | Attributes string `json:"attributes,omitempty"` 26 | } 27 | type setWDMP struct { 28 | Command string `json:"command"` 29 | OldCid string `json:"old-cid,omitempty"` 30 | NewCid string `json:"new-cid,omitempty"` 31 | SyncCmc string `json:"sync-cmc,omitempty"` 32 | Parameters []setParam `json:"parameters,omitempty"` 33 | } 34 | 35 | type setParam struct { 36 | Name *string `json:"name"` 37 | DataType *int8 `json:"dataType,omitempty"` 38 | Value interface{} `json:"value,omitempty"` 39 | Attributes map[string]interface{} `json:"attributes,omitempty"` 40 | } 41 | 42 | type addRowWDMP struct { 43 | Command string `json:"command"` 44 | Table string `json:"table"` 45 | Row map[string]string `json:"row"` 46 | } 47 | 48 | // indexRow facilitates data transfer from json data of the form {index1: {key:val}, index2: {key:val}, ... } 49 | type indexRow map[string]map[string]string 50 | 51 | // replaceRowsWDMP serves as container for data used for the REPLACE_ROWS command 52 | type replaceRowsWDMP struct { 53 | Command string `json:"command"` 54 | Table string `json:"table"` 55 | Rows indexRow `json:"rows"` 56 | } 57 | 58 | type deleteRowDMP struct { 59 | Command string `json:"command"` 60 | Row string `json:"row"` 61 | } 62 | --------------------------------------------------------------------------------