Compare commits

...

37 Commits

Author SHA1 Message Date
dependabot[bot]
a29cf158dd chore(deps): bump golang.org/x/oauth2 from 0.3.0 to 0.4.0 (#423)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/golang/oauth2/releases)
- [Commits](https://github.com/golang/oauth2/compare/v0.3.0...v0.4.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-09 20:30:30 -05:00
TwiN
9d14e3011b docs: Add link to banner 2023-02-08 20:50:30 -05:00
dependabot[bot]
d13998d13d chore(deps): bump modernc.org/sqlite from 1.19.5 to 1.20.3 (#409)
---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-05 22:39:11 -05:00
TwiN
f6a39f6df0 docs(alerting): Update pushover documentation 2023-02-05 21:58:41 -05:00
Marc Brugger
9e2006910d feat(alerting): Add Pushover sound support (#417) 2023-02-05 21:47:39 -05:00
tesibelda
6e4b88dc6e fix(test): flaky TestStore_InsertCleansUpOldUptimeEntriesProperly (#418) 2023-02-01 22:59:31 -05:00
dependabot[bot]
277e805dbb chore(deps): bump docker/build-push-action from 3 to 4 (#415)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-01 01:16:25 -05:00
TwiN
941c10ca45 chore: Remove vendor folder (#414)
* chore: Remove vendor folder

* chore: Add vendor folder to .gitignore
2023-01-30 22:29:30 -05:00
Kevin Woblick
21f62f362f feat(alerting): Add Pushover provider (#405)
* Add a new Pushover provider (#129)
- Adds new provider named Pushover with corresponding tests
- Adds Pushover as a provider to the configuration and adjusts test accordingly
- Adds Pushover to alerting_test.go, provider.go and type.go
- Updates the readme with configuration details

* Correct import order

* Fix some missing pushover references

* Apply suggestions from code review

* Rename application-key to application-token for Pushover

---------

Co-authored-by: TwiN <twin@linux.com>
2023-01-29 17:32:16 -05:00
TwiN
d75180c341 docs: Clarify what the storage is used for
Resolves #411
2023-01-29 16:47:25 -05:00
dependabot[bot]
a82b883276 chore(deps): bump github.com/coreos/go-oidc/v3 from 3.4.0 to 3.5.0 (#404)
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.4.0...v3.5.0)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-22 16:54:53 -05:00
TwiN
24e207c0c6 fix(metrics): Disable compression on prometheus/client_golang's handler
Fixes #406

Root cause seems to be that promhttp.Handler() has its own gzip compression https://github.com/prometheus/client_golang/issues/622
2023-01-19 23:06:38 -05:00
TwiN
90bb8f7b5f test: Don't run tests in parallel 2023-01-19 01:53:52 -05:00
TwiN
0db92f46da test: Add several tests for numerical conditions 2023-01-19 01:37:21 -05:00
dependabot[bot]
0ffa03f42d chore(deps): bump github.com/TwiN/deepmerge from 0.1.0 to 0.2.0 (#401)
Bumps [github.com/TwiN/deepmerge](https://github.com/TwiN/deepmerge) from 0.1.0 to 0.2.0.
- [Release notes](https://github.com/TwiN/deepmerge/releases)
- [Commits](https://github.com/TwiN/deepmerge/compare/v0.1.0...v0.2.0)

---
updated-dependencies:
- dependency-name: github.com/TwiN/deepmerge
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-11 01:05:17 -05:00
TwiN
e61a42220c fix: Log GATUS_CONFIG_FILE deprecation message only if it's non-empty 2023-01-10 19:04:19 -05:00
TwiN
78dccc90e1 fix(#400): Use proper configuration path when iterating over options 2023-01-10 00:24:56 -05:00
TwiN
6bdd3c94fe chore: Add deprecation message for GATUS_CONFIG_FILE 2023-01-09 23:24:20 -05:00
TwiN
4225d22369 docs: Add example of docker-compose with multiple configuration files 2023-01-08 18:34:13 -05:00
TwiN
3059e3e028 feat: Support multiple configuration files (#396)
* Revert "Revert "feat: Support multiple configuration files" (#395)"

This reverts commit 87740e74a6.

* feat: Properly implement support for config directory
2023-01-08 17:53:37 -05:00
TwiN
87740e74a6 Revert "feat: Support multiple configuration files" (#395)
Revert "feat: Support multiple configuration files (#389)"

This reverts commit 8e14302765.
2023-01-07 03:45:43 -05:00
Henning Janßen
8e14302765 feat: Support multiple configuration files (#389)
* Allow configuration to be distributed

* catch iteration errors when collecting config files

* rm unused func

* Fix suffix check for config loading

* test configuration loading

* GATUS_CONFIG_PATH can be a file or a directory now

* Add deprecation note

* Fix cs

Co-authored-by: TwiN <twin@linux.com>

* cs fixes

Co-authored-by: TwiN <twin@linux.com>

* cs fixes

Co-authored-by: TwiN <twin@linux.com>

* cs fixes

Co-authored-by: TwiN <twin@linux.com>

* cs + rm useless line

Co-authored-by: TwiN <twin@linux.com>

* Update config/config.go

Co-authored-by: TwiN <twin@linux.com>
2023-01-06 23:46:19 -05:00
TwiN
844f417ea1 docs: Update sponsor list 2023-01-06 20:06:56 -05:00
TwiN
2f7f782f11 docs: Improve documentation and add ghcr Docker image link 2023-01-06 20:04:21 -05:00
dependabot[bot]
37bea336ca chore(deps): bump github.com/TwiN/health from 1.5.0 to 1.6.0 (#394)
Bumps [github.com/TwiN/health](https://github.com/TwiN/health) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/TwiN/health/releases)
- [Commits](https://github.com/TwiN/health/compare/v1.5.0...v1.6.0)

---
updated-dependencies:
- dependency-name: github.com/TwiN/health
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-04 01:37:24 -05:00
TwiN
616a654b27 fix: Compress everything with Gzip 2022-12-30 21:37:52 -05:00
TwiN
a1c8422c2f ci: Make release also push latest tag 2022-12-23 10:00:17 -05:00
TwiN
947173bf71 fix: Prevent jsonpath from causing panic when body is expected to be array but isn't (#392)
* fix: Prevent jsonpath from causing panic when body is expected to be array but isn't

Fixes #391
2022-12-23 09:55:17 -05:00
TwiN
a81a83e2d4 docs: Update example 2022-12-22 17:38:27 -05:00
Salim B
4599fe4da7 Clarify description of len() function 2022-12-21 22:37:49 -05:00
TwiN
19e90cdf31 test(condition): Add has-key-of-map test case 2022-12-17 23:58:01 -05:00
TwiN
ecc0636a59 feat(alerting): Implement GitHub alerting provider 2022-12-15 23:38:44 -05:00
TwiN
27502acd10 chore: Improve oauth2 configuration error 2022-12-15 23:25:37 -05:00
TwiN
51255e33ea refactor(alerting): Use reflection to retrieve provider based on alert type 2022-12-15 21:37:34 -05:00
TwiN
be0962112e refactor: Remove unnecessary logs 2022-12-15 21:37:34 -05:00
TwiN
dfcea93080 fix(alerting): Use reflection to set invalid providers to nil instead of re-validating on every alert trigger/resolve 2022-12-15 21:37:34 -05:00
dependabot[bot]
a5f135c675 chore(deps): bump github.com/miekg/dns from 1.1.43 to 1.1.50 (#385)
Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.43 to 1.1.50.
- [Release notes](https://github.com/miekg/dns/releases)
- [Changelog](https://github.com/miekg/dns/blob/master/Makefile.release)
- [Commits](https://github.com/miekg/dns/compare/v1.1.43...v1.1.50)

---
updated-dependencies:
- dependency-name: github.com/miekg/dns
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-14 23:51:19 -05:00
2652 changed files with 1489 additions and 4057847 deletions

View File

@@ -0,0 +1,21 @@
endpoints:
- name: check-if-api-is-healthy
group: backend
url: "https://twin.sh/health"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
- "[RESPONSE_TIME] < 1000"
- name: check-if-website-is-pingable
url: "icmp://example.org"
interval: 1m
conditions:
- "[CONNECTED] == true"
- name: check-domain-expiration
url: "https://example.org"
interval: 6h
conditions:
- "[DOMAIN_EXPIRATION] > 720h"

View File

@@ -0,0 +1,8 @@
endpoints:
- name: make-sure-html-rendering-works
group: frontend
url: "https://example.org"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[BODY] == pat(*<h1>Example Domain</h1>*)" # Check for header in HTML page

View File

@@ -0,0 +1,8 @@
metrics: true
debug: false
ui:
header: Example Company
link: https://example.org
buttons:
- name: "Home"
link: "https://example.org"

View File

@@ -0,0 +1,10 @@
version: "3.8"
services:
gatus:
image: twinproduction/gatus:latest
ports:
- "8080:8080"
environment:
- GATUS_CONFIG_PATH=/config
volumes:
- ./config:/config

BIN
.github/assets/github-alerts.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -18,7 +18,7 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
platforms: linux/amd64
pull: true

View File

@@ -30,7 +30,7 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
pull: true

View File

@@ -26,7 +26,7 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
pull: true

View File

@@ -26,9 +26,12 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
pull: true
push: true
tags: ${{ env.IMAGE_REPOSITORY }}:${{ env.RELEASE }},${{ env.IMAGE_REPOSITORY }}:stable
tags: |
${{ env.IMAGE_REPOSITORY }}:${{ env.RELEASE }}
${{ env.IMAGE_REPOSITORY }}:stable
${{ env.IMAGE_REPOSITORY }}:latest

View File

@@ -23,9 +23,12 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
pull: true
push: true
tags: ${{ env.IMAGE_REPOSITORY }}:${{ env.RELEASE }},${{ env.IMAGE_REPOSITORY }}:stable
tags: |
${{ env.IMAGE_REPOSITORY }}:${{ env.RELEASE }}
${{ env.IMAGE_REPOSITORY }}:stable
${{ env.IMAGE_REPOSITORY }}:latest

View File

@@ -3,12 +3,14 @@ on:
pull_request:
paths-ignore:
- '*.md'
- '.examples/*'
push:
branches:
- master
paths-ignore:
- '*.md'
- '.github/*'
- '.examples/*'
jobs:
test:
runs-on: ubuntu-latest

12
.gitignore vendored
View File

@@ -1,6 +1,18 @@
# IDE
*.iml
.idea
.vscode
# OS
.DS_Store
# JS
node_modules
# Go
/vendor
# Misc
*.db
*.db-shm
*.db-wal

View File

@@ -7,7 +7,7 @@ install:
go build -mod vendor -o $(BINARY) .
run:
GATUS_CONFIG_FILE=./config.yaml ./$(BINARY)
GATUS_CONFIG_PATH=./config.yaml ./$(BINARY)
clean:
rm $(BINARY)

165
README.md
View File

@@ -1,4 +1,4 @@
![Gatus](.github/assets/logo-with-dark-text.png)
[![Gatus](.github/assets/logo-with-dark-text.png)](https://gatus.io)
![test](https://github.com/TwiN/gatus/workflows/test/badge.svg?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/TwiN/gatus?)](https://goreportcard.com/report/github.com/TwiN/gatus)
@@ -10,7 +10,7 @@
Gatus is a developer-oriented health dashboard that gives you the ability to monitor your services using HTTP, ICMP, TCP, and even DNS
queries as well as evaluate the result of said queries by using a list of conditions on values like the status code,
the response time, the certificate expiration, the body and many others. The icing on top is that each of these health
checks can be paired with alerting via Slack, PagerDuty, Discord, Twilio and more.
checks can be paired with alerting via Slack, PagerDuty, Pushover, Discord, Twilio and more.
I personally deploy it in my Kubernetes cluster and let it monitor the status of my
core applications: https://status.twin.sh/
@@ -21,15 +21,19 @@ core applications: https://status.twin.sh/
```console
docker run -p 8080:8080 --name gatus twinproduction/gatus
```
You can also use GitHub Container Registry if you prefer:
```console
docker run -p 8080:8080 --name gatus ghcr.io/twin/gatus
```
For more details, see [Usage](#usage)
</details>
> ❤ Like this project? Please consider [sponsoring me](https://github.com/sponsors/TwiN).
![Gatus dashboard](.github/assets/dashboard-dark.png)
Have any feedback or questions? [Create a discussion](https://github.com/TwiN/gatus/discussions/new).
Like this project? Please consider [sponsoring me](https://github.com/sponsors/TwiN).
## Table of Contents
- [Why Gatus?](#why-gatus)
@@ -44,6 +48,7 @@ Like this project? Please consider [sponsoring me](https://github.com/sponsors/T
- [Alerting](#alerting)
- [Configuring Discord alerts](#configuring-discord-alerts)
- [Configuring Email alerts](#configuring-email-alerts)
- [Configuring GitHub alerts](#configuring-github-alerts)
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
- [Configuring Matrix alerts](#configuring-matrix-alerts)
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
@@ -51,6 +56,7 @@ Like this project? Please consider [sponsoring me](https://github.com/sponsors/T
- [Configuring Ntfy alerts](#configuring-ntfy-alerts)
- [Configuring Opsgenie alerts](#configuring-opsgenie-alerts)
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
- [Configuring Pushover alerts](#configuring-pushover-alerts)
- [Configuring Slack alerts](#configuring-slack-alerts)
- [Configuring Teams alerts](#configuring-teams-alerts)
- [Configuring Telegram alerts](#configuring-telegram-alerts)
@@ -128,9 +134,19 @@ The main features of Gatus are:
## Usage
By default, the configuration file is expected to be at `config/config.yaml`.
You can specify a custom path by setting the `GATUS_CONFIG_FILE` environment variable.
<details>
<summary><b>Quick start</b></summary>
```console
docker run -p 8080:8080 --name gatus twinproduction/gatus
```
You can also use GitHub Container Registry if you prefer:
```console
docker run -p 8080:8080 --name gatus ghcr.io/twin/gatus
```
If you want to create your own configuration, see [Docker](#docker) for information on how to mount a configuration file.
</details>
Here's a simple example:
```yaml
@@ -142,18 +158,31 @@ endpoints:
- "[STATUS] == 200" # Status must be 200
- "[BODY].status == UP" # The json path "$.status" must be equal to UP
- "[RESPONSE_TIME] < 300" # Response time must be under 300ms
- name: example
- name: make-sure-header-is-rendered
url: "https://example.org/"
interval: 60s
conditions:
- "[STATUS] == 200"
- "[STATUS] == 200" # Status must be 200
- "[BODY] == pat(*<h1>Example Domain</h1>*)" # Body must contain the specified header
```
This example would look similar to this:
![Simple example](.github/assets/example.png)
Note that you can also use environment variables in the configuration file (e.g. `$DOMAIN`, `${DOMAIN}`)
By default, the configuration file is expected to be at `config/config.yaml`.
You can specify a custom path by setting the `GATUS_CONFIG_PATH` environment variable.
If `GATUS_CONFIG_PATH` points to a directory, all `*.yaml` and `*.yml` files inside said directory and its
subdirectories are merged like so:
- All maps/objects are deep merged (i.e. you could define `alerting.slack` in one file and `alerting.pagerduty` in another file)
- All slices/arrays are appended (i.e. you can define `endpoints` in multiple files and each endpoint will be added to the final list of endpoints)
- Parameters with a primitive value (e.g. `debug`, `metrics`, `alerting.slack.webhook-url`, etc.) may only be defined once to forcefully avoid any ambiguity
- To clarify, this also means that you could not define `alerting.slack.webhook-url` in two files with different values. All files are merged into one before they are processed. This is by design.
> 💡 You can also use environment variables in the configuration file (e.g. `$DOMAIN`, `${DOMAIN}`)
If you want to test it locally, see [Docker](#docker).
@@ -208,6 +237,7 @@ If you want to test it locally, see [Docker](#docker).
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
### Conditions
Here are some examples of conditions you can use:
@@ -249,14 +279,14 @@ Here are some examples of conditions you can use:
#### Functions
| Function | Description | Example |
|:---------|:---------------------------------------------------------------------------------------------------------------|:-----------------------------------|
| `len` | Returns the length of the object/slice. Works only with the `[BODY]` placeholder. | `len([BODY].username) > 8` |
| `has` | Returns `true` or `false` based on whether a given path is valid. Works only with the `[BODY]` placeholder. | `has([BODY].errors) == false` |
| `pat` | Specifies that the string passed as parameter should be evaluated as a pattern. Works only with `==` and `!=`. | `[IP] == pat(192.168.*)` |
| `any` | Specifies that any one of the values passed as parameters is a valid value. Works only with `==` and `!=`. | `[BODY].ip == any(127.0.0.1, ::1)` |
| Function | Description | Example |
|:---------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------|
| `len` | If the given path leads to an array, returns its length. Otherwise, the JSON at the given path is minified and converted to a string, and the resulting number of characters is returned. Works only with the `[BODY]` placeholder. | `len([BODY].username) > 8` |
| `has` | Returns `true` or `false` based on whether a given path is valid. Works only with the `[BODY]` placeholder. | `has([BODY].errors) == false` |
| `pat` | Specifies that the string passed as parameter should be evaluated as a pattern. Works only with `==` and `!=`. | `[IP] == pat(192.168.*)` |
| `any` | Specifies that any one of the values passed as parameters is a valid value. Works only with `==` and `!=`. | `[BODY].ip == any(127.0.0.1, ::1)` |
**NOTE**: Use `pat` only when you need to. `[STATUS] == pat(2*)` is a lot more expensive than `[STATUS] < 300`.
> 💡 Use `pat` only when you need to. `[STATUS] == pat(2*)` is a lot more expensive than `[STATUS] < 300`.
### Storage
@@ -267,6 +297,9 @@ Here are some examples of conditions you can use:
| `storage.type` | Type of storage. Valid types: `memory`, `sqlite`, `postgres`. | `"memory"` |
| `storage.caching` | Whether to use write-through caching. Improves loading time for large dashboards. <br />Only supported if `storage.type` is `sqlite` or `postgres` | `false` |
The results for each endpoint health check as well as the data for uptime and the past events must be persisted
so that they can be displayed on the dashboard. These parameters allow you to configure the storage in question.
- If `storage.type` is `memory` (default):
```yaml
# Note that this is the default value, and you can omit the storage configuration altogether to achieve the same result.
@@ -307,7 +340,7 @@ the client used to send the request.
| `client.oauth2.client-secret` | The client secret which should be used for the `Client credentials flow` | required `""` |
| `client.oauth2.scopes[]` | A list of `scopes` which should be used for the `Client credentials flow`. | required `[""]` |
Note that some of these parameters are ignored based on the type of endpoint. For instance, there's no certificate involved
> 📝 Some of these parameters are ignored based on the type of endpoint. For instance, there's no certificate involved
in ICMP requests (ping), therefore, setting `client.insecure` to `true` for an endpoint of that type will not do anything.
This default configuration is as follows:
@@ -363,7 +396,7 @@ endpoints:
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
individual endpoints with configurable descriptions and thresholds.
Note that if an alerting provider is not properly configured, all alerts configured with the provider's type will be
> 📝 If an alerting provider is not properly configured, all alerts configured with the provider's type will be
ignored.
| Parameter | Description | Default |
@@ -371,6 +404,7 @@ ignored.
| `alerting.custom` | Configuration for custom actions on failure or alerts. <br />See [Configuring Custom alerts](#configuring-custom-alerts). | `{}` |
| `alerting.discord` | Configuration for alerts of type `discord`. <br />See [Configuring Discord alerts](#configuring-discord-alerts). | `{}` |
| `alerting.email` | Configuration for alerts of type `email`. <br />See [Configuring Email alerts](#configuring-email-alerts). | `{}` |
| `alerting.github` | Configuration for alerts of type `github`. <br />See [Configuring GitHub alerts](#configuring-github-alerts). | `{}` |
| `alerting.googlechat` | Configuration for alerts of type `googlechat`. <br />See [Configuring Google Chat alerts](#configuring-google-chat-alerts). | `{}` |
| `alerting.matrix` | Configuration for alerts of type `matrix`. <br />See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` |
| `alerting.mattermost` | Configuration for alerts of type `mattermost`. <br />See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
@@ -378,6 +412,7 @@ ignored.
| `alerting.ntfy` | Configuration for alerts of type `ntfy`. <br />See [Configuring Ntfy alerts](#configuring-ntfy-alerts). | `{}` |
| `alerting.opsgenie` | Configuration for alerts of type `opsgenie`. <br />See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` |
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`. <br />See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` |
| `alerting.pushover` | Configuration for alerts of type `pushover`. <br />See [Configuring Pushover alerts](#configuring-pushover-alerts). | `{}` |
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
| `alerting.teams` | Configuration for alerts of type `teams`. <br />See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` |
| `alerting.telegram` | Configuration for alerts of type `telegram`. <br />See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
@@ -385,7 +420,6 @@ ignored.
#### Configuring Discord alerts
| Parameter | Description | Default |
|:-------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
@@ -416,7 +450,6 @@ endpoints:
#### Configuring Email alerts
| Parameter | Description | Default |
|:-----------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.email` | Configuration for alerts of type `email` | `{}` |
@@ -472,11 +505,47 @@ endpoints:
send-on-resolved: true
```
**NOTE:** Some mail servers are painfully slow.
> Some mail servers are painfully slow.
#### Configuring GitHub alerts
| Parameter | Description | Default |
|:---------------------------------|:-----------------------------------------------------------------------------------------------------------|:--------------|
| `alerting.github` | Configuration for alerts of type `github` | `{}` |
| `alerting.github.repository-url` | GitHub repository URL (e.g. `https://github.com/TwiN/example`) | Required `""` |
| `alerting.github.token` | Personal access token to use for authentication. <br />Must have at least RW on issues and RO on metadata. | Required `""` |
| `alerting.github.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
The GitHub alerting provider creates an issue prefixed with `alert(gatus):` and suffixed with the endpoint's display
name for each alert. If `send-on-resolved` is set to `true` on the endpoint alert, the issue will be automatically
closed when the alert is resolved.
```yaml
alerting:
github:
repository-url: "https://github.com/TwiN/test"
token: "github_pat_12345..."
endpoints:
- name: example
url: "https://twin.sh/health"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
- "[RESPONSE_TIME] < 75"
alerts:
- type: github
failure-threshold: 2
success-threshold: 3
send-on-resolved: true
description: "Everything's burning AAAAAHHHHHHHHHHHHHHH"
```
![GitHub alert](.github/assets/github-alerts.png)
#### Configuring Google Chat alerts
| Parameter | Description | Default |
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:--------------|
| `alerting.googlechat` | Configuration for alerts of type `googlechat` | `{}` |
@@ -485,7 +554,7 @@ endpoints:
| `alerting.googlechat.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
| `alerting.googlechat.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.googlechat.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.googlechat.overrides[].webhook-url` | Teams Webhook URL | `""` |
| `alerting.googlechat.overrides[].webhook-url` | Google Chat Webhook URL | `""` |
```yaml
alerting:
@@ -724,6 +793,39 @@ endpoints:
```
#### Configuring Pushover alerts
| Parameter | Description | Default |
|:---------------------------------------|:------------------------------------------------------------------------------------------------|:-----------------------------|
| `alerting.pushover` | Configuration for alerts of type `pushover` | `{}` |
| `alerting.pushover.application-token` | Pushover application token | `""` |
| `alerting.pushover.user-key` | User or group key | `""` |
| `alerting.pushover.title` | Fixed title for all messages sent via Pushover | Name of your App in Pushover |
| `alerting.pushover.priority` | Priority of all messages, ranging from -2 (very low) to 2 (emergency) | `0` |
| `alerting.pushover.sound` | Sound of all messages<br/ >See [sounds](https://pushover.net/api#sounds) for all valid choices. | `""` |
| `alerting.pushover.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
```yaml
alerting:
pushover:
application-token: "******************************"
user-key: "******************************"
endpoints:
- name: website
url: "https://twin.sh/health"
interval: 30s
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
- "[RESPONSE_TIME] < 300"
alerts:
- type: pushover
failure-threshold: 3
success-threshold: 5
send-on-resolved: true
description: "healthcheck failed"
```
#### Configuring Slack alerts
| Parameter | Description | Default |
|:------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
@@ -733,6 +835,7 @@ endpoints:
| `alerting.slack.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.slack.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.slack.overrides[].webhook-url` | Slack Webhook URL | `""` |
```yaml
alerting:
slack:
@@ -762,7 +865,6 @@ Here's an example of what the notifications look like:
#### Configuring Teams alerts
| Parameter | Description | Default |
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
@@ -1050,7 +1152,7 @@ To do that, you'll have to use the maintenance configuration:
| `maintenance.duration` | Duration of the maintenance window (e.g. `1h`, `30m`) | Required `""` |
| `maintenance.every` | Days on which the maintenance period applies (e.g. `[Monday, Thursday]`).<br />If left empty, the maintenance window applies every day | `[]` |
**Note that the maintenance configuration uses UTC.**
> 📝 The maintenance configuration uses UTC
Here's an example:
```yaml
@@ -1093,8 +1195,8 @@ security:
password-bcrypt-base64: "JDJhJDEwJHRiMnRFakxWazZLdXBzRERQazB1TE8vckRLY05Yb1hSdnoxWU0yQ1FaYXZRSW1McmladDYu"
```
**WARNING:** Make sure to carefully select to cost of the bcrypt hash. The higher the cost, the longer it takes to compute the hash,
and basic auth verifies the password against the hash on every request. As of 2022-01-08, I suggest a cost of 8.
> Make sure to carefully select to cost of the bcrypt hash. The higher the cost, the longer it takes to compute the hash,
and basic auth verifies the password against the hash on every request. As of 2023-01-06, I suggest a cost of 9.
#### OIDC
@@ -1252,7 +1354,7 @@ will send a `POST` request to `http://localhost:8080/playground` with the follow
### Recommended interval
> **NOTE**: This does not apply if `disable-monitoring-lock` is set to `true`, as the monitoring lock is what
> 📝 This does not apply if `disable-monitoring-lock` is set to `true`, as the monitoring lock is what
> tells Gatus to only evaluate one endpoint at a time.
To ensure that Gatus provides reliable and accurate results (i.e. response time), Gatus only evaluates one endpoint at a time
@@ -1308,7 +1410,7 @@ Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `
This works for applications such as databases (Postgres, MySQL, etc.) and caches (Redis, Memcached, etc.).
**NOTE**: `[CONNECTED] == true` does not guarantee that the endpoint itself is healthy - it only guarantees that there's
> 📝 `[CONNECTED] == true` does not guarantee that the endpoint itself is healthy - it only guarantees that there's
something at the given address listening to the given port, and that a connection to that address was successfully
established.
@@ -1424,7 +1526,7 @@ endpoints:
- "[CERTIFICATE_EXPIRATION] > 240h"
```
**NOTE**: The usage of the `[DOMAIN_EXPIRATION]` placeholder requires Gatus to send a request to the official IANA WHOIS service [through a library](https://github.com/TwiN/whois)
> The usage of the `[DOMAIN_EXPIRATION]` placeholder requires Gatus to send a request to the official IANA WHOIS service [through a library](https://github.com/TwiN/whois)
and in some cases, a secondary request to a TLD-specific WHOIS server (e.g. `whois.nic.sh`).
To prevent the WHOIS service from throttling your IP address if you send too many requests, Gatus will prevent you from
using the `[DOMAIN_EXPIRATION]` placeholder on an endpoint with an interval of less than `5m`.
@@ -1465,7 +1567,7 @@ to make.
**If you are not using a file storage**, updating the configuration while Gatus is running is effectively
the same as restarting the application.
**NOTE:** Updates may not be detected if the config file is bound instead of the config folder. See [#151](https://github.com/TwiN/gatus/issues/151).
> 📝 Updates may not be detected if the config file is bound instead of the config folder. See [#151](https://github.com/TwiN/gatus/issues/151).
### Endpoint groups
@@ -1679,4 +1781,3 @@ No such header is required to query the API.
You can find the full list of sponsors [here](https://github.com/sponsors/TwiN).
[<img src="https://github.com/math280h.png" width="50" />](https://github.com/math280h)
[<img src="https://github.com/pyroscope-io.png" width="50" />](https://github.com/pyroscope-io)

View File

@@ -14,6 +14,9 @@ const (
// TypeEmail is the Type for the email alerting provider
TypeEmail Type = "email"
// TypeGitHub is the Type for the github alerting provider
TypeGitHub Type = "github"
// TypeGoogleChat is the Type for the googlechat alerting provider
TypeGoogleChat Type = "googlechat"
@@ -35,6 +38,9 @@ const (
// TypePagerDuty is the Type for the pagerduty alerting provider
TypePagerDuty Type = "pagerduty"
// TypePushover is the Type for the pushover alerting provider
TypePushover Type = "pushover"
// TypeSlack is the Type for the slack alerting provider
TypeSlack Type = "slack"

View File

@@ -1,11 +1,16 @@
package alerting
import (
"log"
"reflect"
"strings"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/alerting/provider"
"github.com/TwiN/gatus/v5/alerting/provider/custom"
"github.com/TwiN/gatus/v5/alerting/provider/discord"
"github.com/TwiN/gatus/v5/alerting/provider/email"
"github.com/TwiN/gatus/v5/alerting/provider/github"
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
@@ -13,6 +18,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
"github.com/TwiN/gatus/v5/alerting/provider/slack"
"github.com/TwiN/gatus/v5/alerting/provider/teams"
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
@@ -24,15 +30,18 @@ type Config struct {
// Custom is the configuration for the custom alerting provider
Custom *custom.AlertProvider `yaml:"custom,omitempty"`
// GoogleChat is the configuration for the Google chat alerting provider
GoogleChat *googlechat.AlertProvider `yaml:"googlechat,omitempty"`
// Discord is the configuration for the discord alerting provider
Discord *discord.AlertProvider `yaml:"discord,omitempty"`
// Email is the configuration for the email alerting provider
Email *email.AlertProvider `yaml:"email,omitempty"`
// GitHub is the configuration for the github alerting provider
GitHub *github.AlertProvider `yaml:"github,omitempty"`
// GoogleChat is the configuration for the googlechat alerting provider
GoogleChat *googlechat.AlertProvider `yaml:"googlechat,omitempty"`
// Matrix is the configuration for the matrix alerting provider
Matrix *matrix.AlertProvider `yaml:"matrix,omitempty"`
@@ -51,6 +60,9 @@ type Config struct {
// PagerDuty is the configuration for the pagerduty alerting provider
PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty,omitempty"`
// Pushover is the configuration for the pushover alerting provider
Pushover *pushover.AlertProvider `yaml:"pushover,omitempty"`
// Slack is the configuration for the slack alerting provider
Slack *slack.AlertProvider `yaml:"slack,omitempty"`
@@ -65,92 +77,30 @@ type Config struct {
}
// GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding alert.Type
func (config Config) GetAlertingProviderByAlertType(alertType alert.Type) provider.AlertProvider {
switch alertType {
case alert.TypeCustom:
if config.Custom == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
func (config *Config) GetAlertingProviderByAlertType(alertType alert.Type) provider.AlertProvider {
entityType := reflect.TypeOf(config).Elem()
for i := 0; i < entityType.NumField(); i++ {
field := entityType.Field(i)
if strings.ToLower(field.Name) == string(alertType) {
fieldValue := reflect.ValueOf(config).Elem().Field(i)
if fieldValue.IsNil() {
return nil
}
return fieldValue.Interface().(provider.AlertProvider)
}
return config.Custom
case alert.TypeDiscord:
if config.Discord == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Discord
case alert.TypeEmail:
if config.Email == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Email
case alert.TypeGoogleChat:
if config.GoogleChat == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.GoogleChat
case alert.TypeMatrix:
if config.Matrix == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Matrix
case alert.TypeMattermost:
if config.Mattermost == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Mattermost
case alert.TypeMessagebird:
if config.Messagebird == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Messagebird
case alert.TypeNtfy:
if config.Ntfy == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Ntfy
case alert.TypeOpsgenie:
if config.Opsgenie == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Opsgenie
case alert.TypePagerDuty:
if config.PagerDuty == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.PagerDuty
case alert.TypeSlack:
if config.Slack == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Slack
case alert.TypeTeams:
if config.Teams == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Teams
case alert.TypeTelegram:
if config.Telegram == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Telegram
case alert.TypeTwilio:
if config.Twilio == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Twilio
}
log.Printf("[alerting][GetAlertingProviderByAlertType] No alerting provider found for alert type %s", alertType)
return nil
}
// SetAlertingProviderToNil Sets an alerting provider to nil to avoid having to revalidate it every time an
// alert of its corresponding type is sent.
func (config *Config) SetAlertingProviderToNil(p provider.AlertProvider) {
entityType := reflect.TypeOf(config).Elem()
for i := 0; i < entityType.NumField(); i++ {
field := entityType.Field(i)
if field.Type == reflect.TypeOf(p) {
reflect.ValueOf(config).Elem().Field(i).Set(reflect.Zero(field.Type))
}
}
}

View File

@@ -0,0 +1,129 @@
package github
import (
"context"
"fmt"
"net/url"
"strings"
"time"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core"
"github.com/google/go-github/v48/github"
"golang.org/x/oauth2"
)
// AlertProvider is the configuration necessary for sending an alert using Discord
type AlertProvider struct {
RepositoryURL string `yaml:"repository-url"` // The URL of the GitHub repository to create issues in
Token string `yaml:"token"` // Token requires at least RW on issues and RO on metadata
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
username string
repositoryOwner string
repositoryName string
githubClient *github.Client
}
// IsValid returns whether the provider's configuration is valid
func (provider *AlertProvider) IsValid() bool {
if len(provider.Token) == 0 || len(provider.RepositoryURL) == 0 {
return false
}
// Validate format of the repository URL
repositoryURL, err := url.Parse(provider.RepositoryURL)
if err != nil {
return false
}
baseURL := repositoryURL.Scheme + "://" + repositoryURL.Host
pathParts := strings.Split(repositoryURL.Path, "/")
if len(pathParts) != 3 {
return false
}
provider.repositoryOwner = pathParts[1]
provider.repositoryName = pathParts[2]
// Create oauth2 HTTP client with GitHub token
httpClientWithStaticTokenSource := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: provider.Token,
}))
// Create GitHub client
if baseURL == "https://github.com" {
provider.githubClient = github.NewClient(httpClientWithStaticTokenSource)
} else {
provider.githubClient, err = github.NewEnterpriseClient(baseURL, baseURL, httpClientWithStaticTokenSource)
if err != nil {
return false
}
}
// Retrieve the username once to validate that the token is valid
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
user, _, err := provider.githubClient.Users.Get(ctx, "")
if err != nil {
return false
}
provider.username = *user.Login
return true
}
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
// or closes the relevant issue(s) if the resolved parameter passed is true.
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
title := "alert(gatus): " + endpoint.DisplayName()
if !resolved {
_, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{
Title: github.String(title),
Body: github.String(provider.buildIssueBody(endpoint, alert, result)),
})
if err != nil {
return fmt.Errorf("failed to create issue: %w", err)
}
} else {
issues, _, err := provider.githubClient.Issues.ListByRepo(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueListByRepoOptions{
State: "open",
Creator: provider.username,
ListOptions: github.ListOptions{PerPage: 100},
})
if err != nil {
return fmt.Errorf("failed to list issues: %w", err)
}
for _, issue := range issues {
if *issue.Title == title {
_, _, err = provider.githubClient.Issues.Edit(context.Background(), provider.repositoryOwner, provider.repositoryName, *issue.Number, &github.IssueRequest{
State: github.String("closed"),
})
if err != nil {
return fmt.Errorf("failed to close issue: %w", err)
}
}
}
}
return nil
}
// buildIssueBody builds the body of the issue
func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result) string {
var results string
for _, conditionResult := range result.ConditionResults {
var prefix string
if conditionResult.Success {
prefix = ":white_check_mark:"
} else {
prefix = ":x:"
}
results += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition)
}
var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = ":\n> " + alertDescription
}
message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
return message + description + "\n\n## Condition results\n" + results
}
// GetDefaultAlert returns the provider's default alert configuration
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}

View File

@@ -0,0 +1,158 @@
package github
import (
"net/http"
"strings"
"testing"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core"
"github.com/TwiN/gatus/v5/test"
"github.com/google/go-github/v48/github"
)
func TestAlertDefaultProvider_IsValid(t *testing.T) {
scenarios := []struct {
Name string
Provider AlertProvider
Expected bool
}{
{
Name: "invalid",
Provider: AlertProvider{RepositoryURL: "", Token: ""},
Expected: false,
},
{
Name: "invalid-token",
Provider: AlertProvider{RepositoryURL: "https://github.com/TwiN/test", Token: "12345"},
Expected: false,
},
{
Name: "missing-repository-name",
Provider: AlertProvider{RepositoryURL: "https://github.com/TwiN", Token: "12345"},
Expected: false,
},
{
Name: "enterprise-client",
Provider: AlertProvider{RepositoryURL: "https://github.example.com/TwiN/test", Token: "12345"},
Expected: false,
},
{
Name: "invalid-url",
Provider: AlertProvider{RepositoryURL: "github.com/TwiN/test", Token: "12345"},
Expected: false,
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
if scenario.Provider.IsValid() != scenario.Expected {
t.Errorf("expected %t, got %t", scenario.Expected, scenario.Provider.IsValid())
}
})
}
}
func TestAlertProvider_Send(t *testing.T) {
defer client.InjectHTTPClient(nil)
firstDescription := "description-1"
secondDescription := "description-2"
scenarios := []struct {
Name string
Provider AlertProvider
Alert alert.Alert
Resolved bool
MockRoundTripper test.MockRoundTripper
ExpectedError bool
}{
{
Name: "triggered-error",
Provider: AlertProvider{RepositoryURL: "https://github.com/TwiN/test", Token: "12345"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedError: true,
},
{
Name: "resolved-error",
Provider: AlertProvider{RepositoryURL: "https://github.com/TwiN/test", Token: "12345"},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedError: true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
scenario.Provider.githubClient = github.NewClient(nil)
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
&scenario.Alert,
&core.Result{
ConditionResults: []*core.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
},
},
scenario.Resolved,
)
if scenario.ExpectedError && err == nil {
t.Error("expected error, got none")
}
if !scenario.ExpectedError && err != nil {
t.Error("expected no error, got", err.Error())
}
})
}
}
func TestAlertProvider_buildRequestBody(t *testing.T) {
firstDescription := "description-1"
scenarios := []struct {
Name string
Endpoint core.Endpoint
Provider AlertProvider
Alert alert.Alert
ExpectedBody string
}{
{
Name: "triggered",
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
},
{
Name: "no-description",
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
Provider: AlertProvider{},
Alert: alert.Alert{FailureThreshold: 10},
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildIssueBody(
&scenario.Endpoint,
&scenario.Alert,
&core.Result{
ConditionResults: []*core.ConditionResult{
{Condition: "[CONNECTED] == true", Success: true},
{Condition: "[STATUS] == 200", Success: false},
},
},
)
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
}
})
}
}
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
t.Error("expected default alert to be not nil")
}
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
t.Error("expected default alert to be nil")
}
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/custom"
"github.com/TwiN/gatus/v5/alerting/provider/discord"
"github.com/TwiN/gatus/v5/alerting/provider/email"
"github.com/TwiN/gatus/v5/alerting/provider/github"
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
@@ -12,6 +13,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
"github.com/TwiN/gatus/v5/alerting/provider/slack"
"github.com/TwiN/gatus/v5/alerting/provider/teams"
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
@@ -58,6 +60,7 @@ var (
_ AlertProvider = (*custom.AlertProvider)(nil)
_ AlertProvider = (*discord.AlertProvider)(nil)
_ AlertProvider = (*email.AlertProvider)(nil)
_ AlertProvider = (*github.AlertProvider)(nil)
_ AlertProvider = (*googlechat.AlertProvider)(nil)
_ AlertProvider = (*matrix.AlertProvider)(nil)
_ AlertProvider = (*mattermost.AlertProvider)(nil)
@@ -65,6 +68,7 @@ var (
_ AlertProvider = (*ntfy.AlertProvider)(nil)
_ AlertProvider = (*opsgenie.AlertProvider)(nil)
_ AlertProvider = (*pagerduty.AlertProvider)(nil)
_ AlertProvider = (*pushover.AlertProvider)(nil)
_ AlertProvider = (*slack.AlertProvider)(nil)
_ AlertProvider = (*teams.AlertProvider)(nil)
_ AlertProvider = (*telegram.AlertProvider)(nil)

View File

@@ -0,0 +1,112 @@
package pushover
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core"
)
const (
restAPIURL = "https://api.pushover.net/1/messages.json"
defaultPriority = 0
)
// AlertProvider is the configuration necessary for sending an alert using Pushover
type AlertProvider struct {
// Key used to authenticate the application sending
// See "Your Applications" on the dashboard, or add a new one: https://pushover.net/apps/build
ApplicationToken string `yaml:"application-token"`
// Key of the user or group the messages should be sent to
UserKey string `yaml:"user-key"`
// The title of your message, likely the application name
// default: the name of your application in Pushover
Title string `yaml:"title,omitempty"`
// Priority of all messages, ranging from -2 (very low) to 2 (Emergency)
// default: 0
Priority int `yaml:"priority,omitempty"`
// Sound of the messages (see: https://pushover.net/api#sounds)
// default: "" (pushover)
Sound string `yaml:"sound,omitempty"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
}
// IsValid returns whether the provider's configuration is valid
func (provider *AlertProvider) IsValid() bool {
if provider.Priority == 0 {
provider.Priority = defaultPriority
}
return len(provider.ApplicationToken) == 30 && len(provider.UserKey) == 30 && provider.Priority >= -2 && provider.Priority <= 2
}
// Send an alert using the provider
// Reference doc for pushover: https://pushover.net/api
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
response, err := client.GetHTTPClient(nil).Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode > 399 {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
}
return err
}
type Body struct {
Token string `json:"token"`
User string `json:"user"`
Title string `json:"title,omitempty"`
Message string `json:"message"`
Priority int `json:"priority"`
Sound string `json:"sound,omitempty"`
}
// buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
var message string
if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
} else {
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
}
body, _ := json.Marshal(Body{
Token: provider.ApplicationToken,
User: provider.UserKey,
Title: provider.Title,
Message: message,
Priority: provider.priority(),
Sound: provider.Sound,
})
return body
}
func (provider *AlertProvider) priority() int {
if provider.Priority == 0 {
return defaultPriority
}
return provider.Priority
}
// GetDefaultAlert returns the provider's default alert configuration
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}

View File

@@ -0,0 +1,181 @@
package pushover
import (
"encoding/json"
"net/http"
"testing"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core"
"github.com/TwiN/gatus/v5/test"
)
func TestPushoverAlertProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{}
if invalidProvider.IsValid() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{
ApplicationToken: "aTokenWithLengthOf30characters",
UserKey: "aTokenWithLengthOf30characters",
Title: "Gatus Notification",
Priority: 1,
}
if !validProvider.IsValid() {
t.Error("provider should've been valid")
}
}
func TestPushoverAlertProvider_IsInvalid(t *testing.T) {
invalidProvider := AlertProvider{
ApplicationToken: "aTokenWithLengthOfMoreThan30characters",
UserKey: "aTokenWithLengthOfMoreThan30characters",
Priority: 5,
}
if invalidProvider.IsValid() {
t.Error("provider should've been invalid")
}
}
func TestAlertProvider_Send(t *testing.T) {
defer client.InjectHTTPClient(nil)
firstDescription := "description-1"
secondDescription := "description-2"
scenarios := []struct {
Name string
Provider AlertProvider
Alert alert.Alert
Resolved bool
MockRoundTripper test.MockRoundTripper
ExpectedError bool
}{
{
Name: "triggered",
Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
}),
ExpectedError: false,
},
{
Name: "triggered-error",
Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody}
}),
ExpectedError: true,
},
{
Name: "resolved",
Provider: AlertProvider{},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
}),
ExpectedError: false,
},
{
Name: "resolved-error",
Provider: AlertProvider{},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody}
}),
ExpectedError: true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send(
&core.Endpoint{Name: "endpoint-name"},
&scenario.Alert,
&core.Result{
ConditionResults: []*core.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
},
},
scenario.Resolved,
)
if scenario.ExpectedError && err == nil {
t.Error("expected error, got none")
}
if !scenario.ExpectedError && err != nil {
t.Error("expected no error, got", err.Error())
}
})
}
}
func TestAlertProvider_buildRequestBody(t *testing.T) {
firstDescription := "description-1"
secondDescription := "description-2"
scenarios := []struct {
Name string
Provider AlertProvider
Alert alert.Alert
Resolved bool
ExpectedBody string
}{
{
Name: "triggered",
Provider: AlertProvider{ApplicationToken: "TokenWithLengthOf30Characters1", UserKey: "TokenWithLengthOf30Characters4"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters1\",\"user\":\"TokenWithLengthOf30Characters4\",\"message\":\"TRIGGERED: endpoint-name - description-1\",\"priority\":0}",
},
{
Name: "resolved",
Provider: AlertProvider{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"RESOLVED: endpoint-name - description-2\",\"priority\":2}",
},
{
Name: "with-sound",
Provider: AlertProvider{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2, Sound: "falling"},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"RESOLVED: endpoint-name - description-2\",\"priority\":2,\"sound\":\"falling\"}",
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"},
&scenario.Alert,
&core.Result{
ConditionResults: []*core.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
},
},
scenario.Resolved,
)
if string(body) != scenario.ExpectedBody {
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
}
out := make(map[string]interface{})
if err := json.Unmarshal(body, &out); err != nil {
t.Error("expected body to be valid JSON, got error:", err.Error())
}
})
}
}
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
t.Error("expected default alert to be not nil")
}
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
t.Error("expected default alert to be nil")
}
}

View File

@@ -36,6 +36,7 @@ func TestGetHTTPClient(t *testing.T) {
}
func TestGetDomainExpiration(t *testing.T) {
t.Parallel()
if domainExpiration, err := GetDomainExpiration("example.com"); err != nil {
t.Fatalf("expected error to be nil, but got: `%s`", err)
} else if domainExpiration <= 0 {
@@ -63,6 +64,7 @@ func TestGetDomainExpiration(t *testing.T) {
}
func TestPing(t *testing.T) {
t.Parallel()
if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond}); !success {
t.Error("expected true")
if rtt == 0 {
@@ -121,6 +123,7 @@ func TestCanPerformStartTLS(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
connected, _, err := CanPerformStartTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
if (err != nil) != tt.wantErr {
t.Errorf("CanPerformStartTLS() err=%v, wantErr=%v", err, tt.wantErr)
@@ -171,6 +174,7 @@ func TestCanPerformTLS(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
connected, _, err := CanPerformTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
if (err != nil) != tt.wantErr {
t.Errorf("CanPerformTLS() err=%v, wantErr=%v", err, tt.wantErr)

View File

@@ -22,7 +22,7 @@ const (
var (
ErrInvalidDNSResolver = errors.New("invalid DNS resolver specified. Required format is {proto}://{ip}:{port}")
ErrInvalidDNSResolverPort = errors.New("invalid DNS resolver port")
ErrInvalidClientOAuth2Config = errors.New("invalid OAuth2 configuration, all fields are required")
ErrInvalidClientOAuth2Config = errors.New("invalid oauth2 configuration: must define all fields for client credentials flow (token-url, client-id, client-secret, scopes)")
defaultConfig = Config{
Insecure: false,

View File

@@ -3,10 +3,13 @@ package config
import (
"errors"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"time"
"github.com/TwiN/deepmerge"
"github.com/TwiN/gatus/v5/alerting"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/alerting/provider"
@@ -18,12 +21,12 @@ import (
"github.com/TwiN/gatus/v5/security"
"github.com/TwiN/gatus/v5/storage"
"github.com/TwiN/gatus/v5/util"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
const (
// DefaultConfigurationFilePath is the default path that will be used to search for the configuration file
// if a custom path isn't configured through the GATUS_CONFIG_FILE environment variable
// if a custom path isn't configured through the GATUS_CONFIG_PATH environment variable
DefaultConfigurationFilePath = "config/config.yaml"
// DefaultFallbackConfigurationFilePath is the default fallback path that will be used to search for the
@@ -32,14 +35,17 @@ const (
)
var (
// ErrNoEndpointInConfig is an error returned when a configuration file has no endpoints configured
ErrNoEndpointInConfig = errors.New("configuration file should contain at least 1 endpoint")
// ErrNoEndpointInConfig is an error returned when a configuration file or directory has no endpoints configured
ErrNoEndpointInConfig = errors.New("configuration should contain at least 1 endpoint")
// ErrConfigFileNotFound is an error returned when the configuration file could not be found
// ErrConfigFileNotFound is an error returned when a configuration file could not be found
ErrConfigFileNotFound = errors.New("configuration file not found")
// ErrInvalidSecurityConfig is an error returned when the security configuration is invalid
ErrInvalidSecurityConfig = errors.New("invalid security configuration")
// errEarlyReturn is returned to break out of a loop from a callback early
errEarlyReturn = errors.New("early escape")
)
// Config is the main configuration structure
@@ -84,11 +90,12 @@ type Config struct {
// WARNING: This is in ALPHA and may change or be completely removed in the future
Remote *remote.Config `yaml:"remote,omitempty"`
filePath string // path to the file from which config was loaded from
configPath string // path to the file or directory from which config was loaded
lastFileModTime time.Time // last modification time
}
func (config *Config) GetEndpointByKey(key string) *core.Endpoint {
// TODO: Should probably add a mutex here to prevent concurrent access
for i := 0; i < len(config.Endpoints); i++ {
ep := config.Endpoints[i]
if util.ConvertGroupAndEndpointNameToKey(ep.Group, ep.Name) == key {
@@ -98,63 +105,111 @@ func (config *Config) GetEndpointByKey(key string) *core.Endpoint {
return nil
}
// HasLoadedConfigurationFileBeenModified returns whether the file that the
// HasLoadedConfigurationBeenModified returns whether one of the file that the
// configuration has been loaded from has been modified since it was last read
func (config Config) HasLoadedConfigurationFileBeenModified() bool {
if fileInfo, err := os.Stat(config.filePath); err == nil {
if !fileInfo.ModTime().IsZero() {
return config.lastFileModTime.Unix() != fileInfo.ModTime().Unix()
}
func (config Config) HasLoadedConfigurationBeenModified() bool {
lastMod := config.lastFileModTime.Unix()
fileInfo, err := os.Stat(config.configPath)
if err != nil {
return false
}
return false
if fileInfo.IsDir() {
err = walkConfigDir(config.configPath, func(path string, d fs.DirEntry, err error) error {
if info, err := d.Info(); err == nil && lastMod < info.ModTime().Unix() {
return errEarlyReturn
}
return nil
})
return err == errEarlyReturn
}
return !fileInfo.ModTime().IsZero() && config.lastFileModTime.Unix() < fileInfo.ModTime().Unix()
}
// UpdateLastFileModTime refreshes Config.lastFileModTime
func (config *Config) UpdateLastFileModTime() {
if fileInfo, err := os.Stat(config.filePath); err == nil {
if !fileInfo.ModTime().IsZero() {
config.lastFileModTime = fileInfo.ModTime()
config.lastFileModTime = time.Now()
}
// LoadConfiguration loads the full configuration composed from the main configuration file
// and all composed configuration files
func LoadConfiguration(configPath string) (*Config, error) {
var configBytes []byte
var fileInfo os.FileInfo
var usedConfigPath string
// Figure out what config path we'll use (either configPath or the default config path)
for _, configurationPath := range []string{configPath, DefaultConfigurationFilePath, DefaultFallbackConfigurationFilePath} {
if len(configurationPath) == 0 {
continue
}
var err error
fileInfo, err = os.Stat(configurationPath)
if err != nil {
continue
}
usedConfigPath = configurationPath
break
}
if len(usedConfigPath) == 0 {
return nil, ErrConfigFileNotFound
}
var config *Config
if fileInfo.IsDir() {
err := walkConfigDir(configPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
log.Printf("[config][LoadConfiguration] Error walking path=%s: %s", path, err)
return err
}
log.Printf("[config][LoadConfiguration] Reading configuration from %s", path)
data, err := os.ReadFile(path)
if err != nil {
log.Printf("[config][LoadConfiguration] Error reading configuration from %s: %s", path, err)
return fmt.Errorf("error reading configuration from file %s: %w", path, err)
}
configBytes, err = deepmerge.YAML(configBytes, data)
return err
})
if err != nil {
return nil, fmt.Errorf("error reading configuration from directory %s: %w", usedConfigPath, err)
}
} else {
log.Println("[config][UpdateLastFileModTime] Ran into error updating lastFileModTime:", err.Error())
}
}
// Load loads a custom configuration file
// Note that the misconfiguration of some fields may lead to panics. This is on purpose.
func Load(configFile string) (*Config, error) {
log.Printf("[config][Load] Reading configuration from configFile=%s", configFile)
cfg, err := readConfigurationFile(configFile)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrConfigFileNotFound
log.Printf("[config][LoadConfiguration] Reading configuration from configFile=%s", configPath)
if data, err := os.ReadFile(usedConfigPath); err != nil {
return nil, err
} else {
configBytes = data
}
}
if len(configBytes) == 0 {
return nil, ErrConfigFileNotFound
}
config, err := parseAndValidateConfigBytes(configBytes)
if err != nil {
return nil, err
}
cfg.filePath = configFile
cfg.UpdateLastFileModTime()
return cfg, nil
config.configPath = usedConfigPath
config.UpdateLastFileModTime()
return config, err
}
// LoadDefaultConfiguration loads the default configuration file
func LoadDefaultConfiguration() (*Config, error) {
cfg, err := Load(DefaultConfigurationFilePath)
if err != nil {
if err == ErrConfigFileNotFound {
return Load(DefaultFallbackConfigurationFilePath)
// walkConfigDir is a wrapper for filepath.WalkDir that strips directories and non-config files
func walkConfigDir(path string, fn fs.WalkDirFunc) error {
if len(path) == 0 {
// If the user didn't provide a directory, we'll just use the default config file, so we can return nil now.
return nil
}
return filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
return nil, err
}
return cfg, nil
}
func readConfigurationFile(fileName string) (config *Config, err error) {
var bytes []byte
if bytes, err = os.ReadFile(fileName); err == nil {
// file exists, so we'll parse it and return it
return parseAndValidateConfigBytes(bytes)
}
return
if d == nil || d.IsDir() {
return nil
}
ext := filepath.Ext(path)
if ext != ".yml" && ext != ".yaml" {
return nil
}
return fn(path, d, err)
})
}
// parseAndValidateConfigBytes parses a Gatus configuration file into a Config struct and validates its parameters
@@ -288,6 +343,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
alertTypes := []alert.Type{
alert.TypeCustom,
alert.TypeDiscord,
alert.TypeGitHub,
alert.TypeGoogleChat,
alert.TypeEmail,
alert.TypeMatrix,
@@ -296,6 +352,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
alert.TypeNtfy,
alert.TypeOpsgenie,
alert.TypePagerDuty,
alert.TypePushover,
alert.TypeSlack,
alert.TypeTeams,
alert.TypeTelegram,
@@ -323,6 +380,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
} else {
log.Printf("[config][validateAlertingConfig] Ignoring provider=%s because configuration is invalid", alertType)
invalidProviders = append(invalidProviders, alertType)
alertingConfig.SetAlertingProviderToNil(alertProvider)
}
} else {
invalidProviders = append(invalidProviders, alertType)

View File

@@ -1,7 +1,10 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"testing"
"time"
@@ -11,6 +14,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/custom"
"github.com/TwiN/gatus/v5/alerting/provider/discord"
"github.com/TwiN/gatus/v5/alerting/provider/email"
"github.com/TwiN/gatus/v5/alerting/provider/github"
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
@@ -18,6 +22,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/ntfy"
"github.com/TwiN/gatus/v5/alerting/provider/opsgenie"
"github.com/TwiN/gatus/v5/alerting/provider/pagerduty"
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
"github.com/TwiN/gatus/v5/alerting/provider/slack"
"github.com/TwiN/gatus/v5/alerting/provider/teams"
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
@@ -26,20 +31,270 @@ import (
"github.com/TwiN/gatus/v5/config/web"
"github.com/TwiN/gatus/v5/core"
"github.com/TwiN/gatus/v5/storage"
"gopkg.in/yaml.v3"
)
func TestLoadFileThatDoesNotExist(t *testing.T) {
_, err := Load("file-that-does-not-exist.yaml")
if err == nil {
t.Error("Should've returned an error, because the file specified doesn't exist")
func TestLoadConfiguration(t *testing.T) {
dir := t.TempDir()
scenarios := []struct {
name string
configPath string // value to pass as the configPath parameter in LoadConfiguration
pathAndFiles map[string]string // files to create in dir
expectedConfig *Config
expectedError error
}{
{
name: "empty-config-file",
configPath: filepath.Join(dir, "config.yaml"),
pathAndFiles: map[string]string{
"config.yaml": "",
},
expectedError: ErrConfigFileNotFound,
},
{
name: "config-file-that-does-not-exist",
configPath: filepath.Join(dir, "config.yaml"),
expectedError: ErrConfigFileNotFound,
},
{
name: "config-file-with-endpoint-that-has-no-url",
configPath: filepath.Join(dir, "config.yaml"),
pathAndFiles: map[string]string{
"config.yaml": `
endpoints:
- name: website`,
},
expectedError: core.ErrEndpointWithNoURL,
},
{
name: "config-file-with-endpoint-that-has-no-conditions",
configPath: filepath.Join(dir, "config.yaml"),
pathAndFiles: map[string]string{
"config.yaml": `
endpoints:
- name: website
url: https://twin.sh/health`,
},
expectedError: core.ErrEndpointWithNoCondition,
},
{
name: "config-file",
configPath: filepath.Join(dir, "config.yaml"),
pathAndFiles: map[string]string{
"config.yaml": `
endpoints:
- name: website
url: https://twin.sh/health
conditions:
- "[STATUS] == 200"`,
},
expectedConfig: &Config{
Endpoints: []*core.Endpoint{
{
Name: "website",
URL: "https://twin.sh/health",
Conditions: []core.Condition{"[STATUS] == 200"},
},
},
},
},
{
name: "empty-dir",
configPath: dir,
pathAndFiles: map[string]string{},
expectedError: ErrConfigFileNotFound,
},
{
name: "dir-with-empty-config-file",
configPath: dir,
pathAndFiles: map[string]string{
"config.yaml": "",
},
expectedError: ErrNoEndpointInConfig,
},
{
name: "dir-with-two-config-files",
configPath: dir,
pathAndFiles: map[string]string{
"config.yaml": `endpoints:
- name: one
url: https://example.com
conditions:
- "[CONNECTED] == true"
- "[STATUS] == 200"
- name: two
url: https://example.org
conditions:
- "len([BODY]) > 0"`,
"config.yml": `endpoints:
- name: three
url: https://twin.sh/health
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"`,
},
expectedConfig: &Config{
Endpoints: []*core.Endpoint{
{
Name: "one",
URL: "https://example.com",
Conditions: []core.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
},
{
Name: "two",
URL: "https://example.org",
Conditions: []core.Condition{"len([BODY]) > 0"},
},
{
Name: "three",
URL: "https://twin.sh/health",
Conditions: []core.Condition{"[STATUS] == 200", "[BODY].status == UP"},
},
},
},
},
{
name: "dir-with-2-config-files-deep-merge-with-map-slice-and-primitive",
configPath: dir,
pathAndFiles: map[string]string{
"a.yaml": `
metrics: true
alerting:
slack:
webhook-url: https://hooks.slack.com/services/xxx/yyy/zzz
endpoints:
- name: example
url: https://example.org
interval: 5s
conditions:
- "[STATUS] == 200"`,
"b.yaml": `
debug: true
alerting:
discord:
webhook-url: https://discord.com/api/webhooks/xxx/yyy
endpoints:
- name: frontend
url: https://example.com
conditions:
- "[STATUS] == 200"`,
},
expectedConfig: &Config{
Debug: true,
Metrics: true,
Alerting: &alerting.Config{
Discord: &discord.AlertProvider{WebhookURL: "https://discord.com/api/webhooks/xxx/yyy"},
Slack: &slack.AlertProvider{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz"},
},
Endpoints: []*core.Endpoint{
{
Name: "example",
URL: "https://example.org",
Interval: 5 * time.Second,
Conditions: []core.Condition{"[STATUS] == 200"},
},
{
Name: "frontend",
URL: "https://example.com",
Conditions: []core.Condition{"[STATUS] == 200"},
},
},
},
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
for path, content := range scenario.pathAndFiles {
if err := os.WriteFile(filepath.Join(dir, path), []byte(content), 0644); err != nil {
t.Fatalf("[%s] failed to write file: %v", scenario.name, err)
}
}
defer func(pathAndFiles map[string]string) {
for path := range pathAndFiles {
_ = os.Remove(filepath.Join(dir, path))
}
}(scenario.pathAndFiles)
config, err := LoadConfiguration(scenario.configPath)
if !errors.Is(err, scenario.expectedError) {
t.Errorf("[%s] expected error %v, got %v", scenario.name, scenario.expectedError, err)
return
} else if err != nil && errors.Is(err, scenario.expectedError) {
return
}
// parse the expected output so that expectations are closer to reality (under the right circumstances, even I can be poetic)
expectedConfigAsYAML, _ := yaml.Marshal(scenario.expectedConfig)
expectedConfigAfterBeingParsedAndValidated, err := parseAndValidateConfigBytes(expectedConfigAsYAML)
if err != nil {
t.Fatalf("[%s] failed to parse expected config: %v", scenario.name, err)
}
// Marshal em' before comparing em' so that we don't have to deal with formatting and ordering
actualConfigAsYAML, err := yaml.Marshal(config)
if err != nil {
t.Fatalf("[%s] failed to marshal actual config: %v", scenario.name, err)
}
expectedConfigAfterBeingParsedAndValidatedAsYAML, _ := yaml.Marshal(expectedConfigAfterBeingParsedAndValidated)
if string(actualConfigAsYAML) != string(expectedConfigAfterBeingParsedAndValidatedAsYAML) {
t.Errorf("[%s] expected config %s, got %s", scenario.name, string(expectedConfigAfterBeingParsedAndValidatedAsYAML), string(actualConfigAsYAML))
}
})
}
}
func TestLoadDefaultConfigurationFile(t *testing.T) {
_, err := LoadDefaultConfiguration()
if err == nil {
t.Error("Should've returned an error, because there's no configuration files at the default path nor the default fallback path")
}
func TestConfig_HasLoadedConfigurationBeenModified(t *testing.T) {
t.Parallel()
dir := t.TempDir()
configFilePath := filepath.Join(dir, "config.yaml")
_ = os.WriteFile(configFilePath, []byte(`endpoints:
- name: website
url: https://twin.sh/health
conditions:
- "[STATUS] == 200"
`), 0644)
t.Run("config-file-as-config-path", func(t *testing.T) {
config, err := LoadConfiguration(configFilePath)
if err != nil {
t.Fatalf("failed to load configuration: %v", err)
}
if config.HasLoadedConfigurationBeenModified() {
t.Errorf("expected config.HasLoadedConfigurationBeenModified() to return false because nothing has happened since it was created")
}
time.Sleep(time.Second) // Because the file mod time only has second precision, we have to wait for a second
// Update the config file
if err = os.WriteFile(filepath.Join(dir, "config.yaml"), []byte(`endpoints:
- name: website
url: https://twin.sh/health
conditions:
- "[STATUS] == 200"`), 0644); err != nil {
t.Fatalf("failed to overwrite config file: %v", err)
}
if !config.HasLoadedConfigurationBeenModified() {
t.Errorf("expected config.HasLoadedConfigurationBeenModified() to return true because a new file has been added in the directory")
}
})
t.Run("config-directory-as-config-path", func(t *testing.T) {
config, err := LoadConfiguration(dir)
if err != nil {
t.Fatalf("failed to load configuration: %v", err)
}
if config.HasLoadedConfigurationBeenModified() {
t.Errorf("expected config.HasLoadedConfigurationBeenModified() to return false because nothing has happened since it was created")
}
time.Sleep(time.Second) // Because the file mod time only has second precision, we have to wait for a second
// Update the config file
if err = os.WriteFile(filepath.Join(dir, "metrics.yaml"), []byte(`metrics: true`), 0644); err != nil {
t.Fatalf("failed to overwrite config file: %v", err)
}
if !config.HasLoadedConfigurationBeenModified() {
t.Errorf("expected config.HasLoadedConfigurationBeenModified() to return true because a new file has been added in the directory")
}
})
}
func TestParseAndValidateConfigBytes(t *testing.T) {
@@ -430,6 +685,9 @@ alerting:
webhook-url: "http://example.org"
pagerduty:
integration-key: "00000000000000000000000000000000"
pushover:
application-token: "000000000000000000000000000000"
user-key: "000000000000000000000000000000"
mattermost:
webhook-url: "http://example.com"
client:
@@ -469,6 +727,7 @@ endpoints:
failure-threshold: 12
success-threshold: 15
- type: teams
- type: pushover
conditions:
- "[STATUS] == 200"
`))
@@ -495,8 +754,8 @@ endpoints:
if config.Endpoints[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
}
if len(config.Endpoints[0].Alerts) != 8 {
t.Fatal("There should've been 8 alerts configured")
if len(config.Endpoints[0].Alerts) != 9 {
t.Fatal("There should've been 9 alerts configured")
}
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
@@ -596,6 +855,13 @@ endpoints:
if config.Endpoints[0].Alerts[7].SuccessThreshold != 2 {
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[7].SuccessThreshold)
}
if config.Endpoints[0].Alerts[8].Type != alert.TypePushover {
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePushover, config.Endpoints[0].Alerts[8].Type)
}
if !config.Endpoints[0].Alerts[8].IsEnabled() {
t.Error("The alert should've been enabled")
}
}
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) {
@@ -620,6 +886,14 @@ alerting:
description: default description
failure-threshold: 7
success-threshold: 5
pushover:
application-token: "000000000000000000000000000000"
user-key: "000000000000000000000000000000"
default-alert:
enabled: true
description: default description
failure-threshold: 5
success-threshold: 3
mattermost:
webhook-url: "http://example.com"
default-alert:
@@ -663,6 +937,7 @@ endpoints:
- type: telegram
- type: twilio
- type: teams
- type: pushover
conditions:
- "[STATUS] == 200"
`))
@@ -699,6 +974,19 @@ endpoints:
t.Errorf("PagerDuty integration key should've been %s, but was %s", "00000000000000000000000000000000", config.Alerting.PagerDuty.IntegrationKey)
}
if config.Alerting.Pushover == nil || !config.Alerting.Pushover.IsValid() {
t.Fatal("Pushover alerting config should've been valid")
}
if config.Alerting.Pushover.GetDefaultAlert() == nil {
t.Fatal("Pushover.GetDefaultAlert() shouldn't have returned nil")
}
if config.Alerting.Pushover.ApplicationToken != "000000000000000000000000000000" {
t.Errorf("Pushover application token should've been %s, but was %s", "000000000000000000000000000000", config.Alerting.Pushover.ApplicationToken)
}
if config.Alerting.Pushover.UserKey != "000000000000000000000000000000" {
t.Errorf("Pushover user key should've been %s, but was %s", "000000000000000000000000000000", config.Alerting.Pushover.UserKey)
}
if config.Alerting.Mattermost == nil || !config.Alerting.Mattermost.IsValid() {
t.Fatal("Mattermost alerting config should've been valid")
}
@@ -772,8 +1060,8 @@ endpoints:
if config.Endpoints[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
}
if len(config.Endpoints[0].Alerts) != 8 {
t.Fatal("There should've been 8 alerts configured")
if len(config.Endpoints[0].Alerts) != 9 {
t.Fatal("There should've been 9 alerts configured")
}
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
@@ -877,6 +1165,19 @@ endpoints:
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[7].SuccessThreshold)
}
if config.Endpoints[0].Alerts[8].Type != alert.TypePushover {
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePushover, config.Endpoints[0].Alerts[8].Type)
}
if !config.Endpoints[0].Alerts[8].IsEnabled() {
t.Error("The alert should've been enabled")
}
if config.Endpoints[0].Alerts[8].FailureThreshold != 5 {
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[8].FailureThreshold)
}
if config.Endpoints[0].Alerts[8].SuccessThreshold != 3 {
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[8].SuccessThreshold)
}
}
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) {
@@ -980,11 +1281,34 @@ endpoints:
if config.Alerting == nil {
t.Fatal("config.Alerting shouldn't have been nil")
}
if config.Alerting.PagerDuty == nil {
t.Fatal("PagerDuty alerting config shouldn't have been nil")
if config.Alerting.PagerDuty != nil {
t.Fatal("PagerDuty alerting config should've been set to nil, because its IsValid() method returned false and therefore alerting.Config.SetAlertingProviderToNil() should've been called")
}
if config.Alerting.PagerDuty.IsValid() {
t.Fatal("PagerDuty alerting config should've been invalid")
}
func TestParseAndValidateConfigBytesWithInvalidPushoverAlertingConfig(t *testing.T) {
config, err := parseAndValidateConfigBytes([]byte(`
alerting:
pushover:
application-token: "INVALID_TOKEN"
endpoints:
- name: website
url: https://twin.sh/health
alerts:
- type: pushover
conditions:
- "[STATUS] == 200"
`))
if err != nil {
t.Error("expected no error, got", err.Error())
}
if config == nil {
t.Fatal("Config shouldn't have been nil")
}
if config.Alerting == nil {
t.Fatal("config.Alerting shouldn't have been nil")
}
if config.Alerting.Pushover != nil {
t.Fatal("Pushover alerting config should've been set to nil, because its IsValid() method returned false and therefore alerting.Config.SetAlertingProviderToNil() should've been called")
}
}
@@ -1015,7 +1339,7 @@ endpoints:
t.Fatal("config.Alerting shouldn't have been nil")
}
if config.Alerting.Custom == nil {
t.Fatal("PagerDuty alerting config shouldn't have been nil")
t.Fatal("Custom alerting config shouldn't have been nil")
}
if !config.Alerting.Custom.IsValid() {
t.Fatal("Custom alerting config should've been valid")
@@ -1060,7 +1384,7 @@ endpoints:
t.Fatal("config.Alerting shouldn't have been nil")
}
if config.Alerting.Custom == nil {
t.Fatal("PagerDuty alerting config shouldn't have been nil")
t.Fatal("Custom alerting config shouldn't have been nil")
}
if !config.Alerting.Custom.IsValid() {
t.Fatal("Custom alerting config should've been valid")
@@ -1100,7 +1424,7 @@ endpoints:
t.Fatal("config.Alerting shouldn't have been nil")
}
if config.Alerting.Custom == nil {
t.Fatal("PagerDuty alerting config shouldn't have been nil")
t.Fatal("Custom alerting config shouldn't have been nil")
}
if !config.Alerting.Custom.IsValid() {
t.Fatal("Custom alerting config should've been valid")
@@ -1222,6 +1546,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
Custom: &custom.AlertProvider{},
Discord: &discord.AlertProvider{},
Email: &email.AlertProvider{},
GitHub: &github.AlertProvider{},
GoogleChat: &googlechat.AlertProvider{},
Matrix: &matrix.AlertProvider{},
Mattermost: &mattermost.AlertProvider{},
@@ -1229,6 +1554,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
Ntfy: &ntfy.AlertProvider{},
Opsgenie: &opsgenie.AlertProvider{},
PagerDuty: &pagerduty.AlertProvider{},
Pushover: &pushover.AlertProvider{},
Slack: &slack.AlertProvider{},
Telegram: &telegram.AlertProvider{},
Twilio: &twilio.AlertProvider{},
@@ -1241,6 +1567,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
{alertType: alert.TypeCustom, expected: alertingConfig.Custom},
{alertType: alert.TypeDiscord, expected: alertingConfig.Discord},
{alertType: alert.TypeEmail, expected: alertingConfig.Email},
{alertType: alert.TypeGitHub, expected: alertingConfig.GitHub},
{alertType: alert.TypeGoogleChat, expected: alertingConfig.GoogleChat},
{alertType: alert.TypeMatrix, expected: alertingConfig.Matrix},
{alertType: alert.TypeMattermost, expected: alertingConfig.Mattermost},
@@ -1248,6 +1575,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
{alertType: alert.TypeNtfy, expected: alertingConfig.Ntfy},
{alertType: alert.TypeOpsgenie, expected: alertingConfig.Opsgenie},
{alertType: alert.TypePagerDuty, expected: alertingConfig.PagerDuty},
{alertType: alert.TypePushover, expected: alertingConfig.Pushover},
{alertType: alert.TypeSlack, expected: alertingConfig.Slack},
{alertType: alert.TypeTelegram, expected: alertingConfig.Telegram},
{alertType: alert.TypeTwilio, expected: alertingConfig.Twilio},

View File

@@ -1,14 +1,11 @@
package handler
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/TwiN/gatus/v5/client"
@@ -31,25 +28,14 @@ var (
)
// EndpointStatuses handles requests to retrieve all EndpointStatus
// Due to the size of the response, this function leverages a cache.
// Must not be wrapped by GzipHandler
// Due to how intensive this operation can be on the storage, this function leverages a cache.
func EndpointStatuses(cfg *config.Config) http.HandlerFunc {
return func(writer http.ResponseWriter, r *http.Request) {
page, pageSize := extractPageAndPageSizeFromRequest(r)
gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
var exists bool
var value interface{}
if gzipped {
writer.Header().Set("Content-Encoding", "gzip")
value, exists = cache.Get(fmt.Sprintf("endpoint-status-%d-%d-gzipped", page, pageSize))
} else {
value, exists = cache.Get(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize))
}
value, exists := cache.Get(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize))
var data []byte
if !exists {
var err error
buffer := &bytes.Buffer{}
gzipWriter := gzip.NewWriter(buffer)
endpointStatuses, err := store.Get().GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(page, pageSize))
if err != nil {
log.Printf("[handler][EndpointStatuses] Failed to retrieve endpoint statuses: %s", err.Error())
@@ -69,14 +55,7 @@ func EndpointStatuses(cfg *config.Config) http.HandlerFunc {
http.Error(writer, "unable to marshal object to JSON", http.StatusInternalServerError)
return
}
_, _ = gzipWriter.Write(data)
_ = gzipWriter.Close()
gzippedData := buffer.Bytes()
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize), data, cacheTTL)
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d-gzipped", page, pageSize), gzippedData, cacheTTL)
if gzipped {
data = gzippedData
}
} else {
data = value.([]byte)
}

View File

@@ -8,14 +8,18 @@ import (
static "github.com/TwiN/gatus/v5/web"
"github.com/TwiN/health"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func CreateRouter(cfg *config.Config) *mux.Router {
router := mux.NewRouter()
if cfg.Metrics {
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
router.Handle("/metrics", promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{
DisableCompression: true,
}))).Methods("GET")
}
router.Use(GzipHandler)
api := router.PathPrefix("/api").Subrouter()
protected := api.PathPrefix("/").Subrouter()
unprotected := api.PathPrefix("/").Subrouter()
@@ -29,8 +33,8 @@ func CreateRouter(cfg *config.Config) *mux.Router {
}
// Endpoints
unprotected.Handle("/v1/config", ConfigHandler{securityConfig: cfg.Security}).Methods("GET")
protected.HandleFunc("/v1/endpoints/statuses", EndpointStatuses(cfg)).Methods("GET") // No GzipHandler for this one, because we cache the content as Gzipped already
protected.HandleFunc("/v1/endpoints/{key}/statuses", GzipHandlerFunc(EndpointStatus)).Methods("GET")
protected.HandleFunc("/v1/endpoints/statuses", EndpointStatuses(cfg)).Methods("GET")
protected.HandleFunc("/v1/endpoints/{key}/statuses", EndpointStatus).Methods("GET")
unprotected.HandleFunc("/v1/endpoints/{key}/health/badge.svg", HealthBadge).Methods("GET")
unprotected.HandleFunc("/v1/endpoints/{key}/uptimes/{duration}/badge.svg", UptimeBadge).Methods("GET")
unprotected.HandleFunc("/v1/endpoints/{key}/response-times/{duration}/badge.svg", ResponseTimeBadge(cfg)).Methods("GET")
@@ -45,6 +49,6 @@ func CreateRouter(cfg *config.Config) *mux.Router {
if err != nil {
panic(err)
}
router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.FS(staticFileSystem))))
router.PathPrefix("/").Handler(http.FileServer(http.FS(staticFileSystem)))
return router
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/TwiN/gatus/v5/pattern"
)
// Placeholders
const (
// StatusPlaceholder is a placeholder for a HTTP status.
//
@@ -49,7 +50,10 @@ const (
// DomainExpirationPlaceholder is a placeholder for the duration before the domain expires, in milliseconds.
DomainExpirationPlaceholder = "[DOMAIN_EXPIRATION]"
)
// Functions
const (
// LengthFunctionPrefix is the prefix for the length function
//
// Usage: len([BODY].articles) == 10, len([BODY].name) > 5
@@ -72,7 +76,10 @@ const (
// FunctionSuffix is the suffix for all functions
FunctionSuffix = ")"
)
// Other constants
const (
// InvalidConditionElementSuffix is the suffix that will be appended to an invalid condition
InvalidConditionElementSuffix = "(INVALID)"

View File

@@ -182,6 +182,34 @@ func TestCondition_evaluate(t *testing.T) {
ExpectedSuccess: true,
ExpectedOutput: "[BODY] == test",
},
{
Name: "body-numerical-equal",
Condition: Condition("[BODY] == 123"),
Result: &Result{body: []byte("123")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY] == 123",
},
{
Name: "body-numerical-less-than",
Condition: Condition("[BODY] < 124"),
Result: &Result{body: []byte("123")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY] < 124",
},
{
Name: "body-numerical-greater-than",
Condition: Condition("[BODY] > 122"),
Result: &Result{body: []byte("123")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY] > 122",
},
{
Name: "body-numerical-greater-than-failure",
Condition: Condition("[BODY] > 123"),
Result: &Result{body: []byte("100")},
ExpectedSuccess: false,
ExpectedOutput: "[BODY] (100) > 123",
},
{
Name: "body-jsonpath",
Condition: Condition("[BODY].status == UP"),
@@ -231,6 +259,13 @@ func TestCondition_evaluate(t *testing.T) {
ExpectedSuccess: true,
ExpectedOutput: "[BODY][0].id == 1",
},
{
Name: "body-jsonpath-when-body-is-array-but-actual-body-is-not",
Condition: Condition("[BODY][0].name == test"),
Result: &Result{body: []byte("{\"statusCode\": 500, \"message\": \"Internal Server Error\"}")},
ExpectedSuccess: false,
ExpectedOutput: "[BODY][0].name (INVALID) == test",
},
{
Name: "body-jsonpath-complex-int",
Condition: Condition("[BODY].data.id == 1"),
@@ -561,6 +596,14 @@ func TestCondition_evaluate(t *testing.T) {
ExpectedSuccess: true,
ExpectedOutput: "has([BODY].errors) == false",
},
{
Name: "has-key-of-map",
Condition: Condition("has([BODY].article) == true"),
Result: &Result{body: []byte("{\n \"article\": {\n \"id\": 123,\n \"title\": \"Hello, world!\",\n \"author\": \"John Doe\",\n \"tags\": [\"hello\", \"world\"],\n \"content\": \"I really like Gatus!\"\n }\n}")},
DontResolveFailedConditions: false,
ExpectedSuccess: true,
ExpectedOutput: "has([BODY].article) == true",
},
{
Name: "has-failure",
Condition: Condition("has([BODY].errors) == false"),

35
go.mod
View File

@@ -3,32 +3,37 @@ module github.com/TwiN/gatus/v5
go 1.19
require (
github.com/TwiN/deepmerge v0.2.0
github.com/TwiN/g8 v1.4.0
github.com/TwiN/gocache/v2 v2.2.0
github.com/TwiN/health v1.5.0
github.com/TwiN/health v1.6.0
github.com/TwiN/whois v1.1.0
github.com/coreos/go-oidc/v3 v3.4.0
github.com/coreos/go-oidc/v3 v3.5.0
github.com/go-ping/ping v0.0.0-20210911151512-381826476871
github.com/google/go-github/v48 v48.2.0
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062
github.com/lib/pq v1.10.7
github.com/miekg/dns v1.1.43
github.com/miekg/dns v1.1.50
github.com/prometheus/client_golang v1.14.0
github.com/wcharczuk/go-chart/v2 v2.1.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/oauth2 v0.4.0
gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v2 v2.4.0
modernc.org/sqlite v1.19.5
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.20.3
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062
github.com/google/go-querystring v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
@@ -37,20 +42,18 @@ require (
github.com/prometheus/procfs v0.8.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
golang.org/x/tools v0.1.7 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.21.5 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
modernc.org/opt v0.1.3 // indirect

314
go.sum
View File

@@ -13,36 +13,15 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -52,17 +31,17 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/TwiN/deepmerge v0.2.0 h1:8P1tp2qDXllX6V1Ailg2XA074easAcvLMmW9v1jn0aE=
github.com/TwiN/deepmerge v0.2.0/go.mod h1:cR9OWsvI13y+FxrbnPLuF6BX2tbYeOjkiI6JWjEGqAE=
github.com/TwiN/g8 v1.4.0 h1:RUk5xTtxKCdMo0GGSbBVyjtAAfi2nqVbA9E0C4u5Cxo=
github.com/TwiN/g8 v1.4.0/go.mod h1:ECyGJsoIb99klUfvVQoS1StgRLte9yvvPigGrHdy284=
github.com/TwiN/gocache/v2 v2.2.0 h1:M3B36KyH24BntxLrLaUb2kgTdq8DzCnfod0IekLG57w=
github.com/TwiN/gocache/v2 v2.2.0/go.mod h1:SnUuBsrwGQeNcDG6vhkOMJnqErZM0JGjgIkuKryokYA=
github.com/TwiN/health v1.5.0 h1:ETTtbQfUbiiIiVTSpAiNzesHQvm8qarV/8ctlZsVhwA=
github.com/TwiN/health v1.5.0/go.mod h1:Z6TszwQPMvtSiVx1QMidVRgvVr4KZGfiwqcD7/Z+3iw=
github.com/TwiN/health v1.6.0 h1:L2ks575JhRgQqWWOfKjw9B0ec172hx7GdToqkYUycQM=
github.com/TwiN/health v1.6.0/go.mod h1:Z6TszwQPMvtSiVx1QMidVRgvVr4KZGfiwqcD7/Z+3iw=
github.com/TwiN/whois v1.1.0 h1:lhyrC/9yIXntEnbJ+0IBy9Z5NBcreieYyamlvniwq88=
github.com/TwiN/whois v1.1.0/go.mod h1:9WbCzYlR+r5eq9vbgJVh7A4H2uR2ct4wKEB0/QITJ/c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -70,13 +49,11 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -85,34 +62,22 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-oidc/v3 v3.4.0 h1:xz7elHb/LDwm/ERpwHd+5nb7wFHL32rsr6bBOgaeu6g=
github.com/coreos/go-oidc/v3 v3.4.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw=
github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw=
github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -138,8 +103,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -155,10 +118,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -169,18 +130,17 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-github/v48 v48.2.0 h1:68puzySE6WqUY9KWmpOsDEQfDZsso98rT6pZcz9HqcE=
github.com/google/go-github/v48 v48.2.0/go.mod h1:dDlehKBDo850ZPvCTK0sEqTCVWcrGl2LcDiajkYi89Y=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -188,35 +148,18 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062 h1:G1+wBT0dwjIrBdLy0MIG0i+E4CQxEnedHXdauJEIH6g=
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@@ -246,8 +189,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@@ -288,18 +231,15 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -308,23 +248,22 @@ github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -350,8 +289,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
@@ -360,11 +297,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -393,47 +328,28 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 h1:2o1E+E8TpNLklK9nHiPiK1uzIYrIHt+cQx3ynCwq9V8=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M=
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -442,11 +358,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -478,57 +393,34 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -572,27 +464,13 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -609,29 +487,6 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -663,61 +518,12 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -730,26 +536,6 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -762,7 +548,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
@@ -776,18 +561,15 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -803,16 +585,16 @@ modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI=
modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.19.5 h1:E3iHL55c1Vw1knqIeU9N7B0fSjuiOjHZo7iVMsO6U5U=
modernc.org/sqlite v1.19.5/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs=
modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=

View File

@@ -93,7 +93,7 @@ func extractValue(currentKey string, value interface{}) interface{} {
currentKeyWithoutIndex := currentKey[:startOfBracket]
// if currentKeyWithoutIndex contains only an index (i.e. [0] or 0)
if len(currentKeyWithoutIndex) == 0 {
array := value.([]interface{})
array, _ := value.([]interface{})
if len(array) > arrayIndex {
if isNestedArray {
return extractValue(currentKey[endOfBracket+1:], array[arrayIndex])
@@ -106,7 +106,7 @@ func extractValue(currentKey string, value interface{}) interface{} {
return nil
}
// if currentKeyWithoutIndex contains both a key and an index (i.e. data[0])
array := value.(map[string]interface{})[currentKeyWithoutIndex].([]interface{})
array, _ := value.(map[string]interface{})[currentKeyWithoutIndex].([]interface{})
if len(array) > arrayIndex {
if isNestedArray {
return extractValue(currentKey[endOfBracket+1:], array[arrayIndex])

19
main.go
View File

@@ -52,14 +52,15 @@ func save() {
}
}
func loadConfiguration() (cfg *config.Config, err error) {
customConfigFile := os.Getenv("GATUS_CONFIG_FILE")
if len(customConfigFile) > 0 {
cfg, err = config.Load(customConfigFile)
} else {
cfg, err = config.LoadDefaultConfiguration()
func loadConfiguration() (*config.Config, error) {
configPath := os.Getenv("GATUS_CONFIG_PATH")
// Backwards compatibility
if len(configPath) == 0 {
if configPath = os.Getenv("GATUS_CONFIG_FILE"); len(configPath) > 0 {
log.Println("WARNING: GATUS_CONFIG_FILE is deprecated. Please use GATUS_CONFIG_PATH instead.")
}
}
return
return config.LoadConfiguration(configPath)
}
// initializeStorage initializes the storage provider
@@ -79,14 +80,14 @@ func initializeStorage(cfg *config.Config) {
}
numberOfEndpointStatusesDeleted := store.Get().DeleteAllEndpointStatusesNotInKeys(keys)
if numberOfEndpointStatusesDeleted > 0 {
log.Printf("[config][validateStorageConfig] Deleted %d endpoint statuses because their matching endpoints no longer existed", numberOfEndpointStatusesDeleted)
log.Printf("[main][initializeStorage] Deleted %d endpoint statuses because their matching endpoints no longer existed", numberOfEndpointStatusesDeleted)
}
}
func listenToConfigurationFileChanges(cfg *config.Config) {
for {
time.Sleep(30 * time.Second)
if cfg.HasLoadedConfigurationFileBeenModified() {
if cfg.HasLoadedConfigurationBeenModified() {
log.Println("[main][listenToConfigurationFileChanges] Configuration file has been modified")
stop()
time.Sleep(time.Second) // Wait a bit to make sure everything is done.

View File

@@ -97,7 +97,7 @@ func TestNewStore(t *testing.T) {
func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpOldUptimeEntriesProperly.db", false)
defer store.Close()
now := time.Now().Round(time.Minute)
now := time.Now().Truncate(time.Hour)
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
store.Insert(&testEndpoint, &core.Result{Timestamp: now.Add(-5 * time.Hour), Success: true})

View File

@@ -1,6 +1,7 @@
package store
import (
"path/filepath"
"testing"
"time"
@@ -566,6 +567,7 @@ func TestGet(t *testing.T) {
}
func TestInitialize(t *testing.T) {
dir := t.TempDir()
type Scenario struct {
Name string
Cfg *storage.Config
@@ -594,7 +596,7 @@ func TestInitialize(t *testing.T) {
},
{
Name: "sqlite-with-path",
Cfg: &storage.Config{Type: storage.TypeSQLite, Path: t.TempDir() + "/TestInitialize_sqlite-with-path.db"},
Cfg: &storage.Config{Type: storage.TypeSQLite, Path: filepath.Join(dir, "TestInitialize_sqlite-with-path.db")},
ExpectedErr: nil,
},
}
@@ -629,7 +631,7 @@ func TestInitialize(t *testing.T) {
}
func TestAutoSave(t *testing.T) {
file := t.TempDir() + "/TestAutoSave.db"
file := filepath.Join(t.TempDir(), "/TestAutoSave.db")
if err := Initialize(&storage.Config{Path: file}); err != nil {
t.Fatal("shouldn't have returned an error")
}

View File

@@ -1 +0,0 @@
* text=lf

View File

@@ -1,2 +0,0 @@
.idea
*.iml

21
vendor/github.com/TwiN/g8/LICENSE generated vendored
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 TwiN
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

287
vendor/github.com/TwiN/g8/README.md generated vendored
View File

@@ -1,287 +0,0 @@
# g8
![test](https://github.com/TwiN/g8/workflows/test/badge.svg?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/TwiN/g8)](https://goreportcard.com/report/github.com/TwiN/g8)
[![codecov](https://codecov.io/gh/TwiN/g8/branch/master/graph/badge.svg)](https://codecov.io/gh/TwiN/g8)
[![Go version](https://img.shields.io/github/go-mod/go-version/TwiN/g8.svg)](https://github.com/TwiN/g8)
[![Go Reference](https://pkg.go.dev/badge/github.com/TwiN/g8.svg)](https://pkg.go.dev/github.com/TwiN/g8)
[![Follow TwiN](https://img.shields.io/github/followers/TwiN?label=Follow&style=social)](https://github.com/TwiN)
g8, pronounced gate, is a simple Go library for protecting HTTP handlers.
Tired of constantly re-implementing a security layer for each application? Me too, that's why I made g8.
## Installation
```console
go get -u github.com/TwiN/g8
```
## Usage
Because the entire purpose of g8 is to NOT waste time configuring the layer of security, the primary emphasis is to
keep it as simple as possible.
### Simple
Just want a simple layer of security without the need for advanced permissions? This configuration is what you're
looking for.
```go
authorizationService := g8.NewAuthorizationService().WithToken("mytoken")
gate := g8.New().WithAuthorizationService(authorizationService)
router := http.NewServeMux()
router.Handle("/unprotected", yourHandler)
router.Handle("/protected", gate.Protect(yourHandler))
http.ListenAndServe(":8080", router)
```
The endpoint `/protected` is now only accessible if you pass the header `Authorization: Bearer mytoken`.
If you use `http.HandleFunc` instead of `http.Handle`, you may use `gate.ProtectFunc(yourHandler)` instead.
If you're not using the `Authorization` header, you can specify a custom token extractor.
This enables use cases like [Protecting a handler using session cookie](#protecting-a-handler-using-session-cookie)
### Advanced permissions
If you have tokens with more permissions than others, g8's permission system will make managing authorization a breeze.
Rather than registering tokens, think of it as registering clients, the only difference being that clients may be
configured with permissions while tokens cannot.
```go
authorizationService := g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken").WithPermission("admin"))
gate := g8.New().WithAuthorizationService(authorizationService)
router := http.NewServeMux()
router.Handle("/unprotected", yourHandler)
router.Handle("/protected-with-admin", gate.ProtectWithPermissions(yourHandler, []string{"admin"}))
http.ListenAndServe(":8080", router)
```
The endpoint `/protected-with-admin` is now only accessible if you pass the header `Authorization: Bearer mytoken`,
because the client with the token `mytoken` has the permission `admin`. Note that the following handler would also be
accessible with that token:
```go
router.Handle("/protected", gate.Protect(yourHandler))
```
To clarify, both clients and tokens have access to handlers that aren't protected with extra permissions, and
essentially, tokens are registered as clients with no extra permissions in the background.
Creating a token like so:
```go
authorizationService := g8.NewAuthorizationService().WithToken("mytoken")
```
is the equivalent of creating the following client:
```go
authorizationService := g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken"))
```
### With client provider
A client provider's task is to retrieve a Client from an external source (e.g. a database) when provided with a token.
You should use a client provider when you have a lot of tokens and it wouldn't make sense to register all of them using
`AuthorizationService`'s `WithToken`/`WithTokens`/`WithClient`/`WithClients`.
Note that the provider is used as a fallback source. As such, if a token is explicitly registered using one of the 4
aforementioned functions, the client provider will not be used.
```go
clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
// We'll assume that the following function calls your database and returns a struct "User" that
// has the user's token as well as the permissions granted to said user
user := database.GetUserByToken(token)
if user != nil {
return g8.NewClient(user.Token).WithPermissions(user.Permissions)
}
return nil
})
authorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)
gate := g8.New().WithAuthorizationService(authorizationService)
```
You can also configure the client provider to cache the output of the function you provide to retrieve clients by token:
```go
clientProvider := g8.NewClientProvider(...).WithCache(ttl, maxSize)
```
Since g8 leverages [TwiN/gocache](https://github.com/TwiN/gocache), you can also use gocache's
constants for configuring the TTL and the maximum size:
- Setting the TTL to `gocache.NoExpiration` (-1) will disable the TTL.
- Setting the maximum size to `gocache.NoMaxSize` (0) will disable the maximum cache size
If you're using a TTL and have a lot of tokens (100k+), you may want to use `clientProvider.StartJanitor()` to allow
the cache to passively delete expired entries. If you have to re-initialize the client provider after the janitor has
been started, make sure to stop the janitor first (`clientProvider.StopJanitor()`). This is because the janitor runs on
a separate goroutine, thus, if you were to re-create a client provider and re-assign it, the old client provider would
still exist in memory with the old cache. I'm only specifying this for completeness, because for the overwhelming
majority of people, the gate will be created on application start and never modified again until the application shuts
down, in which case, you don't even need to worry about stopping the janitor.
To avoid any misunderstandings, using a client provider is not mandatory. If you only have a few tokens and you can load
them on application start, you can just leverage `AuthorizationService`'s `WithToken`/`WithTokens`/`WithClient`/`WithClients`.
## AuthorizationService
As the previous examples may have hinted, there are several ways to create clients. The one thing they have
in common is that they all go through AuthorizationService, which is in charge of both managing clients and determining
whether a request should be blocked or allowed through.
| Function | Description |
|:-------------------|:---------------------------------------------------------------------------------------------------------------------------------|
| WithToken | Creates a single static client with no extra permissions |
| WithTokens | Creates a slice of static clients with no extra permissions |
| WithClient | Creates a single static client |
| WithClients | Creates a slice of static clients |
| WithClientProvider | Creates a client provider which will allow a fallback to a dynamic source (e.g. to a database) when a static client is not found |
Except for `WithClientProvider`, every functions listed above can be called more than once.
As a result, you may safely perform actions like this:
```go
authorizationService := g8.NewAuthorizationService().
WithToken("123").
WithToken("456").
WithClient(g8.NewClient("789").WithPermission("admin"))
gate := g8.New().WithAuthorizationService(authorizationService)
```
Be aware that g8.Client supports a list of permissions as well. You may call `WithPermission` several times, or call
`WithPermissions` with a slice of permissions instead.
### Permissions
Unlike client permissions, handler permissions are requirements.
A client may have as many permissions as you want, but for said client to have access to a handler protected by
permissions, the client must have all permissions defined by said handler in order to have access to it.
In other words, a client with the permissions `create`, `read`, `update` and `delete` would have access to all of these handlers:
```go
gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken").WithPermissions([]string{"create", "read", "update", "delete"})))
router := http.NewServeMux()
router.Handle("/", gate.Protect(homeHandler)) // equivalent of gate.ProtectWithPermissions(homeHandler, []string{})
router.Handle("/create", gate.ProtectWithPermissions(createHandler, []string{"create"}))
router.Handle("/read", gate.ProtectWithPermissions(readHandler, []string{"read"}))
router.Handle("/update", gate.ProtectWithPermissions(updateHandler, []string{"update"}))
router.Handle("/delete", gate.ProtectWithPermissions(deleteHandler, []string{"delete"}))
router.Handle("/crud", gate.ProtectWithPermissions(crudHandler, []string{"create", "read", "update", "delete"}))
```
But it would not have access to the following handler, because while `mytoken` has the `read` permission, it does not
have the `backup` permission:
```go
router.Handle("/backup", gate.ProtectWithPermissions(&testHandler{}, []string{"read", "backup"}))
```
If you're using an HTTP library that supports middlewares like [mux](https://github.com/gorilla/mux), you can protect
an entire group of handlers instead using `gate.Protect` or `gate.PermissionMiddleware()`:
```go
router := mux.NewRouter()
userRouter := router.PathPrefix("/").Subrouter()
userRouter.Use(gate.Protect)
userRouter.HandleFunc("/api/v1/users/me", getUserProfile).Methods("GET")
userRouter.HandleFunc("/api/v1/users/me/friends", getUserFriends).Methods("GET")
userRouter.HandleFunc("/api/v1/users/me/email", updateUserEmail).Methods("PATCH")
adminRouter := router.PathPrefix("/").Subrouter()
adminRouter.Use(gate.PermissionMiddleware("admin"))
adminRouter.HandleFunc("/api/v1/users/{id}/ban", banUserByID).Methods("POST")
adminRouter.HandleFunc("/api/v1/users/{id}/delete", deleteUserByID).Methods("DELETE")
```
## Rate limiting
To add a rate limit of 100 requests per second:
```go
gate := g8.New().WithRateLimit(100)
```
## Accessing the token from the protected handlers
If you need to access the token from the handlers you are protecting with g8, you can retrieve it from the
request context by using the key `g8.TokenContextKey`:
```go
http.Handle("/handle", gate.ProtectFunc(func(w http.ResponseWriter, r *http.Request) {
token, _ := r.Context().Value(g8.TokenContextKey).(string)
// ...
}))
```
## Examples
### Protecting a handler using session cookie
If you want to only allow authenticated users to access a handler, you can use a custom token extractor function
combined with a client provider.
First, we'll create a function to extract the session ID from the session cookie. While a session ID does not
theoretically refer to a token, g8 uses the term `token` as a blanket term to refer to any string that can be used to
identify a client.
```go
customTokenExtractorFunc := func(request *http.Request) string {
sessionCookie, err := request.Cookie("session")
if err != nil {
return ""
}
return sessionCookie.Value
}
```
Next, we need to create a client provider that will validate our token, which refers to the session ID in this case.
```go
clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
// We'll assume that the following function calls your database and validates whether the session is valid.
isSessionValid := database.CheckIfSessionIsValid(token)
if !isSessionValid {
return nil // Returning nil will cause the gate to return a 401 Unauthorized.
}
// You could also retrieve the user and their permissions if you wanted instead, but for this example,
// all we care about is confirming whether the session is valid or not.
return g8.NewClient(token)
})
```
Keep in mind that you can get really creative with the client provider above.
For instance, you could refresh the session's expiration time, which will allow the user to stay logged in for
as long as they're active.
You're also not limited to using something stateful like the example above. You could use a JWT and have your client
provider validate said JWT.
Finally, we can create the authorization service and the gate:
```go
authorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)
gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
```
If you need to access the token (session ID in this case) from the protected handlers, you can retrieve it from the
request context by using the key `g8.TokenContextKey`:
```go
http.Handle("/handle", gate.ProtectFunc(func(w http.ResponseWriter, r *http.Request) {
sessionID, _ := r.Context().Value(g8.TokenContextKey).(string)
// ...
}))
```
### Using a custom header
The logic is the same as the example above:
```go
customTokenExtractorFunc := func(request *http.Request) string {
return request.Header.Get("X-API-Token")
}
clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
// We'll assume that the following function calls your database and returns a struct "User" that
// has the user's token as well as the permissions granted to said user
user := database.GetUserByToken(token)
if user != nil {
return g8.NewClient(user.Token).WithPermissions(user.Permissions)
}
return nil
})
authorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)
gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
```

View File

@@ -1,122 +0,0 @@
package g8
import (
"sync"
)
// AuthorizationService is the service that manages client/token registry and client fallback as well as the service
// that determines whether a token meets the specific requirements to be authorized by a Gate or not.
type AuthorizationService struct {
clients map[string]*Client
clientProvider *ClientProvider
mutex sync.RWMutex
}
// NewAuthorizationService creates a new AuthorizationService
func NewAuthorizationService() *AuthorizationService {
return &AuthorizationService{
clients: make(map[string]*Client),
}
}
// WithToken is used to specify a single token for which authorization will be granted
//
// The client that will be created from this token will have access to all handlers that are not protected with a
// specific permission.
//
// In other words, if you were to do the following:
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("12345"))
//
// The following handler would be accessible with the token 12345:
// router.Handle("/1st-handler", gate.Protect(yourHandler))
//
// But not this one would not be accessible with the token 12345:
// router.Handle("/2nd-handler", gate.ProtectWithPermissions(yourOtherHandler, []string{"admin"}))
//
// Calling this function multiple times will add multiple clients, though you may want to use WithTokens instead
// if you plan to add multiple clients
//
// If you wish to configure advanced permissions, consider using WithClient instead.
//
func (authorizationService *AuthorizationService) WithToken(token string) *AuthorizationService {
authorizationService.mutex.Lock()
authorizationService.clients[token] = NewClient(token)
authorizationService.mutex.Unlock()
return authorizationService
}
// WithTokens is used to specify a slice of tokens for which authorization will be granted
func (authorizationService *AuthorizationService) WithTokens(tokens []string) *AuthorizationService {
authorizationService.mutex.Lock()
for _, token := range tokens {
authorizationService.clients[token] = NewClient(token)
}
authorizationService.mutex.Unlock()
return authorizationService
}
// WithClient is used to specify a single client for which authorization will be granted
//
// When compared to WithToken, the advantage of using this function is that you may specify the client's
// permissions and thus, be a lot more granular with what endpoint a token has access to.
//
// In other words, if you were to do the following:
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("12345").WithPermission("mod")))
//
// The following handlers would be accessible with the token 12345:
// router.Handle("/1st-handler", gate.ProtectWithPermissions(yourHandler, []string{"mod"}))
// router.Handle("/2nd-handler", gate.Protect(yourOtherHandler))
//
// But not this one, because the user does not have the permission "admin":
// router.Handle("/3rd-handler", gate.ProtectWithPermissions(yetAnotherHandler, []string{"admin"}))
//
// Calling this function multiple times will add multiple clients, though you may want to use WithClients instead
// if you plan to add multiple clients
func (authorizationService *AuthorizationService) WithClient(client *Client) *AuthorizationService {
authorizationService.mutex.Lock()
authorizationService.clients[client.Token] = client
authorizationService.mutex.Unlock()
return authorizationService
}
// WithClients is used to specify a slice of clients for which authorization will be granted
func (authorizationService *AuthorizationService) WithClients(clients []*Client) *AuthorizationService {
authorizationService.mutex.Lock()
for _, client := range clients {
authorizationService.clients[client.Token] = client
}
authorizationService.mutex.Unlock()
return authorizationService
}
// WithClientProvider allows specifying a custom provider to fetch clients by token.
//
// For example, you can use it to fallback to making a call in your database when a request is made with a token that
// hasn't been specified via WithToken, WithTokens, WithClient or WithClients.
func (authorizationService *AuthorizationService) WithClientProvider(provider *ClientProvider) *AuthorizationService {
authorizationService.clientProvider = provider
return authorizationService
}
// IsAuthorized checks whether a client with a given token exists and has the permissions required.
//
// If permissionsRequired is nil or empty and a client with the given token exists, said client will have access to all
// handlers that are not protected by a given permission.
func (authorizationService *AuthorizationService) IsAuthorized(token string, permissionsRequired []string) bool {
if len(token) == 0 {
return false
}
authorizationService.mutex.RLock()
client, _ := authorizationService.clients[token]
authorizationService.mutex.RUnlock()
// If there's no clients with the given token directly stored in the AuthorizationService, fall back to the
// client provider, if there's one configured.
if client == nil && authorizationService.clientProvider != nil {
client = authorizationService.clientProvider.GetClientByToken(token)
}
if client != nil {
return client.HasPermissions(permissionsRequired)
}
return false
}

58
vendor/github.com/TwiN/g8/client.go generated vendored
View File

@@ -1,58 +0,0 @@
package g8
// Client is a struct containing both a Token and a slice of extra Permissions that said token has.
type Client struct {
// Token is the value used to authenticate with the API.
Token string
// Permissions is a slice of extra permissions that may be used for more granular access control.
//
// If you only wish to use Gate.Protect and Gate.ProtectFunc, you do not have to worry about this,
// since they're only used by Gate.ProtectWithPermissions and Gate.ProtectFuncWithPermissions
Permissions []string
}
// NewClient creates a Client with a given token
func NewClient(token string) *Client {
return &Client{
Token: token,
}
}
// NewClientWithPermissions creates a Client with a slice of permissions
// Equivalent to using NewClient and WithPermissions
func NewClientWithPermissions(token string, permissions []string) *Client {
return NewClient(token).WithPermissions(permissions)
}
// WithPermissions adds a slice of permissions to a client
func (client *Client) WithPermissions(permissions []string) *Client {
client.Permissions = append(client.Permissions, permissions...)
return client
}
// WithPermission adds a permission to a client
func (client *Client) WithPermission(permission string) *Client {
client.Permissions = append(client.Permissions, permission)
return client
}
// HasPermission checks whether a client has a given permission
func (client Client) HasPermission(permissionRequired string) bool {
for _, permission := range client.Permissions {
if permissionRequired == permission {
return true
}
}
return false
}
// HasPermissions checks whether a client has the all permissions passed
func (client Client) HasPermissions(permissionsRequired []string) bool {
for _, permissionRequired := range permissionsRequired {
if !client.HasPermission(permissionRequired) {
return false
}
}
return true
}

View File

@@ -1,140 +0,0 @@
package g8
import (
"errors"
"time"
"github.com/TwiN/gocache/v2"
)
var (
// ErrNoExpiration is the error returned by ClientProvider.StartCacheJanitor if there was an attempt to start the
// janitor despite no expiration being configured.
// To clarify, this is because the cache janitor is only useful when an expiration is set.
ErrNoExpiration = errors.New("no point starting the janitor if the TTL is set to not expire")
// ErrCacheNotInitialized is the error returned by ClientProvider.StartCacheJanitor if there was an attempt to start
// the janitor despite the cache not having been initialized using ClientProvider.WithCache
ErrCacheNotInitialized = errors.New("cannot start janitor because cache is not configured")
)
// ClientProvider has the task of retrieving a Client from an external source (e.g. a database) when provided with a
// token. It should be used when you have a lot of tokens, and it wouldn't make sense to register all of them using
// AuthorizationService's WithToken, WithTokens, WithClient or WithClients.
//
// Note that the provider is used as a fallback source. As such, if a token is explicitly registered using one of the 4
// aforementioned functions, the client provider will not be used by the AuthorizationService when a request is made
// with said token. It will, however, be called upon if a token that is not explicitly registered in
// AuthorizationService is sent alongside a request going through the Gate.
//
// clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
// // We'll assume that the following function calls your database and returns a struct "User" that
// // has the user's token as well as the permissions granted to said user
// user := database.GetUserByToken(token)
// if user != nil {
// return g8.NewClient(user.Token).WithPermissions(user.Permissions)
// }
// return nil
// })
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClientProvider(clientProvider))
//
type ClientProvider struct {
getClientByTokenFunc func(token string) *Client
cache *gocache.Cache
ttl time.Duration
}
// NewClientProvider creates a ClientProvider
// The parameter that must be passed is a function that the provider will use to retrieve a client by a given token
//
// Example:
// clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
// // We'll assume that the following function calls your database and returns a struct "User" that
// // has the user's token as well as the permissions granted to said user
// user := database.GetUserByToken(token)
// if user == nil {
// return nil
// }
// return g8.NewClient(user.Token).WithPermissions(user.Permissions)
// })
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClientProvider(clientProvider))
//
func NewClientProvider(getClientByTokenFunc func(token string) *Client) *ClientProvider {
return &ClientProvider{
getClientByTokenFunc: getClientByTokenFunc,
}
}
// WithCache adds cache options to the ClientProvider.
//
// ttl is the time until the cache entry will expire. A TTL of gocache.NoExpiration (-1) means no expiration
// maxSize is the maximum amount of entries that can be in the cache at any given time.
// If a value of gocache.NoMaxSize (0) or less is provided for maxSize, there will be no maximum size.
//
// Example:
// clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
// // We'll assume that the following function calls your database and returns a struct "User" that
// // has the user's token as well as the permissions granted to said user
// user := database.GetUserByToken(token)
// if user != nil {
// return g8.NewClient(user.Token).WithPermissions(user.Permissions)
// }
// return nil
// })
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClientProvider(clientProvider.WithCache(time.Hour, 70000)))
//
func (provider *ClientProvider) WithCache(ttl time.Duration, maxSize int) *ClientProvider {
provider.cache = gocache.NewCache().WithEvictionPolicy(gocache.LeastRecentlyUsed).WithMaxSize(maxSize)
provider.ttl = ttl
return provider
}
// StartCacheJanitor starts the cache janitor, which passively deletes expired cache entries in the background.
//
// Not really necessary unless you have a lot of clients (100000+).
//
// Even without the janitor, active eviction will still happen (i.e. when GetClientByToken is called, but the cache
// entry for the given token has expired, the cache entry will be automatically deleted and re-fetched from the
// user-defined getClientByTokenFunc)
func (provider *ClientProvider) StartCacheJanitor() error {
if provider.cache == nil {
// Can't start the cache janitor if there's no cache
return ErrCacheNotInitialized
}
if provider.ttl != gocache.NoExpiration {
return provider.cache.StartJanitor()
}
return ErrNoExpiration
}
// StopCacheJanitor stops the cache janitor
//
// Not required unless your application initializes multiple providers over the course of its lifecycle.
// In English, that means if you initialize a ClientProvider only once on application start and it stays up
// until your application shuts down, you don't need to call this function.
func (provider *ClientProvider) StopCacheJanitor() {
if provider.cache != nil {
provider.cache.StopJanitor()
}
}
// GetClientByToken retrieves a client by its token through the provided getClientByTokenFunc.
func (provider *ClientProvider) GetClientByToken(token string) *Client {
if provider.cache == nil {
return provider.getClientByTokenFunc(token)
}
if cachedClient, exists := provider.cache.Get(token); exists {
if cachedClient == nil {
return nil
}
// Safely typecast the client.
// Regardless of whether the typecast is successful or not, we return client since it'll be either client or
// nil. Technically, it should never be nil, but it's better to be safe than sorry.
client, _ := cachedClient.(*Client)
return client
}
client := provider.getClientByTokenFunc(token)
provider.cache.SetWithTTL(token, client, provider.ttl)
return client
}

238
vendor/github.com/TwiN/g8/gate.go generated vendored
View File

@@ -1,238 +0,0 @@
package g8
import (
"context"
"net/http"
"strings"
)
const (
// AuthorizationHeader is the header in which g8 looks for the authorization bearer token
AuthorizationHeader = "Authorization"
// DefaultUnauthorizedResponseBody is the default response body returned if a request was sent with a missing or invalid token
DefaultUnauthorizedResponseBody = "token is missing or invalid"
// DefaultTooManyRequestsResponseBody is the default response body returned if a request exceeded the allowed rate limit
DefaultTooManyRequestsResponseBody = "too many requests"
// TokenContextKey is the key used to store the token in the context.
TokenContextKey = "g8.token"
)
// Gate is lock to the front door of your API, letting only those you allow through.
type Gate struct {
authorizationService *AuthorizationService
unauthorizedResponseBody []byte
customTokenExtractorFunc func(request *http.Request) string
rateLimiter *RateLimiter
tooManyRequestsResponseBody []byte
}
// Deprecated: use New instead.
func NewGate(authorizationService *AuthorizationService) *Gate {
return &Gate{
authorizationService: authorizationService,
unauthorizedResponseBody: []byte(DefaultUnauthorizedResponseBody),
tooManyRequestsResponseBody: []byte(DefaultTooManyRequestsResponseBody),
}
}
// New creates a new Gate.
func New() *Gate {
return &Gate{
unauthorizedResponseBody: []byte(DefaultUnauthorizedResponseBody),
tooManyRequestsResponseBody: []byte(DefaultTooManyRequestsResponseBody),
}
}
// WithAuthorizationService sets the authorization service to use.
//
// If there is no authorization service, Gate will not enforce authorization.
func (gate *Gate) WithAuthorizationService(authorizationService *AuthorizationService) *Gate {
gate.authorizationService = authorizationService
return gate
}
// WithCustomUnauthorizedResponseBody sets a custom response body when Gate determines that a request must be blocked
func (gate *Gate) WithCustomUnauthorizedResponseBody(unauthorizedResponseBody []byte) *Gate {
gate.unauthorizedResponseBody = unauthorizedResponseBody
return gate
}
// WithCustomTokenExtractor allows the specification of a custom function to extract a token from a request.
// If a custom token extractor is not specified, the token will be extracted from the Authorization header.
//
// For instance, if you're using a session cookie, you can extract the token from the cookie like so:
//
// authorizationService := g8.NewAuthorizationService()
// customTokenExtractorFunc := func(request *http.Request) string {
// sessionCookie, err := request.Cookie("session")
// if err != nil {
// return ""
// }
// return sessionCookie.Value
// }
// gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
//
// You would normally use this with a client provider that matches whatever need you have.
// For example, if you're using a session cookie, your client provider would retrieve the user from the session ID
// extracted by this custom token extractor.
//
// Note that for the sake of convenience, the token extracted from the request is passed the protected handlers request
// context under the key TokenContextKey. This is especially useful if the token is in fact a session ID.
func (gate *Gate) WithCustomTokenExtractor(customTokenExtractorFunc func(request *http.Request) string) *Gate {
gate.customTokenExtractorFunc = customTokenExtractorFunc
return gate
}
// WithRateLimit adds rate limiting to the Gate
//
// If you just want to use a gate for rate limiting purposes:
//
// gate := g8.New().WithRateLimit(50)
func (gate *Gate) WithRateLimit(maximumRequestsPerSecond int) *Gate {
gate.rateLimiter = NewRateLimiter(maximumRequestsPerSecond)
return gate
}
// Protect secures a handler, requiring requests going through to have a valid Authorization Bearer token.
// Unlike ProtectWithPermissions, Protect will allow access to any registered tokens, regardless of their permissions
// or lack thereof.
//
// Example:
//
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("token"))
// router := http.NewServeMux()
// // Without protection
// router.Handle("/handle", yourHandler)
// // With protection
// router.Handle("/handle", gate.Protect(yourHandler))
//
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
func (gate *Gate) Protect(handler http.Handler) http.Handler {
return gate.ProtectWithPermissions(handler, nil)
}
// ProtectWithPermissions secures a handler, requiring requests going through to have a valid Authorization Bearer token
// as well as a slice of permissions that must be met.
//
// Example:
//
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("ADMIN")))
// router := http.NewServeMux()
// // Without protection
// router.Handle("/handle", yourHandler)
// // With protection
// router.Handle("/handle", gate.ProtectWithPermissions(yourHandler, []string{"admin"}))
//
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
func (gate *Gate) ProtectWithPermissions(handler http.Handler, permissions []string) http.Handler {
return gate.ProtectFuncWithPermissions(func(writer http.ResponseWriter, request *http.Request) {
handler.ServeHTTP(writer, request)
}, permissions)
}
// ProtectWithPermission does the same thing as ProtectWithPermissions, but for a single permission instead of a
// slice of permissions
//
// See ProtectWithPermissions for further documentation
func (gate *Gate) ProtectWithPermission(handler http.Handler, permission string) http.Handler {
return gate.ProtectFuncWithPermissions(func(writer http.ResponseWriter, request *http.Request) {
handler.ServeHTTP(writer, request)
}, []string{permission})
}
// ProtectFunc secures a handlerFunc, requiring requests going through to have a valid Authorization Bearer token.
// Unlike ProtectFuncWithPermissions, ProtectFunc will allow access to any registered tokens, regardless of their
// permissions or lack thereof.
//
// Example:
//
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("token"))
// router := http.NewServeMux()
// // Without protection
// router.HandleFunc("/handle", yourHandlerFunc)
// // With protection
// router.HandleFunc("/handle", gate.ProtectFunc(yourHandlerFunc))
//
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
func (gate *Gate) ProtectFunc(handlerFunc http.HandlerFunc) http.HandlerFunc {
return gate.ProtectFuncWithPermissions(handlerFunc, nil)
}
// ProtectFuncWithPermissions secures a handler, requiring requests going through to have a valid Authorization Bearer
// token as well as a slice of permissions that must be met.
//
// Example:
//
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("admin")))
// router := http.NewServeMux()
// // Without protection
// router.HandleFunc("/handle", yourHandlerFunc)
// // With protection
// router.HandleFunc("/handle", gate.ProtectFuncWithPermissions(yourHandlerFunc, []string{"admin"}))
//
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
func (gate *Gate) ProtectFuncWithPermissions(handlerFunc http.HandlerFunc, permissions []string) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
if gate.rateLimiter != nil {
if !gate.rateLimiter.Try() {
writer.WriteHeader(http.StatusTooManyRequests)
_, _ = writer.Write(gate.tooManyRequestsResponseBody)
return
}
}
if gate.authorizationService != nil {
token := gate.ExtractTokenFromRequest(request)
if !gate.authorizationService.IsAuthorized(token, permissions) {
writer.WriteHeader(http.StatusUnauthorized)
_, _ = writer.Write(gate.unauthorizedResponseBody)
return
}
request = request.WithContext(context.WithValue(request.Context(), TokenContextKey, token))
}
handlerFunc(writer, request)
}
}
// ProtectFuncWithPermission does the same thing as ProtectFuncWithPermissions, but for a single permission instead of a
// slice of permissions
//
// See ProtectFuncWithPermissions for further documentation
func (gate *Gate) ProtectFuncWithPermission(handlerFunc http.HandlerFunc, permission string) http.HandlerFunc {
return gate.ProtectFuncWithPermissions(handlerFunc, []string{permission})
}
// ExtractTokenFromRequest extracts a token from a request.
//
// By default, it extracts the bearer token from the AuthorizationHeader, but if a customTokenExtractorFunc is defined,
// it will use that instead.
//
// Note that this method is internally used by Protect, ProtectWithPermission, ProtectFunc and
// ProtectFuncWithPermissions, but it is exposed in case you need to use it directly.
func (gate *Gate) ExtractTokenFromRequest(request *http.Request) string {
if gate.customTokenExtractorFunc != nil {
// A custom token extractor function is defined, so we'll use it instead of the default token extraction logic
return gate.customTokenExtractorFunc(request)
}
return strings.TrimPrefix(request.Header.Get(AuthorizationHeader), "Bearer ")
}
// PermissionMiddleware is a middleware that behaves like ProtectWithPermission, but it is meant to be used
// as a middleware for libraries that support such a feature.
//
// For instance, if you are using github.com/gorilla/mux, you can use PermissionMiddleware like so:
//
// router := mux.NewRouter()
// router.Use(gate.PermissionMiddleware("admin"))
// router.Handle("/admin/handle", adminHandler)
//
// If you do not want to protect a router with a specific permission, you can use Gate.Protect instead.
func (gate *Gate) PermissionMiddleware(permissions ...string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return gate.ProtectWithPermissions(next, permissions)
}
}

View File

@@ -1,42 +0,0 @@
package g8
import (
"sync"
"time"
)
// RateLimiter is a fixed rate limiter
type RateLimiter struct {
maximumExecutionsPerSecond int
executionsLeftInWindow int
windowStartTime time.Time
mutex sync.Mutex
}
// NewRateLimiter creates a RateLimiter
func NewRateLimiter(maximumExecutionsPerSecond int) *RateLimiter {
return &RateLimiter{
windowStartTime: time.Now(),
executionsLeftInWindow: maximumExecutionsPerSecond,
maximumExecutionsPerSecond: maximumExecutionsPerSecond,
}
}
// Try updates the number of executions if the rate limit quota hasn't been reached and returns whether the
// attempt was successful or not.
//
// Returns false if the execution was not successful (rate limit quota has been reached)
// Returns true if the execution was successful (rate limit quota has not been reached)
func (r *RateLimiter) Try() bool {
r.mutex.Lock()
defer r.mutex.Unlock()
if time.Now().Add(-time.Second).After(r.windowStartTime) {
r.windowStartTime = time.Now()
r.executionsLeftInWindow = r.maximumExecutionsPerSecond
}
if r.executionsLeftInWindow == 0 {
return false
}
r.executionsLeftInWindow--
return true
}

View File

@@ -1 +0,0 @@
* text=lf

View File

@@ -1 +0,0 @@
.idea

View File

@@ -1,9 +0,0 @@
MIT License
Copyright (c) 2022 TwiN
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,361 +0,0 @@
# gocache
![test](https://github.com/TwiN/gocache/workflows/test/badge.svg?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/TwiN/gocache)](https://goreportcard.com/report/github.com/TwiN/gocache)
[![codecov](https://codecov.io/gh/TwiN/gocache/branch/master/graph/badge.svg)](https://codecov.io/gh/TwiN/gocache)
[![Go version](https://img.shields.io/github/go-mod/go-version/TwiN/gocache.svg)](https://github.com/TwiN/gocache)
[![Go Reference](https://pkg.go.dev/badge/github.com/TwiN/gocache.svg)](https://pkg.go.dev/github.com/TwiN/gocache/v2)
[![Follow TwiN](https://img.shields.io/github/followers/TwiN?label=Follow&style=social)](https://github.com/TwiN)
gocache is an easy-to-use, high-performance, lightweight and thread-safe (goroutine-safe) in-memory key-value cache
with support for LRU and FIFO eviction policies as well as expiration, bulk operations and even retrieval of keys by pattern.
## Table of Contents
- [Features](#features)
- [Usage](#usage)
- [Initializing the cache](#initializing-the-cache)
- [Functions](#functions)
- [Examples](#examples)
- [Creating or updating an entry](#creating-or-updating-an-entry)
- [Getting an entry](#getting-an-entry)
- [Deleting an entry](#deleting-an-entry)
- [Complex example](#complex-example)
- [Persistence](#persistence)
- [Eviction](#eviction)
- [MaxSize](#maxsize)
- [MaxMemoryUsage](#maxmemoryusage)
- [Expiration](#expiration)
- [Performance](#performance)
- [Summary](#summary)
- [Results](#results)
- [FAQ](#faq)
- [How can I persist the data on application termination?](#how-can-i-persist-the-data-on-application-termination)
## Features
gocache supports the following cache eviction policies:
- First in first out (FIFO)
- Least recently used (LRU)
It also supports cache entry TTL, which is both active and passive. Active expiration means that if you attempt
to retrieve a cache key that has already expired, it will delete it on the spot and the behavior will be as if
the cache key didn't exist. As for passive expiration, there's a background task that will take care of deleting
expired keys.
It also includes what you'd expect from a cache, like GET/SET, bulk operations and get by pattern.
## Usage
```
go get -u github.com/TwiN/gocache/v2
```
### Initializing the cache
```go
cache := gocache.NewCache().WithMaxSize(1000).WithEvictionPolicy(gocache.LeastRecentlyUsed)
```
If you're planning on using expiration (`SetWithTTL` or `Expire`) and you want expired entries to be automatically deleted
in the background, make sure to start the janitor when you instantiate the cache:
```go
cache.StartJanitor()
```
### Functions
| Function | Description |
|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| WithMaxSize | Sets the max size of the cache. `gocache.NoMaxSize` means there is no limit. If not set, the default max size is `gocache.DefaultMaxSize`. |
| WithMaxMemoryUsage | Sets the max memory usage of the cache. `gocache.NoMaxMemoryUsage` means there is no limit. The default behavior is to not evict based on memory usage. |
| WithEvictionPolicy | Sets the eviction algorithm to be used when the cache reaches the max size. If not set, the default eviction policy is `gocache.FirstInFirstOut` (FIFO). |
| WithDefaultTTL | Sets the default TTL for each entry. |
| WithForceNilInterfaceOnNilPointer | Configures whether values with a nil pointer passed to write functions should be forcefully set to nil. Defaults to true. |
| StartJanitor | Starts the janitor, which is in charge of deleting expired cache entries in the background. |
| StopJanitor | Stops the janitor. |
| Set | Same as `SetWithTTL`, but using the default TTL (which is `gocache.NoExpiration`, unless configured otherwise). |
| SetWithTTL | Creates or updates a cache entry with the given key, value and expiration time. If the max size after the aforementioned operation is above the configured max size, the tail will be evicted. Depending on the eviction policy, the tail is defined as the oldest |
| SetAll | Same as `Set`, but in bulk. |
| SetAllWithTTL | Same as `SetWithTTL`, but in bulk. |
| Get | Gets a cache entry by its key. |
| GetByKeys | Gets a map of entries by their keys. The resulting map will contain all keys, even if some of the keys in the slice passed as parameter were not present in the cache. |
| GetAll | Gets all cache entries. |
| GetKeysByPattern | Retrieves a slice of keys that matches a given pattern. |
| Delete | Removes a key from the cache. |
| DeleteAll | Removes multiple keys from the cache. |
| DeleteKeysByPattern | Removes all keys that that matches a given pattern. |
| Count | Gets the size of the cache. This includes cache keys which may have already expired, but have not been removed yet. |
| Clear | Wipes the cache. |
| TTL | Gets the time until a cache key expires. |
| Expire | Sets the expiration time of an existing cache key. |
For further documentation, please refer to [Go Reference](https://pkg.go.dev/github.com/TwiN/gocache)
### Examples
#### Creating or updating an entry
```go
cache.Set("key", "value")
cache.Set("key", 1)
cache.Set("key", struct{ Text string }{Test: "value"})
cache.SetWithTTL("key", []byte("value"), 24*time.Hour)
```
#### Getting an entry
```go
value, exists := cache.Get("key")
```
You can also get multiple entries by using `cache.GetByKeys([]string{"key1", "key2"})`
#### Deleting an entry
```go
cache.Delete("key")
```
You can also delete multiple entries by using `cache.DeleteAll([]string{"key1", "key2"})`
#### Complex example
```go
package main
import (
"fmt"
"time"
"github.com/TwiN/gocache/v2"
)
func main() {
cache := gocache.NewCache().WithEvictionPolicy(gocache.LeastRecentlyUsed).WithMaxSize(10000)
cache.StartJanitor() // Passively manages expired entries
defer cache.StopJanitor()
cache.Set("key", "value")
cache.SetWithTTL("key-with-ttl", "value", 60*time.Minute)
cache.SetAll(map[string]any{"k1": "v1", "k2": "v2", "k3": "v3"})
fmt.Println("[Count] Cache size:", cache.Count())
value, exists := cache.Get("key")
fmt.Printf("[Get] key=key; value=%s; exists=%v\n", value, exists)
for key, value := range cache.GetByKeys([]string{"k1", "k2", "k3"}) {
fmt.Printf("[GetByKeys] key=%s; value=%s\n", key, value)
}
for _, key := range cache.GetKeysByPattern("key*", 0) {
fmt.Printf("[GetKeysByPattern] pattern=key*; key=%s\n", key)
}
cache.Expire("key", time.Hour)
time.Sleep(500*time.Millisecond)
timeUntilExpiration, _ := cache.TTL("key")
fmt.Println("[TTL] Number of minutes before 'key' expires:", int(timeUntilExpiration.Seconds()))
cache.Delete("key")
cache.DeleteAll([]string{"k1", "k2", "k3"})
cache.Clear()
fmt.Println("[Count] Cache size after clearing the cache:", cache.Count())
}
```
<details>
<summary>Output</summary>
```
[Count] Cache size: 5
[Get] key=key; value=value; exists=true
[GetByKeys] key=k1; value=v1
[GetByKeys] key=k2; value=v2
[GetByKeys] key=k3; value=v3
[GetKeysByPattern] pattern=key*; key=key-with-ttl
[GetKeysByPattern] pattern=key*; key=key
[TTL] Number of minutes before 'key' expires: 3599
[Count] Cache size after clearing the cache: 0
```
</details>
## Persistence
Prior to v2, gocache supported persistence out of the box.
After some thinking, I decided that persistence added too many dependencies, and given than this is a cache library
and most people wouldn't be interested in persistence, I decided to get rid of it.
That being said, you can use the `GetAll` and `SetAll` methods of `gocache.Cache` to implement persistence yourself.
## Eviction
### MaxSize
Eviction by MaxSize is the default behavior, and is also the most efficient.
The code below will create a cache that has a maximum size of 1000:
```go
cache := gocache.NewCache().WithMaxSize(1000)
```
This means that whenever an operation causes the total size of the cache to go above 1000, the tail will be evicted.
### MaxMemoryUsage
Eviction by MaxMemoryUsage is **disabled by default**, and is in alpha.
The code below will create a cache that has a maximum memory usage of 50MB:
```go
cache := gocache.NewCache().WithMaxSize(0).WithMaxMemoryUsage(50*gocache.Megabyte)
```
This means that whenever an operation causes the total memory usage of the cache to go above 50MB, one or more tails
will be evicted.
Unlike evictions caused by reaching the MaxSize, evictions triggered by MaxMemoryUsage may lead to multiple entries
being evicted in a row. The reason for this is that if, for instance, you had 100 entries of 0.1MB each and you suddenly added
a single entry of 10MB, 100 entries would need to be evicted to make enough space for that new big entry.
It's very important to keep in mind that eviction by MaxMemoryUsage is approximate.
**The only memory taken into consideration is the size of the cache, not the size of the entire application.**
If you pass along 100MB worth of data in a matter of seconds, even though the cache's memory usage will remain
under 50MB (or whatever you configure the MaxMemoryUsage to), the memory footprint generated by that 100MB will
still exist until the next GC cycle.
As previously mentioned, this is a work in progress, and here's a list of the things you should keep in mind:
- The memory usage of structs are a gross estimation and may not reflect the actual memory usage.
- Native types (string, int, bool, []byte, etc.) are the most accurate for calculating the memory usage.
- Adding an entry bigger than the configured MaxMemoryUsage will work, but it will evict all other entries.
## Expiration
There are two ways that the deletion of expired keys can take place:
- Active
- Passive
**Active deletion of expired keys** happens when an attempt is made to access the value of a cache entry that expired.
`Get`, `GetByKeys` and `GetAll` are the only functions that can trigger active deletion of expired keys.
**Passive deletion of expired keys** runs in the background and is managed by the janitor.
If you do not start the janitor, there will be no passive deletion of expired keys.
## Performance
### Summary
- **Set**: Both map and gocache have the same performance.
- **Get**: Map is faster than gocache.
This is because gocache keeps track of the head and the tail for eviction and expiration/TTL.
Ultimately, the difference is negligible.
We could add a way to disable eviction or disable expiration altogether just to match the map's performance,
but if you're looking into using a library like gocache, odds are, you want more than just a map.
### Results
| key | value |
|:-------|:---------|
| goos | windows |
| goarch | amd64 |
| cpu | i7-9700K |
| mem | 32G DDR4 |
```
// Normal map
BenchmarkMap_Get-8 49944228 24.2 ns/op 7 B/op 0 allocs/op
BenchmarkMap_Set/small_value-8 3939964 394.1 ns/op 188 B/op 2 allocs/op
BenchmarkMap_Set/medium_value-8 3868586 395.5 ns/op 191 B/op 2 allocs/op
BenchmarkMap_Set/large_value-8 3992138 385.3 ns/op 186 B/op 2 allocs/op
// Gocache
BenchmarkCache_Get/FirstInFirstOut-8 27907950 44.3 ns/op 7 B/op 0 allocs/op
BenchmarkCache_Get/LeastRecentlyUsed-8 28211396 44.2 ns/op 7 B/op 0 allocs/op
BenchmarkCache_Set/FirstInFirstOut_small_value-8 3139538 373.5 ns/op 185 B/op 3 allocs/op
BenchmarkCache_Set/FirstInFirstOut_medium_value-8 3099516 378.6 ns/op 186 B/op 3 allocs/op
BenchmarkCache_Set/FirstInFirstOut_large_value-8 3086776 386.7 ns/op 186 B/op 3 allocs/op
BenchmarkCache_Set/LeastRecentlyUsed_small_value-8 3070555 379.0 ns/op 187 B/op 3 allocs/op
BenchmarkCache_Set/LeastRecentlyUsed_medium_value-8 3056928 383.8 ns/op 187 B/op 3 allocs/op
BenchmarkCache_Set/LeastRecentlyUsed_large_value-8 3108250 383.8 ns/op 186 B/op 3 allocs/op
BenchmarkCache_SetUsingMaxMemoryUsage/medium_value-8 2773315 449.0 ns/op 210 B/op 4 allocs/op
BenchmarkCache_SetUsingMaxMemoryUsage/large_value-8 2731818 440.0 ns/op 211 B/op 4 allocs/op
BenchmarkCache_SetUsingMaxMemoryUsage/small_value-8 2659296 446.8 ns/op 213 B/op 4 allocs/op
BenchmarkCache_SetWithMaxSize/100_small_value-8 4848658 248.8 ns/op 114 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSize/10000_small_value-8 4117632 293.7 ns/op 106 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSize/100000_small_value-8 3867402 313.0 ns/op 110 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSize/100_medium_value-8 4750057 250.1 ns/op 113 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSize/10000_medium_value-8 4143772 294.5 ns/op 106 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSize/100000_medium_value-8 3768883 313.2 ns/op 111 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSize/100_large_value-8 4822646 251.1 ns/op 114 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSize/10000_large_value-8 4154428 291.6 ns/op 106 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSize/100000_large_value-8 3897358 313.7 ns/op 110 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSizeAndLRU/100_small_value-8 4784180 254.2 ns/op 114 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSizeAndLRU/10000_small_value-8 4067042 292.0 ns/op 106 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSizeAndLRU/100000_small_value-8 3832760 313.8 ns/op 111 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSizeAndLRU/100_medium_value-8 4846706 252.2 ns/op 114 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSizeAndLRU/10000_medium_value-8 4103817 292.5 ns/op 106 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSizeAndLRU/100000_medium_value-8 3845623 315.1 ns/op 111 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSizeAndLRU/100_large_value-8 4744513 257.9 ns/op 114 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSizeAndLRU/10000_large_value-8 3956316 299.5 ns/op 106 B/op 3 allocs/op
BenchmarkCache_SetWithMaxSizeAndLRU/100000_large_value-8 3876843 351.3 ns/op 110 B/op 3 allocs/op
BenchmarkCache_GetSetMultipleConcurrent-8 750088 1566.0 ns/op 128 B/op 8 allocs/op
BenchmarkCache_GetSetConcurrentWithFrequentEviction/FirstInFirstOut-8 3836961 316.2 ns/op 80 B/op 1 allocs/op
BenchmarkCache_GetSetConcurrentWithFrequentEviction/LeastRecentlyUsed-8 3846165 315.6 ns/op 80 B/op 1 allocs/op
BenchmarkCache_GetConcurrently/FirstInFirstOut-8 4830342 239.8 ns/op 8 B/op 1 allocs/op
BenchmarkCache_GetConcurrently/LeastRecentlyUsed-8 4895587 243.2 ns/op 8 B/op 1 allocs/op
(Trimmed "BenchmarkCache_" for readability)
WithForceNilInterfaceOnNilPointer/true_with_nil_struct_pointer-8 6901461 178.5 ns/op 7 B/op 1 allocs/op
WithForceNilInterfaceOnNilPointer/true-8 6629566 180.7 ns/op 7 B/op 1 allocs/op
WithForceNilInterfaceOnNilPointer/false_with_nil_struct_pointer-8 6282798 170.1 ns/op 7 B/op 1 allocs/op
WithForceNilInterfaceOnNilPointer/false-8 6741382 172.6 ns/op 7 B/op 1 allocs/op
WithForceNilInterfaceOnNilPointerWithConcurrency/true_with_nil_struct_pointer-8 4432951 258.0 ns/op 8 B/op 1 allocs/op
WithForceNilInterfaceOnNilPointerWithConcurrency/true-8 4676943 244.4 ns/op 8 B/op 1 allocs/op
WithForceNilInterfaceOnNilPointerWithConcurrency/false_with_nil_struct_pointer-8 4818418 239.6 ns/op 8 B/op 1 allocs/op
WithForceNilInterfaceOnNilPointerWithConcurrency/false-8 5025937 238.2 ns/op 8 B/op 1 allocs/op
```
## FAQ
### How can I persist the data on application termination?
While creating your own auto save feature might come in handy, it may still lead to loss of data if the application
automatically saves every 10 minutes and your application crashes 9 minutes after the previous save.
To increase your odds of not losing any data, you can use Go's `signal` package, more specifically its `Notify` function
which allows listening for termination signals like SIGTERM and SIGINT. Once a termination signal is caught, you can
add the necessary logic for a graceful shutdown.
In the following example, the code that would usually be present in the `main` function is moved to a different function
named `Start` which is launched on a different goroutine so that listening for a termination signals is what blocks the
main goroutine instead:
```go
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/TwiN/gocache/v2"
)
var cache = gocache.NewCache()
func main() {
data := retrieveCacheEntriesUsingWhateverMeanYouUsedToPersistIt()
cache.SetAll(data)
// Start everything else on another goroutine to prevent blocking the main goroutine
go Start()
// Wait for termination signal
sig := make(chan os.Signal, 1)
done := make(chan bool, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
go func() {
<-sig
log.Println("Received termination signal, attempting to gracefully shut down")
// Persist the cache entries
cacheEntries := cache.GetAll()
persistCacheEntriesHoweverYouWant(cacheEntries)
// Tell the main goroutine that we're done
done <- true
}()
<-done
log.Println("Shutting down")
}
```
Note that this won't protect you from a SIGKILL, as this signal cannot be caught.

View File

@@ -1,108 +0,0 @@
package gocache
import (
"fmt"
"time"
"unsafe"
)
// Entry is a cache entry
type Entry struct {
// Key is the name of the cache entry
Key string
// Value is the value of the cache entry
Value any
// RelevantTimestamp is the variable used to store either:
// - creation timestamp, if the Cache's EvictionPolicy is FirstInFirstOut
// - last access timestamp, if the Cache's EvictionPolicy is LeastRecentlyUsed
//
// Note that updating an existing entry will also update this value
RelevantTimestamp time.Time
// Expiration is the unix time in nanoseconds at which the entry will expire (-1 means no expiration)
Expiration int64
next *Entry
previous *Entry
}
// Accessed updates the Entry's RelevantTimestamp to now
func (entry *Entry) Accessed() {
entry.RelevantTimestamp = time.Now()
}
// Expired returns whether the Entry has expired
func (entry Entry) Expired() bool {
if entry.Expiration > 0 {
if time.Now().UnixNano() > entry.Expiration {
return true
}
}
return false
}
// SizeInBytes returns the size of an entry in bytes, approximately.
func (entry *Entry) SizeInBytes() int {
return toBytes(entry.Key) + toBytes(entry.Value) + 32
}
func toBytes(value any) int {
switch value.(type) {
case string:
return int(unsafe.Sizeof(value)) + len(value.(string))
case int8, uint8, bool:
return int(unsafe.Sizeof(value)) + 1
case int16, uint16:
return int(unsafe.Sizeof(value)) + 2
case int32, uint32, float32, complex64:
return int(unsafe.Sizeof(value)) + 4
case int64, uint64, int, uint, float64, complex128:
return int(unsafe.Sizeof(value)) + 8
case []any:
size := 0
for _, v := range value.([]any) {
size += toBytes(v)
}
return int(unsafe.Sizeof(value)) + size
case []string:
size := 0
for _, v := range value.([]string) {
size += toBytes(v)
}
return int(unsafe.Sizeof(value)) + size
case []int8:
return int(unsafe.Sizeof(value)) + len(value.([]int8))
case []uint8:
return int(unsafe.Sizeof(value)) + len(value.([]uint8))
case []bool:
return int(unsafe.Sizeof(value)) + len(value.([]bool))
case []int16:
return int(unsafe.Sizeof(value)) + (len(value.([]int16)) * 2)
case []uint16:
return int(unsafe.Sizeof(value)) + (len(value.([]uint16)) * 2)
case []int32:
return int(unsafe.Sizeof(value)) + (len(value.([]int32)) * 4)
case []uint32:
return int(unsafe.Sizeof(value)) + (len(value.([]uint32)) * 4)
case []float32:
return int(unsafe.Sizeof(value)) + (len(value.([]float32)) * 4)
case []complex64:
return int(unsafe.Sizeof(value)) + (len(value.([]complex64)) * 4)
case []int64:
return int(unsafe.Sizeof(value)) + (len(value.([]int64)) * 8)
case []uint64:
return int(unsafe.Sizeof(value)) + (len(value.([]uint64)) * 8)
case []int:
return int(unsafe.Sizeof(value)) + (len(value.([]int)) * 8)
case []uint:
return int(unsafe.Sizeof(value)) + (len(value.([]uint)) * 8)
case []float64:
return int(unsafe.Sizeof(value)) + (len(value.([]float64)) * 8)
case []complex128:
return int(unsafe.Sizeof(value)) + (len(value.([]complex128)) * 8)
default:
return int(unsafe.Sizeof(value)) + len(fmt.Sprintf("%v", value))
}
}

View File

@@ -1,599 +0,0 @@
package gocache
import (
"errors"
"reflect"
"sync"
"time"
)
var (
Debug = false
)
const (
// NoMaxSize means that the cache has no maximum number of entries in the cache
// Setting Cache.maxSize to this value also means there will be no eviction
NoMaxSize = 0
// NoMaxMemoryUsage means that the cache has no maximum number of entries in the cache
NoMaxMemoryUsage = 0
// DefaultMaxSize is the max size set if no max size is specified
DefaultMaxSize = 100000
// NoExpiration is the value that must be used as TTL to specify that the given key should never expire
NoExpiration = -1
Kilobyte = 1024
Megabyte = 1024 * Kilobyte
Gigabyte = 1024 * Megabyte
)
var (
ErrKeyDoesNotExist = errors.New("key does not exist") // Returned when a cache key does not exist
ErrKeyHasNoExpiration = errors.New("key has no expiration") // Returned when a cache key has no expiration
ErrJanitorAlreadyRunning = errors.New("janitor is already running") // Returned when the janitor has already been started
)
// Cache is the core struct of gocache which contains the data as well as all relevant configuration fields
//
// Do not instantiate this struct directly, use NewCache instead
type Cache struct {
// maxSize is the maximum amount of entries that can be in the cache at any given time
// By default, this is set to DefaultMaxSize
maxSize int
// maxMemoryUsage is the maximum amount of memory that can be taken up by the cache at any time
// By default, this is set to NoMaxMemoryUsage, meaning that the default behavior is to not evict
// based on maximum memory usage
maxMemoryUsage int
// evictionPolicy is the eviction policy
evictionPolicy EvictionPolicy
// defaultTTL is the default TTL for each entry
// Defaults to NoExpiration
defaultTTL time.Duration
// stats is the object that contains cache statistics/metrics
stats *Statistics
// entries is the content of the cache
entries map[string]*Entry
// mutex is the lock for making concurrent operations on the cache
mutex sync.RWMutex
// head is the cache entry at the head of the cache
head *Entry
// tail is the last cache node and also the next entry that will be evicted
tail *Entry
// stopJanitor is the channel used to stop the janitor
stopJanitor chan bool
// memoryUsage is the approximate memory usage of the cache (dataset only) in bytes
memoryUsage int
// forceNilInterfaceOnNilPointer determines whether all Set-like functions should set a value as nil if the
// interface passed has a nil value but not a nil type.
//
// By default, interfaces are only nil when both their type and value is nil.
// This means that when you pass a pointer to a nil value, the type of the interface
// will still show as nil, which means that if you don't cast the interface after
// retrieving it, a nil check will return that the value is not false.
forceNilInterfaceOnNilPointer bool
}
// MaxSize returns the maximum amount of keys that can be present in the cache before
// new entries trigger the eviction of the tail
func (cache *Cache) MaxSize() int {
return cache.maxSize
}
// MaxMemoryUsage returns the configured maxMemoryUsage of the cache
func (cache *Cache) MaxMemoryUsage() int {
return cache.maxMemoryUsage
}
// EvictionPolicy returns the EvictionPolicy of the Cache
func (cache *Cache) EvictionPolicy() EvictionPolicy {
return cache.evictionPolicy
}
// Stats returns statistics from the cache
func (cache *Cache) Stats() Statistics {
cache.mutex.RLock()
stats := Statistics{
EvictedKeys: cache.stats.EvictedKeys,
ExpiredKeys: cache.stats.ExpiredKeys,
Hits: cache.stats.Hits,
Misses: cache.stats.Misses,
}
cache.mutex.RUnlock()
return stats
}
// MemoryUsage returns the current memory usage of the cache's dataset in bytes
// If MaxMemoryUsage is set to NoMaxMemoryUsage, this will return 0
func (cache *Cache) MemoryUsage() int {
return cache.memoryUsage
}
// WithMaxSize sets the maximum amount of entries that can be in the cache at any given time
// A maxSize of 0 or less means infinite
func (cache *Cache) WithMaxSize(maxSize int) *Cache {
if maxSize < 0 {
maxSize = NoMaxSize
}
if maxSize != NoMaxSize && cache.Count() == 0 {
cache.entries = make(map[string]*Entry, maxSize)
}
cache.maxSize = maxSize
return cache
}
// WithMaxMemoryUsage sets the maximum amount of memory that can be used by the cache at any given time
//
// NOTE: This is approximate.
//
// Setting this to NoMaxMemoryUsage will disable eviction by memory usage
func (cache *Cache) WithMaxMemoryUsage(maxMemoryUsageInBytes int) *Cache {
if maxMemoryUsageInBytes < 0 {
maxMemoryUsageInBytes = NoMaxMemoryUsage
}
cache.maxMemoryUsage = maxMemoryUsageInBytes
return cache
}
// WithEvictionPolicy sets eviction algorithm.
//
// Defaults to FirstInFirstOut (FIFO)
func (cache *Cache) WithEvictionPolicy(policy EvictionPolicy) *Cache {
cache.evictionPolicy = policy
return cache
}
// WithDefaultTTL sets the default TTL for each entry (unless a different TTL is specified using SetWithTTL or SetAllWithTTL)
//
// Defaults to NoExpiration (-1)
func (cache *Cache) WithDefaultTTL(ttl time.Duration) *Cache {
if ttl > 1 {
cache.defaultTTL = ttl
}
return cache
}
// WithForceNilInterfaceOnNilPointer sets whether all Set-like functions should set a value as nil if the
// interface passed has a nil value but not a nil type.
//
// In Go, an interface is only nil if both its type and value are nil, which means that a nil pointer
// (e.g. (*Struct)(nil)) will retain its attribution to the type, and the unmodified value returned from
// Cache.Get, for instance, would return false when compared with nil if this option is set to false.
//
// We can bypass this by detecting if the interface's value is nil and setting it to nil rather than
// a nil pointer, which will make the value returned from Cache.Get return true when compared with nil.
// This is exactly what passing true to WithForceNilInterfaceOnNilPointer does, and it's also the default behavior.
//
// Alternatively, you may pass false to WithForceNilInterfaceOnNilPointer, which will mean that you'll have
// to cast the value returned from Cache.Get to its original type to check for whether the pointer returned
// is nil or not.
//
// If set to true (default):
//
// cache := gocache.NewCache().WithForceNilInterfaceOnNilPointer(true)
// cache.Set("key", (*Struct)(nil))
// value, _ := cache.Get("key")
// // the following returns true, because the interface{} (any) was forcefully set to nil
// if value == nil {}
// // the following will panic, because the value has been casted to its type (which is nil)
// if value.(*Struct) == nil {}
//
// If set to false:
//
// cache := gocache.NewCache().WithForceNilInterfaceOnNilPointer(false)
// cache.Set("key", (*Struct)(nil))
// value, _ := cache.Get("key")
// // the following returns false, because the interface{} (any) returned has a non-nil type (*Struct)
// if value == nil {}
// // the following returns true, because the value has been cast to its type
// if value.(*Struct) == nil {}
//
// In other words, if set to true, you do not need to cast the value returned from the cache to
// check if the value is nil.
//
// Defaults to true
func (cache *Cache) WithForceNilInterfaceOnNilPointer(forceNilInterfaceOnNilPointer bool) *Cache {
cache.forceNilInterfaceOnNilPointer = forceNilInterfaceOnNilPointer
return cache
}
// NewCache creates a new Cache
//
// Should be used in conjunction with Cache.WithMaxSize, Cache.WithMaxMemoryUsage and/or Cache.WithEvictionPolicy
//
// gocache.NewCache().WithMaxSize(10000).WithEvictionPolicy(gocache.LeastRecentlyUsed)
func NewCache() *Cache {
return &Cache{
maxSize: DefaultMaxSize,
evictionPolicy: FirstInFirstOut,
defaultTTL: NoExpiration,
stats: &Statistics{},
entries: make(map[string]*Entry),
mutex: sync.RWMutex{},
stopJanitor: nil,
forceNilInterfaceOnNilPointer: true,
}
}
// Set creates or updates a key with a given value
func (cache *Cache) Set(key string, value any) {
cache.SetWithTTL(key, value, cache.defaultTTL)
}
// SetWithTTL creates or updates a key with a given value and sets an expiration time (-1 is NoExpiration)
//
// The TTL provided must be greater than 0, or NoExpiration (-1). If a negative value that isn't -1 (NoExpiration) is
// provided, the entry will not be created if the key doesn't exist
func (cache *Cache) SetWithTTL(key string, value any, ttl time.Duration) {
// An interface is only nil if both its value and its type are nil, however, passing a nil pointer as an interface{}
// means that the interface itself is not nil, because the interface value is nil but not the type.
if cache.forceNilInterfaceOnNilPointer {
if value != nil && (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) {
value = nil
}
}
cache.mutex.Lock()
entry, ok := cache.get(key)
if !ok {
// A negative TTL that isn't -1 (NoExpiration) or 0 is an entry that will expire instantly,
// so might as well just not create it in the first place
if ttl != NoExpiration && ttl < 1 {
cache.mutex.Unlock()
return
}
// Cache entry doesn't exist, so we have to create a new one
entry = &Entry{
Key: key,
Value: value,
RelevantTimestamp: time.Now(),
next: cache.head,
}
if cache.head == nil {
cache.tail = entry
} else {
cache.head.previous = entry
}
cache.head = entry
cache.entries[key] = entry
if cache.maxMemoryUsage != NoMaxMemoryUsage {
cache.memoryUsage += entry.SizeInBytes()
}
} else {
// A negative TTL that isn't -1 (NoExpiration) or 0 is an entry that will expire instantly,
// so might as well just delete it immediately instead of updating it
if ttl != NoExpiration && ttl < 1 {
cache.delete(key)
cache.mutex.Unlock()
return
}
if cache.maxMemoryUsage != NoMaxMemoryUsage {
// Subtract the old entry from the cache's memoryUsage
cache.memoryUsage -= entry.SizeInBytes()
}
// Update existing entry's value
entry.Value = value
entry.RelevantTimestamp = time.Now()
if cache.maxMemoryUsage != NoMaxMemoryUsage {
// Add the memory usage of the new entry to the cache's memoryUsage
cache.memoryUsage += entry.SizeInBytes()
}
// Because we just updated the entry, we need to move it back to HEAD
cache.moveExistingEntryToHead(entry)
}
if ttl != NoExpiration {
entry.Expiration = time.Now().Add(ttl).UnixNano()
} else {
entry.Expiration = NoExpiration
}
// If the cache doesn't have a maxSize/maxMemoryUsage, then there's no point
// checking if we need to evict an entry, so we'll just return now
if cache.maxSize == NoMaxSize && cache.maxMemoryUsage == NoMaxMemoryUsage {
cache.mutex.Unlock()
return
}
// If there's a maxSize and the cache has more entries than the maxSize, evict
if cache.maxSize != NoMaxSize && len(cache.entries) > cache.maxSize {
cache.evict()
}
// If there's a maxMemoryUsage and the memoryUsage is above the maxMemoryUsage, evict
if cache.maxMemoryUsage != NoMaxMemoryUsage && cache.memoryUsage > cache.maxMemoryUsage {
for cache.memoryUsage > cache.maxMemoryUsage && len(cache.entries) > 0 {
cache.evict()
}
}
cache.mutex.Unlock()
}
// SetAll creates or updates multiple values
func (cache *Cache) SetAll(entries map[string]any) {
cache.SetAllWithTTL(entries, cache.defaultTTL)
}
// SetAllWithTTL creates or updates multiple values
func (cache *Cache) SetAllWithTTL(entries map[string]any, ttl time.Duration) {
for key, value := range entries {
cache.SetWithTTL(key, value, ttl)
}
}
// Get retrieves an entry using the key passed as parameter
// If there is no such entry, the value returned will be nil and the boolean will be false
// If there is an entry, the value returned will be the value cached and the boolean will be true
func (cache *Cache) Get(key string) (any, bool) {
cache.mutex.Lock()
entry, ok := cache.get(key)
if !ok {
cache.stats.Misses++
cache.mutex.Unlock()
return nil, false
}
if entry.Expired() {
cache.stats.ExpiredKeys++
cache.delete(key)
cache.mutex.Unlock()
return nil, false
}
cache.stats.Hits++
if cache.evictionPolicy == LeastRecentlyUsed {
entry.Accessed()
if cache.head == entry {
cache.mutex.Unlock()
return entry.Value, true
}
// Because the eviction policy is LRU, we need to move the entry back to HEAD
cache.moveExistingEntryToHead(entry)
}
cache.mutex.Unlock()
return entry.Value, true
}
// GetValue retrieves an entry using the key passed as parameter
// Unlike Get, this function only returns the value
func (cache *Cache) GetValue(key string) any {
value, _ := cache.Get(key)
return value
}
// GetByKeys retrieves multiple entries using the keys passed as parameter
// All keys are returned in the map, regardless of whether they exist or not, however, entries that do not exist in the
// cache will return nil, meaning that there is no way of determining whether a key genuinely has the value nil, or
// whether it doesn't exist in the cache using only this function.
func (cache *Cache) GetByKeys(keys []string) map[string]any {
entries := make(map[string]any)
for _, key := range keys {
entries[key], _ = cache.Get(key)
}
return entries
}
// GetAll retrieves all cache entries
//
// If the eviction policy is LeastRecentlyUsed, note that unlike Get and GetByKeys, this does not update the last access
// timestamp. The reason for this is that since all cache entries will be accessed, updating the last access timestamp
// would provide very little benefit while harming the ability to accurately determine the next key that will be evicted
//
// You should probably avoid using this if you have a lot of entries.
//
// GetKeysByPattern is a good alternative if you want to retrieve entries that you do not have the key for, as it only
// retrieves the keys and does not trigger active eviction and has a parameter for setting a limit to the number of keys
// you wish to retrieve.
func (cache *Cache) GetAll() map[string]any {
entries := make(map[string]any)
cache.mutex.Lock()
for key, entry := range cache.entries {
if entry.Expired() {
cache.delete(key)
continue
}
entries[key] = entry.Value
}
cache.stats.Hits += uint64(len(entries))
cache.mutex.Unlock()
return entries
}
// GetKeysByPattern retrieves a slice of keys that match a given pattern
// If the limit is set to 0, the entire cache will be searched for matching keys.
// If the limit is above 0, the search will stop once the specified number of matching keys have been found.
//
// e.g.
//
// cache.GetKeysByPattern("*some*", 0) will return all keys containing "some" in them
// cache.GetKeysByPattern("*some*", 5) will return 5 keys (or less) containing "some" in them
//
// Note that GetKeysByPattern does not trigger active evictions, nor does it count as accessing the entry (if LRU).
// The reason for that behavior is that these two (active eviction and access) only applies when you access the value
// of the cache entry, and this function only returns the keys.
func (cache *Cache) GetKeysByPattern(pattern string, limit int) []string {
var matchingKeys []string
cache.mutex.Lock()
for key, value := range cache.entries {
if value.Expired() {
continue
}
if MatchPattern(pattern, key) {
matchingKeys = append(matchingKeys, key)
if limit > 0 && len(matchingKeys) >= limit {
break
}
}
}
cache.mutex.Unlock()
return matchingKeys
}
// Delete removes a key from the cache
//
// Returns false if the key did not exist.
func (cache *Cache) Delete(key string) bool {
cache.mutex.Lock()
ok := cache.delete(key)
cache.mutex.Unlock()
return ok
}
// DeleteAll deletes multiple entries based on the keys passed as parameter
//
// Returns the number of keys deleted
func (cache *Cache) DeleteAll(keys []string) int {
numberOfKeysDeleted := 0
cache.mutex.Lock()
for _, key := range keys {
if cache.delete(key) {
numberOfKeysDeleted++
}
}
cache.mutex.Unlock()
return numberOfKeysDeleted
}
// DeleteKeysByPattern deletes all entries matching a given key pattern and returns the number of entries deleted.
//
// Note that DeleteKeysByPattern does not trigger active evictions, nor does it count as accessing the entry (if LRU).
func (cache *Cache) DeleteKeysByPattern(pattern string) int {
return cache.DeleteAll(cache.GetKeysByPattern(pattern, 0))
}
// Count returns the total amount of entries in the cache, regardless of whether they're expired or not
func (cache *Cache) Count() int {
cache.mutex.RLock()
count := len(cache.entries)
cache.mutex.RUnlock()
return count
}
// Clear deletes all entries from the cache
func (cache *Cache) Clear() {
cache.mutex.Lock()
cache.entries = make(map[string]*Entry)
cache.memoryUsage = 0
cache.head = nil
cache.tail = nil
cache.mutex.Unlock()
}
// TTL returns the time until the cache entry specified by the key passed as parameter
// will be deleted.
func (cache *Cache) TTL(key string) (time.Duration, error) {
cache.mutex.RLock()
entry, ok := cache.get(key)
cache.mutex.RUnlock()
if !ok {
return 0, ErrKeyDoesNotExist
}
if entry.Expiration == NoExpiration {
return 0, ErrKeyHasNoExpiration
}
timeUntilExpiration := time.Until(time.Unix(0, entry.Expiration))
if timeUntilExpiration < 0 {
// The key has already expired but hasn't been deleted yet.
// From the client's perspective, this means that the cache entry doesn't exist
return 0, ErrKeyDoesNotExist
}
return timeUntilExpiration, nil
}
// Expire sets a key's expiration time
//
// A TTL of -1 means that the key will never expire
// A TTL of 0 means that the key will expire immediately
// If using LRU, note that this does not reset the position of the key
//
// Returns true if the cache key exists and has had its expiration time altered
func (cache *Cache) Expire(key string, ttl time.Duration) bool {
entry, ok := cache.get(key)
if !ok || entry.Expired() {
return false
}
if ttl != NoExpiration {
entry.Expiration = time.Now().Add(ttl).UnixNano()
} else {
entry.Expiration = NoExpiration
}
return true
}
// get retrieves an entry using the key passed as parameter, but unlike Get, it doesn't update the access time or
// move the position of the entry to the head
func (cache *Cache) get(key string) (*Entry, bool) {
entry, ok := cache.entries[key]
return entry, ok
}
func (cache *Cache) delete(key string) bool {
entry, ok := cache.entries[key]
if ok {
if cache.maxMemoryUsage != NoMaxMemoryUsage {
cache.memoryUsage -= entry.SizeInBytes()
}
cache.removeExistingEntryReferences(entry)
delete(cache.entries, key)
}
return ok
}
// moveExistingEntryToHead replaces the current cache head for an existing entry
func (cache *Cache) moveExistingEntryToHead(entry *Entry) {
if !(entry == cache.head && entry == cache.tail) {
cache.removeExistingEntryReferences(entry)
}
if entry != cache.head {
entry.next = cache.head
entry.previous = nil
if cache.head != nil {
cache.head.previous = entry
}
cache.head = entry
}
}
// removeExistingEntryReferences modifies the next and previous reference of an existing entry and re-links
// the next and previous entry accordingly, as well as the cache head or/and the cache tail if necessary.
// Note that it does not remove the entry from the cache, only the references.
func (cache *Cache) removeExistingEntryReferences(entry *Entry) {
if cache.tail == entry && cache.head == entry {
cache.tail = nil
cache.head = nil
} else if cache.tail == entry {
cache.tail = cache.tail.previous
} else if cache.head == entry {
cache.head = cache.head.next
}
if entry.previous != nil {
entry.previous.next = entry.next
}
if entry.next != nil {
entry.next.previous = entry.previous
}
entry.next = nil
entry.previous = nil
}
// evict removes the tail from the cache
func (cache *Cache) evict() {
if cache.tail == nil || len(cache.entries) == 0 {
return
}
if cache.tail != nil {
oldTail := cache.tail
cache.removeExistingEntryReferences(oldTail)
delete(cache.entries, oldTail.Key)
if cache.maxMemoryUsage != NoMaxMemoryUsage {
cache.memoryUsage -= oldTail.SizeInBytes()
}
cache.stats.EvictedKeys++
}
}

View File

@@ -1,146 +0,0 @@
package gocache
import (
"log"
"time"
)
const (
// JanitorShiftTarget is the target number of expired keys to find during passive clean up duty
// before pausing the passive expired keys eviction process
JanitorShiftTarget = 25
// JanitorMaxIterationsPerShift is the maximum number of nodes to traverse before pausing
//
// This is to prevent the janitor from traversing the entire cache, which could take a long time
// to complete depending on the size of the cache.
//
// By limiting it to a small number, we are effectively reducing the impact of passive eviction.
JanitorMaxIterationsPerShift = 1000
// JanitorMinShiftBackOff is the minimum interval between each iteration of steps
// defined by JanitorMaxIterationsPerShift
JanitorMinShiftBackOff = 50 * time.Millisecond
// JanitorMaxShiftBackOff is the maximum interval between each iteration of steps
// defined by JanitorMaxIterationsPerShift
JanitorMaxShiftBackOff = 500 * time.Millisecond
)
// StartJanitor starts the janitor on a different goroutine
// The janitor's job is to delete expired keys in the background, in other words, it takes care of passive eviction.
// It can be stopped by calling Cache.StopJanitor.
// If you do not start the janitor, expired keys will only be deleted when they are accessed through Get, GetByKeys, or
// GetAll.
func (cache *Cache) StartJanitor() error {
if cache.stopJanitor != nil {
return ErrJanitorAlreadyRunning
}
cache.stopJanitor = make(chan bool)
go func() {
// rather than starting from the tail on every run, we can try to start from the last traversed entry
var lastTraversedNode *Entry
totalNumberOfExpiredKeysInPreviousRunFromTailToHead := 0
backOff := JanitorMinShiftBackOff
for {
select {
case <-time.After(backOff):
// Passive clean up duty
cache.mutex.Lock()
if cache.tail != nil {
start := time.Now()
steps := 0
expiredEntriesFound := 0
current := cache.tail
if lastTraversedNode != nil {
// Make sure the lastTraversedNode is still in the cache, otherwise we might be traversing nodes that were already deleted.
// Furthermore, we need to make sure that the entry from the cache has the same pointer as the lastTraversedNode
// to verify that there isn't just a new cache entry with the same key (i.e. in case lastTraversedNode got evicted)
if entryFromCache, isInCache := cache.get(lastTraversedNode.Key); isInCache && entryFromCache == lastTraversedNode {
current = lastTraversedNode
}
}
if current == cache.tail {
if Debug {
log.Printf("There are currently %d entries in the cache. The last walk resulted in finding %d expired keys", len(cache.entries), totalNumberOfExpiredKeysInPreviousRunFromTailToHead)
}
totalNumberOfExpiredKeysInPreviousRunFromTailToHead = 0
}
for current != nil {
// since we're walking from the tail to the head, we get the previous reference
var previous *Entry
steps++
if current.Expired() {
expiredEntriesFound++
// Because delete will remove the previous reference from the entry, we need to store the
// previous reference before we delete it
previous = current.previous
cache.delete(current.Key)
cache.stats.ExpiredKeys++
}
if current == cache.head {
lastTraversedNode = nil
break
}
// Travel to the current node's previous node only if no specific previous node has been specified
if previous != nil {
current = previous
} else {
current = current.previous
}
lastTraversedNode = current
if steps == JanitorMaxIterationsPerShift || expiredEntriesFound >= JanitorShiftTarget {
if expiredEntriesFound > 0 {
backOff = JanitorMinShiftBackOff
} else {
if backOff*2 <= JanitorMaxShiftBackOff {
backOff *= 2
} else {
backOff = JanitorMaxShiftBackOff
}
}
break
}
}
if Debug {
log.Printf("traversed %d nodes and found %d expired entries in %s before stopping\n", steps, expiredEntriesFound, time.Since(start))
}
totalNumberOfExpiredKeysInPreviousRunFromTailToHead += expiredEntriesFound
} else {
if backOff*2 < JanitorMaxShiftBackOff {
backOff *= 2
} else {
backOff = JanitorMaxShiftBackOff
}
}
cache.mutex.Unlock()
case <-cache.stopJanitor:
cache.stopJanitor <- true
return
}
}
}()
//if Debug {
// go func() {
// var m runtime.MemStats
// for {
// runtime.ReadMemStats(&m)
// log.Printf("Alloc=%vMB; HeapReleased=%vMB; Sys=%vMB; HeapInUse=%vMB; HeapObjects=%v; HeapObjectsFreed=%v; GC=%v; cache.memoryUsage=%vMB; cacheSize=%d\n", m.Alloc/1024/1024, m.HeapReleased/1024/1024, m.Sys/1024/1024, m.HeapInuse/1024/1024, m.HeapObjects, m.Frees, m.NumGC, cache.memoryUsage/1024/1024, cache.Count())
// time.Sleep(3 * time.Second)
// }
// }()
//}
return nil
}
// StopJanitor stops the janitor
func (cache *Cache) StopJanitor() {
if cache.stopJanitor != nil {
// Tell the janitor to stop, and then wait for the janitor to reply on the same channel that it's stopping
// This may seem a bit odd, but this allows us to avoid a data race condition when trying to set
// cache.stopJanitor to nil
cache.stopJanitor <- true
<-cache.stopJanitor
cache.stopJanitor = nil
}
}

View File

@@ -1,12 +0,0 @@
package gocache
import "path/filepath"
// MatchPattern checks whether a string matches a pattern
func MatchPattern(pattern, s string) bool {
if pattern == "*" {
return true
}
matched, _ := filepath.Match(pattern, s)
return matched
}

View File

@@ -1,33 +0,0 @@
package gocache
// EvictionPolicy is what dictates how evictions are handled
type EvictionPolicy string
const (
// LeastRecentlyUsed is an eviction policy that causes the most recently accessed cache entry to be moved to the
// head of the cache. Effectively, this causes the cache entries that have not been accessed for some time to
// gradually move closer and closer to the tail, and since the tail is the entry that gets deleted when an eviction
// is required, it allows less used cache entries to be evicted while keeping recently accessed entries at or close
// to the head.
//
// For instance, creating a Cache with a Cache.MaxSize of 3 and creating the entries 1, 2 and 3 in that order would
// put 3 at the head and 1 at the tail:
// 3 (head) -> 2 -> 1 (tail)
// If the cache entry 1 was then accessed, 1 would become the head and 2 the tail:
// 1 (head) -> 3 -> 2 (tail)
// If a cache entry 4 was then created, because the Cache.MaxSize is 3, the tail (2) would then be evicted:
// 4 (head) -> 1 -> 3 (tail)
LeastRecentlyUsed EvictionPolicy = "LeastRecentlyUsed"
// FirstInFirstOut is an eviction policy that causes cache entries to be evicted in the same order that they are
// created.
//
// For instance, creating a Cache with a Cache.MaxSize of 3 and creating the entries 1, 2 and 3 in that order would
// put 3 at the head and 1 at the tail:
// 3 (head) -> 2 -> 1 (tail)
// If the cache entry 1 was then accessed, unlike with LeastRecentlyUsed, nothing would change:
// 3 (head) -> 2 -> 1 (tail)
// If a cache entry 4 was then created, because the Cache.MaxSize is 3, the tail (1) would then be evicted:
// 4 (head) -> 3 -> 2 (tail)
FirstInFirstOut EvictionPolicy = "FirstInFirstOut"
)

View File

@@ -1,15 +0,0 @@
package gocache
type Statistics struct {
// EvictedKeys is the number of keys that were evicted
EvictedKeys uint64
// ExpiredKeys is the number of keys that were automatically deleted as a result of expiring
ExpiredKeys uint64
// Hits is the number of cache hits
Hits uint64
// Misses is the number of cache misses
Misses uint64
}

View File

@@ -1 +0,0 @@
* text=auto eol=lf

View File

@@ -1,2 +0,0 @@
.idea
*.iml

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 TwiN
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,5 +0,0 @@
bench:
go test -bench . -race
test:
go test . -race

View File

@@ -1,92 +0,0 @@
# health
![test](https://github.com/TwiN/health/workflows/test/badge.svg?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/TwiN/health)](https://goreportcard.com/report/github.com/TwiN/health)
[![codecov](https://codecov.io/gh/TwiN/health/branch/master/graph/badge.svg)](https://codecov.io/gh/TwiN/health)
[![Go version](https://img.shields.io/github/go-mod/go-version/TwiN/health.svg)](https://github.com/TwiN/health)
[![Go Reference](https://pkg.go.dev/badge/github.com/TwiN/health.svg)](https://pkg.go.dev/github.com/TwiN/health)
Health is a library used for creating a very simple health endpoint.
While implementing a health endpoint is very simple, I've grown tired of implementing
it over and over again.
## Installation
```console
go get -u github.com/TwiN/health
```
## Usage
To retrieve the handler, you must use `health.Handler()` and are expected to pass it to the router like so:
```go
router := http.NewServeMux()
router.Handle("/health", health.Handler())
server := &http.Server{
Addr: ":8080",
Handler: router,
}
```
By default, the handler will return `UP` when the status is up, and `DOWN` when the status is down.
If you prefer using JSON, however, you may initialize the health handler like so:
```go
router.Handle("/health", health.Handler().WithJSON(true))
```
The above will cause the response body to become `{"status":"UP"}` and `{"status":"DOWN"}` for both status respectively,
unless there is a reason, in which case a reason set to `because` would return `{"status":"UP", "reason":"because"}`
and `{"status":"DOWN", "reason":"because"}` respectively.
To set the health status to `DOWN` with a reason:
```go
health.SetUnhealthy("<enter reason here>")
```
The string passed will be automatically set as the reason.
In a similar fashion, to set the health status to `UP` and clear the reason:
```go
health.SetHealthy()
```
Alternatively, to set the status and the reason individually you can use `health.SetStatus(<status>)` where `<status>` is `health.Up`
or `health.Down`:
```go
health.SetStatus(health.Up)
health.SetStatus(health.Down)
```
As for the reason:
```go
health.SetReason("database is unreachable")
```
Generally speaking, you'd only want to include a reason if the status is `Down`, but you can do as you desire.
For the sake of convenience, you can also use `health.SetStatusAndReason(<status>, <reason>)` instead of doing
`health.SetStatus(<status>)` and `health.SetReason(<reason>)` separately.
### Complete example
```go
package main
import (
"net/http"
"time"
"github.com/TwiN/health"
)
func main() {
router := http.NewServeMux()
router.Handle("/health", health.Handler())
server := &http.Server{
Addr: "0.0.0.0:8080",
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 15 * time.Second,
}
server.ListenAndServe()
}
```

View File

@@ -1,130 +0,0 @@
package health
import (
"encoding/json"
"net/http"
"sync"
)
var (
handler = &healthHandler{
useJSON: false,
status: Up,
}
)
// responseBody is the body of the response returned by the health handler.
type responseBody struct {
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
}
// healthHandler is the HTTP handler for serving the health endpoint
type healthHandler struct {
useJSON bool
status Status
reason string
mutex sync.RWMutex
}
// WithJSON configures whether the handler should output a response in JSON or in raw text
//
// Defaults to false
func (h *healthHandler) WithJSON(v bool) *healthHandler {
h.useJSON = v
return h
}
// ServeHTTP serves the HTTP request for the health handler
func (h *healthHandler) ServeHTTP(writer http.ResponseWriter, _ *http.Request) {
var statusCode int
var body []byte
h.mutex.RLock()
status, reason, useJSON := h.status, h.reason, h.useJSON
h.mutex.RUnlock()
if status == Up {
statusCode = http.StatusOK
} else {
statusCode = http.StatusInternalServerError
}
if useJSON {
// We can safely ignore the error here because we know that both values are strings, therefore are supported encoders.
body, _ = json.Marshal(responseBody{Status: string(status), Reason: reason})
writer.Header().Set("Content-Type", "application/json")
} else {
if len(reason) == 0 {
body = []byte(status)
} else {
body = []byte(string(status) + ": " + reason)
}
}
writer.WriteHeader(statusCode)
_, _ = writer.Write(body)
}
// Handler retrieves the health handler
func Handler() *healthHandler {
return handler
}
// GetStatus retrieves the current status returned by the health handler
func GetStatus() Status {
handler.mutex.RLock()
defer handler.mutex.RUnlock()
return handler.status
}
// SetStatus sets the status to be returned by the health handler
func SetStatus(status Status) {
handler.mutex.Lock()
handler.status = status
handler.mutex.Unlock()
}
// GetReason retrieves the current status returned by the health handler
func GetReason() string {
handler.mutex.RLock()
defer handler.mutex.RUnlock()
return handler.reason
}
// SetReason sets a reason for the current status to be returned by the health handler
func SetReason(reason string) {
handler.mutex.Lock()
handler.reason = reason
handler.mutex.Unlock()
}
// SetStatusAndReason sets the status and reason to be returned by the health handler
func SetStatusAndReason(status Status, reason string) {
handler.mutex.Lock()
handler.status = status
handler.reason = reason
handler.mutex.Unlock()
}
// SetStatusAndResetReason sets the status and resets the reason to a blank string
func SetStatusAndResetReason(status Status) {
handler.mutex.Lock()
handler.status = status
handler.reason = ""
handler.mutex.Unlock()
}
// SetHealthy sets the status to Up and the reason to a blank string
func SetHealthy() {
SetStatusAndResetReason(Up)
}
// SetUnhealthy sets the status to Down and the reason to the string passed as parameter
//
// Unlike SetHealthy, this function enforces setting a reason, because it's good practice to give at least a bit
// of information as to why an application is unhealthy, and this library attempts to promote good practices.
func SetUnhealthy(reason string) {
handler.mutex.Lock()
handler.status = Down
handler.reason = reason
handler.mutex.Unlock()
}

View File

@@ -1,8 +0,0 @@
package health
type Status string
var (
Down Status = "DOWN" // For when the application is unhealthy
Up Status = "UP" // For when the application is healthy
)

View File

@@ -1,2 +0,0 @@
.idea
bin

21
vendor/github.com/TwiN/whois/LICENSE generated vendored
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 TwiN
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,4 +0,0 @@
.PHONY: build-binaries
build-binaries:
./scripts/build.sh

View File

@@ -1,81 +0,0 @@
# whois
![test](https://github.com/TwiN/whois/workflows/test/badge.svg?branch=master)
Lightweight library for retrieving WHOIS information on a domain.
It automatically retrieves the appropriate WHOIS server based on the domain's TLD by first querying IANA.
## Usage
### As an executable
To install it:
```console
go install github.com/TwiN/whois/cmd/whois@latest
```
To run it:
```console
whois example.com
```
### As a library
```console
go get github.com/TwiN/whois
```
#### Query
If all you want is the text a WHOIS server would return you, you can use the `Query` method of the `whois.Client` type:
```go
package main
import "github.com/TwiN/whois"
func main() {
client := whois.NewClient()
output, err := client.Query("example.com")
if err != nil {
panic(err)
}
println(output)
}
```
#### QueryAndParse
If you want specific pieces of information, you can use the `QueryAndParse` method of the `whois.Client` type:
```go
package main
import "github.com/TwiN/whois"
func main() {
client := whois.NewClient()
response, err := client.QueryAndParse("example.com")
if err != nil {
panic(err)
}
println(response.ExpirationDate.String())
}
```
Note that because there is no standardized format for WHOIS responses, this parsing may not be successful for every single TLD.
Currently, the only fields parsed are:
- `ExpirationDate`: The time.Time at which the domain will expire
- `DomainStatuses`: The statuses that the domain currently has (e.g. `clientTransferProhibited`)
- `NameServers`: The nameservers currently tied to the domain
If you'd like one or more other fields to be parsed, please don't be shy and create an issue or a pull request.
#### Caching referral WHOIS servers
The way that WHOIS scales is by having one "main" WHOIS server, namely `whois.iana.org:43`, refer to other WHOIS server
on a per-TLD basis.
In other word, let's say that you wanted to have the WHOIS information for `example.com`.
The first step would be to query `whois.iana.org:43` with `com`, which would return `whois.verisign-grs.com`.
Then, you would query `whois.verisign-grs.com:43` for the WHOIS information on `example.com`.
If you're querying a lot of servers, making two queries instead of one can be a little wasteful, hence `WithReferralCache(true)`:
```go
client := whois.NewClient().WithReferralCache(true)
```
The above will cache the referral WHOIS server for each TLD, so that you can directly query the appropriate WHOIS server
instead of first querying `whois.iana.org:43` for the referral.

130
vendor/github.com/TwiN/whois/whois.go generated vendored
View File

@@ -1,130 +0,0 @@
package whois
import (
"io"
"net"
"strings"
"time"
)
const (
ianaWHOISServerAddress = "whois.iana.org:43"
)
type Client struct {
whoisServerAddress string
isCachingReferralWHOISServers bool
referralWHOISServersCache map[string]string
}
func NewClient() *Client {
return &Client{
whoisServerAddress: ianaWHOISServerAddress,
referralWHOISServersCache: make(map[string]string),
}
}
// WithReferralCache allows you to enable or disable the referral WHOIS server cache.
// While ianaWHOISServerAddress is the "entry point" for WHOIS queries, it sometimes has
// availability issues. One way to mitigate this is to cache the referral WHOIS server.
//
// This is disabled by default
func (c *Client) WithReferralCache(enabled bool) *Client {
c.isCachingReferralWHOISServers = enabled
if enabled {
// We'll set a couple of common ones right away to avoid unnecessary queries
c.referralWHOISServersCache = map[string]string{
"com": "whois.verisign-grs.com",
"black": "whois.nic.black",
"dev": "whois.nic.google",
"green": "whois.nic.green",
"io": "whois.nic.io",
"net": "whois.verisign-grs.com",
"org": "whois.publicinterestregistry.org",
"red": "whois.nic.red",
"sh": "whois.nic.sh",
}
}
return c
}
func (c *Client) Query(domain string) (string, error) {
parts := strings.Split(domain, ".")
if c.isCachingReferralWHOISServers {
if cachedWHOISServer, ok := c.referralWHOISServersCache[domain]; ok {
return c.query(cachedWHOISServer, domain)
}
}
output, err := c.query(c.whoisServerAddress, parts[len(parts)-1])
if err != nil {
return "", err
}
if strings.Contains(output, "whois:") {
startIndex := strings.Index(output, "whois:") + 6
endIndex := strings.Index(output[startIndex:], "\n") + startIndex
whois := strings.TrimSpace(output[startIndex:endIndex])
if referOutput, err := c.query(whois+":43", domain); err == nil {
if c.isCachingReferralWHOISServers {
c.referralWHOISServersCache[domain] = whois + ":43"
}
return referOutput, nil
}
return "", err
}
return output, nil
}
func (c Client) query(whoisServerAddress, domain string) (string, error) {
connection, err := net.DialTimeout("tcp", whoisServerAddress, 10*time.Second)
if err != nil {
return "", err
}
defer connection.Close()
_ = connection.SetDeadline(time.Now().Add(5 * time.Second))
_, err = connection.Write([]byte(domain + "\r\n"))
if err != nil {
return "", err
}
output, err := io.ReadAll(connection)
if err != nil {
return "", err
}
return string(output), nil
}
type Response struct {
ExpirationDate time.Time
DomainStatuses []string
NameServers []string
}
// QueryAndParse tries to parse the response from the WHOIS server
// There is no standardized format for WHOIS responses, so this is an attempt at best.
//
// Being the selfish person that I am, I also only parse the fields that I need.
// If you need more fields, please open an issue or pull request.
func (c Client) QueryAndParse(domain string) (*Response, error) {
text, err := c.Query(domain)
if err != nil {
return nil, err
}
response := Response{}
for _, line := range strings.Split(text, "\n") {
line = strings.TrimSpace(line)
valueStartIndex := strings.Index(line, ":")
if valueStartIndex == -1 {
continue
}
key := strings.ToLower(strings.TrimSpace(line[:valueStartIndex]))
value := strings.TrimSpace(line[valueStartIndex+1:])
if response.ExpirationDate.Unix() != 0 && strings.Contains(key, "expir") && strings.Contains(key, "date") {
response.ExpirationDate, _ = time.Parse(time.RFC3339, strings.ToUpper(value))
} else if strings.Contains(key, "domain status") {
response.DomainStatuses = append(response.DomainStatuses, value)
} else if strings.Contains(key, "name server") {
response.NameServers = append(response.NameServers, value)
}
}
return &response, nil
}

View File

@@ -1,20 +0,0 @@
Copyright (C) 2013 Blake Mizerany
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -1,316 +0,0 @@
// Package quantile computes approximate quantiles over an unbounded data
// stream within low memory and CPU bounds.
//
// A small amount of accuracy is traded to achieve the above properties.
//
// Multiple streams can be merged before calling Query to generate a single set
// of results. This is meaningful when the streams represent the same type of
// data. See Merge and Samples.
//
// For more detailed information about the algorithm used, see:
//
// Effective Computation of Biased Quantiles over Data Streams
//
// http://www.cs.rutgers.edu/~muthu/bquant.pdf
package quantile
import (
"math"
"sort"
)
// Sample holds an observed value and meta information for compression. JSON
// tags have been added for convenience.
type Sample struct {
Value float64 `json:",string"`
Width float64 `json:",string"`
Delta float64 `json:",string"`
}
// Samples represents a slice of samples. It implements sort.Interface.
type Samples []Sample
func (a Samples) Len() int { return len(a) }
func (a Samples) Less(i, j int) bool { return a[i].Value < a[j].Value }
func (a Samples) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type invariant func(s *stream, r float64) float64
// NewLowBiased returns an initialized Stream for low-biased quantiles
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
// error guarantees can still be given even for the lower ranks of the data
// distribution.
//
// The provided epsilon is a relative error, i.e. the true quantile of a value
// returned by a query is guaranteed to be within (1±Epsilon)*Quantile.
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
// properties.
func NewLowBiased(epsilon float64) *Stream {
ƒ := func(s *stream, r float64) float64 {
return 2 * epsilon * r
}
return newStream(ƒ)
}
// NewHighBiased returns an initialized Stream for high-biased quantiles
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
// error guarantees can still be given even for the higher ranks of the data
// distribution.
//
// The provided epsilon is a relative error, i.e. the true quantile of a value
// returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile).
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
// properties.
func NewHighBiased(epsilon float64) *Stream {
ƒ := func(s *stream, r float64) float64 {
return 2 * epsilon * (s.n - r)
}
return newStream(ƒ)
}
// NewTargeted returns an initialized Stream concerned with a particular set of
// quantile values that are supplied a priori. Knowing these a priori reduces
// space and computation time. The targets map maps the desired quantiles to
// their absolute errors, i.e. the true quantile of a value returned by a query
// is guaranteed to be within (Quantile±Epsilon).
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties.
func NewTargeted(targetMap map[float64]float64) *Stream {
// Convert map to slice to avoid slow iterations on a map.
// ƒ is called on the hot path, so converting the map to a slice
// beforehand results in significant CPU savings.
targets := targetMapToSlice(targetMap)
ƒ := func(s *stream, r float64) float64 {
var m = math.MaxFloat64
var f float64
for _, t := range targets {
if t.quantile*s.n <= r {
f = (2 * t.epsilon * r) / t.quantile
} else {
f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile)
}
if f < m {
m = f
}
}
return m
}
return newStream(ƒ)
}
type target struct {
quantile float64
epsilon float64
}
func targetMapToSlice(targetMap map[float64]float64) []target {
targets := make([]target, 0, len(targetMap))
for quantile, epsilon := range targetMap {
t := target{
quantile: quantile,
epsilon: epsilon,
}
targets = append(targets, t)
}
return targets
}
// Stream computes quantiles for a stream of float64s. It is not thread-safe by
// design. Take care when using across multiple goroutines.
type Stream struct {
*stream
b Samples
sorted bool
}
func newStream(ƒ invariant) *Stream {
x := &stream{ƒ: ƒ}
return &Stream{x, make(Samples, 0, 500), true}
}
// Insert inserts v into the stream.
func (s *Stream) Insert(v float64) {
s.insert(Sample{Value: v, Width: 1})
}
func (s *Stream) insert(sample Sample) {
s.b = append(s.b, sample)
s.sorted = false
if len(s.b) == cap(s.b) {
s.flush()
}
}
// Query returns the computed qth percentiles value. If s was created with
// NewTargeted, and q is not in the set of quantiles provided a priori, Query
// will return an unspecified result.
func (s *Stream) Query(q float64) float64 {
if !s.flushed() {
// Fast path when there hasn't been enough data for a flush;
// this also yields better accuracy for small sets of data.
l := len(s.b)
if l == 0 {
return 0
}
i := int(math.Ceil(float64(l) * q))
if i > 0 {
i -= 1
}
s.maybeSort()
return s.b[i].Value
}
s.flush()
return s.stream.query(q)
}
// Merge merges samples into the underlying streams samples. This is handy when
// merging multiple streams from separate threads, database shards, etc.
//
// ATTENTION: This method is broken and does not yield correct results. The
// underlying algorithm is not capable of merging streams correctly.
func (s *Stream) Merge(samples Samples) {
sort.Sort(samples)
s.stream.merge(samples)
}
// Reset reinitializes and clears the list reusing the samples buffer memory.
func (s *Stream) Reset() {
s.stream.reset()
s.b = s.b[:0]
}
// Samples returns stream samples held by s.
func (s *Stream) Samples() Samples {
if !s.flushed() {
return s.b
}
s.flush()
return s.stream.samples()
}
// Count returns the total number of samples observed in the stream
// since initialization.
func (s *Stream) Count() int {
return len(s.b) + s.stream.count()
}
func (s *Stream) flush() {
s.maybeSort()
s.stream.merge(s.b)
s.b = s.b[:0]
}
func (s *Stream) maybeSort() {
if !s.sorted {
s.sorted = true
sort.Sort(s.b)
}
}
func (s *Stream) flushed() bool {
return len(s.stream.l) > 0
}
type stream struct {
n float64
l []Sample
ƒ invariant
}
func (s *stream) reset() {
s.l = s.l[:0]
s.n = 0
}
func (s *stream) insert(v float64) {
s.merge(Samples{{v, 1, 0}})
}
func (s *stream) merge(samples Samples) {
// TODO(beorn7): This tries to merge not only individual samples, but
// whole summaries. The paper doesn't mention merging summaries at
// all. Unittests show that the merging is inaccurate. Find out how to
// do merges properly.
var r float64
i := 0
for _, sample := range samples {
for ; i < len(s.l); i++ {
c := s.l[i]
if c.Value > sample.Value {
// Insert at position i.
s.l = append(s.l, Sample{})
copy(s.l[i+1:], s.l[i:])
s.l[i] = Sample{
sample.Value,
sample.Width,
math.Max(sample.Delta, math.Floor(s.ƒ(s, r))-1),
// TODO(beorn7): How to calculate delta correctly?
}
i++
goto inserted
}
r += c.Width
}
s.l = append(s.l, Sample{sample.Value, sample.Width, 0})
i++
inserted:
s.n += sample.Width
r += sample.Width
}
s.compress()
}
func (s *stream) count() int {
return int(s.n)
}
func (s *stream) query(q float64) float64 {
t := math.Ceil(q * s.n)
t += math.Ceil(s.ƒ(s, t) / 2)
p := s.l[0]
var r float64
for _, c := range s.l[1:] {
r += p.Width
if r+c.Width+c.Delta > t {
return p.Value
}
p = c
}
return p.Value
}
func (s *stream) compress() {
if len(s.l) < 2 {
return
}
x := s.l[len(s.l)-1]
xi := len(s.l) - 1
r := s.n - 1 - x.Width
for i := len(s.l) - 2; i >= 0; i-- {
c := s.l[i]
if c.Width+x.Width+x.Delta <= s.ƒ(s, r) {
x.Width += c.Width
s.l[xi] = x
// Remove element at i.
copy(s.l[i:], s.l[i+1:])
s.l = s.l[:len(s.l)-1]
xi -= 1
} else {
x = c
xi = i
}
r -= c.Width
}
}
func (s *stream) samples() Samples {
samples := make(Samples, len(s.l))
copy(samples, s.l)
return samples
}

View File

@@ -1,22 +0,0 @@
Copyright (c) 2016 Caleb Spare
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,69 +0,0 @@
# xxhash
[![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2)
[![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml)
xxhash is a Go implementation of the 64-bit
[xxHash](http://cyan4973.github.io/xxHash/) algorithm, XXH64. This is a
high-quality hashing algorithm that is much faster than anything in the Go
standard library.
This package provides a straightforward API:
```
func Sum64(b []byte) uint64
func Sum64String(s string) uint64
type Digest struct{ ... }
func New() *Digest
```
The `Digest` type implements hash.Hash64. Its key methods are:
```
func (*Digest) Write([]byte) (int, error)
func (*Digest) WriteString(string) (int, error)
func (*Digest) Sum64() uint64
```
This implementation provides a fast pure-Go implementation and an even faster
assembly implementation for amd64.
## Compatibility
This package is in a module and the latest code is in version 2 of the module.
You need a version of Go with at least "minimal module compatibility" to use
github.com/cespare/xxhash/v2:
* 1.9.7+ for Go 1.9
* 1.10.3+ for Go 1.10
* Go 1.11 or later
I recommend using the latest release of Go.
## Benchmarks
Here are some quick benchmarks comparing the pure-Go and assembly
implementations of Sum64.
| input size | purego | asm |
| --- | --- | --- |
| 5 B | 979.66 MB/s | 1291.17 MB/s |
| 100 B | 7475.26 MB/s | 7973.40 MB/s |
| 4 KB | 17573.46 MB/s | 17602.65 MB/s |
| 10 MB | 17131.46 MB/s | 17142.16 MB/s |
These numbers were generated on Ubuntu 18.04 with an Intel i7-8700K CPU using
the following commands under Go 1.11.2:
```
$ go test -tags purego -benchtime 10s -bench '/xxhash,direct,bytes'
$ go test -benchtime 10s -bench '/xxhash,direct,bytes'
```
## Projects using this package
- [InfluxDB](https://github.com/influxdata/influxdb)
- [Prometheus](https://github.com/prometheus/prometheus)
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
- [FreeCache](https://github.com/coocood/freecache)
- [FastCache](https://github.com/VictoriaMetrics/fastcache)

View File

@@ -1,235 +0,0 @@
// Package xxhash implements the 64-bit variant of xxHash (XXH64) as described
// at http://cyan4973.github.io/xxHash/.
package xxhash
import (
"encoding/binary"
"errors"
"math/bits"
)
const (
prime1 uint64 = 11400714785074694791
prime2 uint64 = 14029467366897019727
prime3 uint64 = 1609587929392839161
prime4 uint64 = 9650029242287828579
prime5 uint64 = 2870177450012600261
)
// NOTE(caleb): I'm using both consts and vars of the primes. Using consts where
// possible in the Go code is worth a small (but measurable) performance boost
// by avoiding some MOVQs. Vars are needed for the asm and also are useful for
// convenience in the Go code in a few places where we need to intentionally
// avoid constant arithmetic (e.g., v1 := prime1 + prime2 fails because the
// result overflows a uint64).
var (
prime1v = prime1
prime2v = prime2
prime3v = prime3
prime4v = prime4
prime5v = prime5
)
// Digest implements hash.Hash64.
type Digest struct {
v1 uint64
v2 uint64
v3 uint64
v4 uint64
total uint64
mem [32]byte
n int // how much of mem is used
}
// New creates a new Digest that computes the 64-bit xxHash algorithm.
func New() *Digest {
var d Digest
d.Reset()
return &d
}
// Reset clears the Digest's state so that it can be reused.
func (d *Digest) Reset() {
d.v1 = prime1v + prime2
d.v2 = prime2
d.v3 = 0
d.v4 = -prime1v
d.total = 0
d.n = 0
}
// Size always returns 8 bytes.
func (d *Digest) Size() int { return 8 }
// BlockSize always returns 32 bytes.
func (d *Digest) BlockSize() int { return 32 }
// Write adds more data to d. It always returns len(b), nil.
func (d *Digest) Write(b []byte) (n int, err error) {
n = len(b)
d.total += uint64(n)
if d.n+n < 32 {
// This new data doesn't even fill the current block.
copy(d.mem[d.n:], b)
d.n += n
return
}
if d.n > 0 {
// Finish off the partial block.
copy(d.mem[d.n:], b)
d.v1 = round(d.v1, u64(d.mem[0:8]))
d.v2 = round(d.v2, u64(d.mem[8:16]))
d.v3 = round(d.v3, u64(d.mem[16:24]))
d.v4 = round(d.v4, u64(d.mem[24:32]))
b = b[32-d.n:]
d.n = 0
}
if len(b) >= 32 {
// One or more full blocks left.
nw := writeBlocks(d, b)
b = b[nw:]
}
// Store any remaining partial block.
copy(d.mem[:], b)
d.n = len(b)
return
}
// Sum appends the current hash to b and returns the resulting slice.
func (d *Digest) Sum(b []byte) []byte {
s := d.Sum64()
return append(
b,
byte(s>>56),
byte(s>>48),
byte(s>>40),
byte(s>>32),
byte(s>>24),
byte(s>>16),
byte(s>>8),
byte(s),
)
}
// Sum64 returns the current hash.
func (d *Digest) Sum64() uint64 {
var h uint64
if d.total >= 32 {
v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4
h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4)
h = mergeRound(h, v1)
h = mergeRound(h, v2)
h = mergeRound(h, v3)
h = mergeRound(h, v4)
} else {
h = d.v3 + prime5
}
h += d.total
i, end := 0, d.n
for ; i+8 <= end; i += 8 {
k1 := round(0, u64(d.mem[i:i+8]))
h ^= k1
h = rol27(h)*prime1 + prime4
}
if i+4 <= end {
h ^= uint64(u32(d.mem[i:i+4])) * prime1
h = rol23(h)*prime2 + prime3
i += 4
}
for i < end {
h ^= uint64(d.mem[i]) * prime5
h = rol11(h) * prime1
i++
}
h ^= h >> 33
h *= prime2
h ^= h >> 29
h *= prime3
h ^= h >> 32
return h
}
const (
magic = "xxh\x06"
marshaledSize = len(magic) + 8*5 + 32
)
// MarshalBinary implements the encoding.BinaryMarshaler interface.
func (d *Digest) MarshalBinary() ([]byte, error) {
b := make([]byte, 0, marshaledSize)
b = append(b, magic...)
b = appendUint64(b, d.v1)
b = appendUint64(b, d.v2)
b = appendUint64(b, d.v3)
b = appendUint64(b, d.v4)
b = appendUint64(b, d.total)
b = append(b, d.mem[:d.n]...)
b = b[:len(b)+len(d.mem)-d.n]
return b, nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
func (d *Digest) UnmarshalBinary(b []byte) error {
if len(b) < len(magic) || string(b[:len(magic)]) != magic {
return errors.New("xxhash: invalid hash state identifier")
}
if len(b) != marshaledSize {
return errors.New("xxhash: invalid hash state size")
}
b = b[len(magic):]
b, d.v1 = consumeUint64(b)
b, d.v2 = consumeUint64(b)
b, d.v3 = consumeUint64(b)
b, d.v4 = consumeUint64(b)
b, d.total = consumeUint64(b)
copy(d.mem[:], b)
d.n = int(d.total % uint64(len(d.mem)))
return nil
}
func appendUint64(b []byte, x uint64) []byte {
var a [8]byte
binary.LittleEndian.PutUint64(a[:], x)
return append(b, a[:]...)
}
func consumeUint64(b []byte) ([]byte, uint64) {
x := u64(b)
return b[8:], x
}
func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) }
func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) }
func round(acc, input uint64) uint64 {
acc += input * prime2
acc = rol31(acc)
acc *= prime1
return acc
}
func mergeRound(acc, val uint64) uint64 {
val = round(0, val)
acc ^= val
acc = acc*prime1 + prime4
return acc
}
func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) }
func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) }
func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) }
func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) }
func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) }
func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) }
func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) }
func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) }

View File

@@ -1,13 +0,0 @@
// +build !appengine
// +build gc
// +build !purego
package xxhash
// Sum64 computes the 64-bit xxHash digest of b.
//
//go:noescape
func Sum64(b []byte) uint64
//go:noescape
func writeBlocks(d *Digest, b []byte) int

View File

@@ -1,215 +0,0 @@
// +build !appengine
// +build gc
// +build !purego
#include "textflag.h"
// Register allocation:
// AX h
// SI pointer to advance through b
// DX n
// BX loop end
// R8 v1, k1
// R9 v2
// R10 v3
// R11 v4
// R12 tmp
// R13 prime1v
// R14 prime2v
// DI prime4v
// round reads from and advances the buffer pointer in SI.
// It assumes that R13 has prime1v and R14 has prime2v.
#define round(r) \
MOVQ (SI), R12 \
ADDQ $8, SI \
IMULQ R14, R12 \
ADDQ R12, r \
ROLQ $31, r \
IMULQ R13, r
// mergeRound applies a merge round on the two registers acc and val.
// It assumes that R13 has prime1v, R14 has prime2v, and DI has prime4v.
#define mergeRound(acc, val) \
IMULQ R14, val \
ROLQ $31, val \
IMULQ R13, val \
XORQ val, acc \
IMULQ R13, acc \
ADDQ DI, acc
// func Sum64(b []byte) uint64
TEXT ·Sum64(SB), NOSPLIT, $0-32
// Load fixed primes.
MOVQ ·prime1v(SB), R13
MOVQ ·prime2v(SB), R14
MOVQ ·prime4v(SB), DI
// Load slice.
MOVQ b_base+0(FP), SI
MOVQ b_len+8(FP), DX
LEAQ (SI)(DX*1), BX
// The first loop limit will be len(b)-32.
SUBQ $32, BX
// Check whether we have at least one block.
CMPQ DX, $32
JLT noBlocks
// Set up initial state (v1, v2, v3, v4).
MOVQ R13, R8
ADDQ R14, R8
MOVQ R14, R9
XORQ R10, R10
XORQ R11, R11
SUBQ R13, R11
// Loop until SI > BX.
blockLoop:
round(R8)
round(R9)
round(R10)
round(R11)
CMPQ SI, BX
JLE blockLoop
MOVQ R8, AX
ROLQ $1, AX
MOVQ R9, R12
ROLQ $7, R12
ADDQ R12, AX
MOVQ R10, R12
ROLQ $12, R12
ADDQ R12, AX
MOVQ R11, R12
ROLQ $18, R12
ADDQ R12, AX
mergeRound(AX, R8)
mergeRound(AX, R9)
mergeRound(AX, R10)
mergeRound(AX, R11)
JMP afterBlocks
noBlocks:
MOVQ ·prime5v(SB), AX
afterBlocks:
ADDQ DX, AX
// Right now BX has len(b)-32, and we want to loop until SI > len(b)-8.
ADDQ $24, BX
CMPQ SI, BX
JG fourByte
wordLoop:
// Calculate k1.
MOVQ (SI), R8
ADDQ $8, SI
IMULQ R14, R8
ROLQ $31, R8
IMULQ R13, R8
XORQ R8, AX
ROLQ $27, AX
IMULQ R13, AX
ADDQ DI, AX
CMPQ SI, BX
JLE wordLoop
fourByte:
ADDQ $4, BX
CMPQ SI, BX
JG singles
MOVL (SI), R8
ADDQ $4, SI
IMULQ R13, R8
XORQ R8, AX
ROLQ $23, AX
IMULQ R14, AX
ADDQ ·prime3v(SB), AX
singles:
ADDQ $4, BX
CMPQ SI, BX
JGE finalize
singlesLoop:
MOVBQZX (SI), R12
ADDQ $1, SI
IMULQ ·prime5v(SB), R12
XORQ R12, AX
ROLQ $11, AX
IMULQ R13, AX
CMPQ SI, BX
JL singlesLoop
finalize:
MOVQ AX, R12
SHRQ $33, R12
XORQ R12, AX
IMULQ R14, AX
MOVQ AX, R12
SHRQ $29, R12
XORQ R12, AX
IMULQ ·prime3v(SB), AX
MOVQ AX, R12
SHRQ $32, R12
XORQ R12, AX
MOVQ AX, ret+24(FP)
RET
// writeBlocks uses the same registers as above except that it uses AX to store
// the d pointer.
// func writeBlocks(d *Digest, b []byte) int
TEXT ·writeBlocks(SB), NOSPLIT, $0-40
// Load fixed primes needed for round.
MOVQ ·prime1v(SB), R13
MOVQ ·prime2v(SB), R14
// Load slice.
MOVQ b_base+8(FP), SI
MOVQ b_len+16(FP), DX
LEAQ (SI)(DX*1), BX
SUBQ $32, BX
// Load vN from d.
MOVQ d+0(FP), AX
MOVQ 0(AX), R8 // v1
MOVQ 8(AX), R9 // v2
MOVQ 16(AX), R10 // v3
MOVQ 24(AX), R11 // v4
// We don't need to check the loop condition here; this function is
// always called with at least one block of data to process.
blockLoop:
round(R8)
round(R9)
round(R10)
round(R11)
CMPQ SI, BX
JLE blockLoop
// Copy vN back to d.
MOVQ R8, 0(AX)
MOVQ R9, 8(AX)
MOVQ R10, 16(AX)
MOVQ R11, 24(AX)
// The number of bytes written is SI minus the old base pointer.
SUBQ b_base+8(FP), SI
MOVQ SI, ret+32(FP)
RET

View File

@@ -1,76 +0,0 @@
// +build !amd64 appengine !gc purego
package xxhash
// Sum64 computes the 64-bit xxHash digest of b.
func Sum64(b []byte) uint64 {
// A simpler version would be
// d := New()
// d.Write(b)
// return d.Sum64()
// but this is faster, particularly for small inputs.
n := len(b)
var h uint64
if n >= 32 {
v1 := prime1v + prime2
v2 := prime2
v3 := uint64(0)
v4 := -prime1v
for len(b) >= 32 {
v1 = round(v1, u64(b[0:8:len(b)]))
v2 = round(v2, u64(b[8:16:len(b)]))
v3 = round(v3, u64(b[16:24:len(b)]))
v4 = round(v4, u64(b[24:32:len(b)]))
b = b[32:len(b):len(b)]
}
h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4)
h = mergeRound(h, v1)
h = mergeRound(h, v2)
h = mergeRound(h, v3)
h = mergeRound(h, v4)
} else {
h = prime5
}
h += uint64(n)
i, end := 0, len(b)
for ; i+8 <= end; i += 8 {
k1 := round(0, u64(b[i:i+8:len(b)]))
h ^= k1
h = rol27(h)*prime1 + prime4
}
if i+4 <= end {
h ^= uint64(u32(b[i:i+4:len(b)])) * prime1
h = rol23(h)*prime2 + prime3
i += 4
}
for ; i < end; i++ {
h ^= uint64(b[i]) * prime5
h = rol11(h) * prime1
}
h ^= h >> 33
h *= prime2
h ^= h >> 29
h *= prime3
h ^= h >> 32
return h
}
func writeBlocks(d *Digest, b []byte) int {
v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4
n := len(b)
for len(b) >= 32 {
v1 = round(v1, u64(b[0:8:len(b)]))
v2 = round(v2, u64(b[8:16:len(b)]))
v3 = round(v3, u64(b[16:24:len(b)]))
v4 = round(v4, u64(b[24:32:len(b)]))
b = b[32:len(b):len(b)]
}
d.v1, d.v2, d.v3, d.v4 = v1, v2, v3, v4
return n - len(b)
}

View File

@@ -1,15 +0,0 @@
// +build appengine
// This file contains the safe implementations of otherwise unsafe-using code.
package xxhash
// Sum64String computes the 64-bit xxHash digest of s.
func Sum64String(s string) uint64 {
return Sum64([]byte(s))
}
// WriteString adds more data to d. It always returns len(s), nil.
func (d *Digest) WriteString(s string) (n int, err error) {
return d.Write([]byte(s))
}

View File

@@ -1,57 +0,0 @@
// +build !appengine
// This file encapsulates usage of unsafe.
// xxhash_safe.go contains the safe implementations.
package xxhash
import (
"unsafe"
)
// In the future it's possible that compiler optimizations will make these
// XxxString functions unnecessary by realizing that calls such as
// Sum64([]byte(s)) don't need to copy s. See https://golang.org/issue/2205.
// If that happens, even if we keep these functions they can be replaced with
// the trivial safe code.
// NOTE: The usual way of doing an unsafe string-to-[]byte conversion is:
//
// var b []byte
// bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
// bh.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
// bh.Len = len(s)
// bh.Cap = len(s)
//
// Unfortunately, as of Go 1.15.3 the inliner's cost model assigns a high enough
// weight to this sequence of expressions that any function that uses it will
// not be inlined. Instead, the functions below use a different unsafe
// conversion designed to minimize the inliner weight and allow both to be
// inlined. There is also a test (TestInlining) which verifies that these are
// inlined.
//
// See https://github.com/golang/go/issues/42739 for discussion.
// Sum64String computes the 64-bit xxHash digest of s.
// It may be faster than Sum64([]byte(s)) by avoiding a copy.
func Sum64String(s string) uint64 {
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))
return Sum64(b)
}
// WriteString adds more data to d. It always returns len(s), nil.
// It may be faster than Write([]byte(s)) by avoiding a copy.
func (d *Digest) WriteString(s string) (n int, err error) {
d.Write(*(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)})))
// d.Write always returns len(s), nil.
// Ignoring the return output and returning these fixed values buys a
// savings of 6 in the inliner's cost model.
return len(s), nil
}
// sliceHeader is similar to reflect.SliceHeader, but it assumes that the layout
// of the first two words is the same as the layout of a string.
type sliceHeader struct {
s string
cap int
}

View File

@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"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.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"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.
"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).
"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.
"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."
"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.
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.
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.
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:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(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
(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.
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.
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.
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.
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.
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.
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.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
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.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,5 +0,0 @@
CoreOS Project
Copyright 2014 CoreOS, Inc
This product includes software developed at CoreOS, Inc.
(http://www.coreos.com/).

View File

@@ -1,16 +0,0 @@
package oidc
// JOSE asymmetric signing algorithm values as defined by RFC 7518
//
// see: https://tools.ietf.org/html/rfc7518#section-3.1
const (
RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256
RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384
RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512
ES256 = "ES256" // ECDSA using P-256 and SHA-256
ES384 = "ES384" // ECDSA using P-384 and SHA-384
ES512 = "ES512" // ECDSA using P-521 and SHA-512
PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256
PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384
PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512
)

View File

@@ -1,248 +0,0 @@
package oidc
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"errors"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
jose "gopkg.in/square/go-jose.v2"
)
// StaticKeySet is a verifier that validates JWT against a static set of public keys.
type StaticKeySet struct {
// PublicKeys used to verify the JWT. Supported types are *rsa.PublicKey and
// *ecdsa.PublicKey.
PublicKeys []crypto.PublicKey
}
// VerifySignature compares the signature against a static set of public keys.
func (s *StaticKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
jws, err := jose.ParseSigned(jwt)
if err != nil {
return nil, fmt.Errorf("parsing jwt: %v", err)
}
for _, pub := range s.PublicKeys {
switch pub.(type) {
case *rsa.PublicKey:
case *ecdsa.PublicKey:
default:
return nil, fmt.Errorf("invalid public key type provided: %T", pub)
}
payload, err := jws.Verify(pub)
if err != nil {
continue
}
return payload, nil
}
return nil, fmt.Errorf("no public keys able to verify jwt")
}
// NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP
// GETs to fetch JSON web token sets hosted at a remote URL. This is automatically
// used by NewProvider using the URLs returned by OpenID Connect discovery, but is
// exposed for providers that don't support discovery or to prevent round trips to the
// discovery URL.
//
// The returned KeySet is a long lived verifier that caches keys based on any
// keys change. Reuse a common remote key set instead of creating new ones as needed.
func NewRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet {
return newRemoteKeySet(ctx, jwksURL, time.Now)
}
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *RemoteKeySet {
if now == nil {
now = time.Now
}
return &RemoteKeySet{jwksURL: jwksURL, ctx: cloneContext(ctx), now: now}
}
// RemoteKeySet is a KeySet implementation that validates JSON web tokens against
// a jwks_uri endpoint.
type RemoteKeySet struct {
jwksURL string
ctx context.Context
now func() time.Time
// guard all other fields
mu sync.RWMutex
// inflight suppresses parallel execution of updateKeys and allows
// multiple goroutines to wait for its result.
inflight *inflight
// A set of cached keys.
cachedKeys []jose.JSONWebKey
}
// inflight is used to wait on some in-flight request from multiple goroutines.
type inflight struct {
doneCh chan struct{}
keys []jose.JSONWebKey
err error
}
func newInflight() *inflight {
return &inflight{doneCh: make(chan struct{})}
}
// wait returns a channel that multiple goroutines can receive on. Once it returns
// a value, the inflight request is done and result() can be inspected.
func (i *inflight) wait() <-chan struct{} {
return i.doneCh
}
// done can only be called by a single goroutine. It records the result of the
// inflight request and signals other goroutines that the result is safe to
// inspect.
func (i *inflight) done(keys []jose.JSONWebKey, err error) {
i.keys = keys
i.err = err
close(i.doneCh)
}
// result cannot be called until the wait() channel has returned a value.
func (i *inflight) result() ([]jose.JSONWebKey, error) {
return i.keys, i.err
}
// paresdJWTKey is a context key that allows common setups to avoid parsing the
// JWT twice. It holds a *jose.JSONWebSignature value.
var parsedJWTKey contextKey
// VerifySignature validates a payload against a signature from the jwks_uri.
//
// Users MUST NOT call this method directly and should use an IDTokenVerifier
// instead. This method skips critical validations such as 'alg' values and is
// only exported to implement the KeySet interface.
func (r *RemoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
jws, ok := ctx.Value(parsedJWTKey).(*jose.JSONWebSignature)
if !ok {
var err error
jws, err = jose.ParseSigned(jwt)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
}
return r.verify(ctx, jws)
}
func (r *RemoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
// We don't support JWTs signed with multiple signatures.
keyID := ""
for _, sig := range jws.Signatures {
keyID = sig.Header.KeyID
break
}
keys := r.keysFromCache()
for _, key := range keys {
if keyID == "" || key.KeyID == keyID {
if payload, err := jws.Verify(&key); err == nil {
return payload, nil
}
}
}
// If the kid doesn't match, check for new keys from the remote. This is the
// strategy recommended by the spec.
//
// https://openid.net/specs/openid-connect-core-1_0.html#RotateSigKeys
keys, err := r.keysFromRemote(ctx)
if err != nil {
return nil, fmt.Errorf("fetching keys %v", err)
}
for _, key := range keys {
if keyID == "" || key.KeyID == keyID {
if payload, err := jws.Verify(&key); err == nil {
return payload, nil
}
}
}
return nil, errors.New("failed to verify id token signature")
}
func (r *RemoteKeySet) keysFromCache() (keys []jose.JSONWebKey) {
r.mu.RLock()
defer r.mu.RUnlock()
return r.cachedKeys
}
// keysFromRemote syncs the key set from the remote set, records the values in the
// cache, and returns the key set.
func (r *RemoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) {
// Need to lock to inspect the inflight request field.
r.mu.Lock()
// If there's not a current inflight request, create one.
if r.inflight == nil {
r.inflight = newInflight()
// This goroutine has exclusive ownership over the current inflight
// request. It releases the resource by nil'ing the inflight field
// once the goroutine is done.
go func() {
// Sync keys and finish inflight when that's done.
keys, err := r.updateKeys()
r.inflight.done(keys, err)
// Lock to update the keys and indicate that there is no longer an
// inflight request.
r.mu.Lock()
defer r.mu.Unlock()
if err == nil {
r.cachedKeys = keys
}
// Free inflight so a different request can run.
r.inflight = nil
}()
}
inflight := r.inflight
r.mu.Unlock()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-inflight.wait():
return inflight.result()
}
}
func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) {
req, err := http.NewRequest("GET", r.jwksURL, nil)
if err != nil {
return nil, fmt.Errorf("oidc: can't create request: %v", err)
}
resp, err := doRequest(r.ctx, req)
if err != nil {
return nil, fmt.Errorf("oidc: get keys failed %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body)
}
var keySet jose.JSONWebKeySet
err = unmarshalResp(resp, body, &keySet)
if err != nil {
return nil, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body)
}
return keySet.Keys, nil
}

View File

@@ -1,522 +0,0 @@
// Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package.
package oidc
import (
"context"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"hash"
"io/ioutil"
"mime"
"net/http"
"strings"
"time"
"golang.org/x/oauth2"
)
const (
// ScopeOpenID is the mandatory scope for all OpenID Connect OAuth2 requests.
ScopeOpenID = "openid"
// ScopeOfflineAccess is an optional scope defined by OpenID Connect for requesting
// OAuth2 refresh tokens.
//
// Support for this scope differs between OpenID Connect providers. For instance
// Google rejects it, favoring appending "access_type=offline" as part of the
// authorization request instead.
//
// See: https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
ScopeOfflineAccess = "offline_access"
)
var (
errNoAtHash = errors.New("id token did not have an access token hash")
errInvalidAtHash = errors.New("access token hash does not match value in ID token")
)
type contextKey int
var issuerURLKey contextKey
// ClientContext returns a new Context that carries the provided HTTP client.
//
// This method sets the same context key used by the golang.org/x/oauth2 package,
// so the returned context works for that package too.
//
// myClient := &http.Client{}
// ctx := oidc.ClientContext(parentContext, myClient)
//
// // This will use the custom client
// provider, err := oidc.NewProvider(ctx, "https://accounts.example.com")
//
func ClientContext(ctx context.Context, client *http.Client) context.Context {
return context.WithValue(ctx, oauth2.HTTPClient, client)
}
// cloneContext copies a context's bag-of-values into a new context that isn't
// associated with its cancellation. This is used to initialize remote keys sets
// which run in the background and aren't associated with the initial context.
func cloneContext(ctx context.Context) context.Context {
cp := context.Background()
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
cp = ClientContext(cp, c)
}
return cp
}
// InsecureIssuerURLContext allows discovery to work when the issuer_url reported
// by upstream is mismatched with the discovery URL. This is meant for integration
// with off-spec providers such as Azure.
//
// discoveryBaseURL := "https://login.microsoftonline.com/organizations/v2.0"
// issuerURL := "https://login.microsoftonline.com/my-tenantid/v2.0"
//
// ctx := oidc.InsecureIssuerURLContext(parentContext, issuerURL)
//
// // Provider will be discovered with the discoveryBaseURL, but use issuerURL
// // for future issuer validation.
// provider, err := oidc.NewProvider(ctx, discoveryBaseURL)
//
// This is insecure because validating the correct issuer is critical for multi-tenant
// proivders. Any overrides here MUST be carefully reviewed.
func InsecureIssuerURLContext(ctx context.Context, issuerURL string) context.Context {
return context.WithValue(ctx, issuerURLKey, issuerURL)
}
func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
client := http.DefaultClient
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
client = c
}
return client.Do(req.WithContext(ctx))
}
// Provider represents an OpenID Connect server's configuration.
type Provider struct {
issuer string
authURL string
tokenURL string
userInfoURL string
algorithms []string
// Raw claims returned by the server.
rawClaims []byte
remoteKeySet KeySet
}
type providerJSON struct {
Issuer string `json:"issuer"`
AuthURL string `json:"authorization_endpoint"`
TokenURL string `json:"token_endpoint"`
JWKSURL string `json:"jwks_uri"`
UserInfoURL string `json:"userinfo_endpoint"`
Algorithms []string `json:"id_token_signing_alg_values_supported"`
}
// supportedAlgorithms is a list of algorithms explicitly supported by this
// package. If a provider supports other algorithms, such as HS256 or none,
// those values won't be passed to the IDTokenVerifier.
var supportedAlgorithms = map[string]bool{
RS256: true,
RS384: true,
RS512: true,
ES256: true,
ES384: true,
ES512: true,
PS256: true,
PS384: true,
PS512: true,
}
// ProviderConfig allows creating providers when discovery isn't supported. It's
// generally easier to use NewProvider directly.
type ProviderConfig struct {
// IssuerURL is the identity of the provider, and the string it uses to sign
// ID tokens with. For example "https://accounts.google.com". This value MUST
// match ID tokens exactly.
IssuerURL string
// AuthURL is the endpoint used by the provider to support the OAuth 2.0
// authorization endpoint.
AuthURL string
// TokenURL is the endpoint used by the provider to support the OAuth 2.0
// token endpoint.
TokenURL string
// UserInfoURL is the endpoint used by the provider to support the OpenID
// Connect UserInfo flow.
//
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
UserInfoURL string
// JWKSURL is the endpoint used by the provider to advertise public keys to
// verify issued ID tokens. This endpoint is polled as new keys are made
// available.
JWKSURL string
// Algorithms, if provided, indicate a list of JWT algorithms allowed to sign
// ID tokens. If not provided, this defaults to the algorithms advertised by
// the JWK endpoint, then the set of algorithms supported by this package.
Algorithms []string
}
// NewProvider initializes a provider from a set of endpoints, rather than
// through discovery.
func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider {
return &Provider{
issuer: p.IssuerURL,
authURL: p.AuthURL,
tokenURL: p.TokenURL,
userInfoURL: p.UserInfoURL,
algorithms: p.Algorithms,
remoteKeySet: NewRemoteKeySet(cloneContext(ctx), p.JWKSURL),
}
}
// NewProvider uses the OpenID Connect discovery mechanism to construct a Provider.
//
// The issuer is the URL identifier for the service. For example: "https://accounts.google.com"
// or "https://login.salesforce.com".
func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
req, err := http.NewRequest("GET", wellKnown, nil)
if err != nil {
return nil, err
}
resp, err := doRequest(ctx, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", resp.Status, body)
}
var p providerJSON
err = unmarshalResp(resp, body, &p)
if err != nil {
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
}
issuerURL, skipIssuerValidation := ctx.Value(issuerURLKey).(string)
if !skipIssuerValidation {
issuerURL = issuer
}
if p.Issuer != issuerURL && !skipIssuerValidation {
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer)
}
var algs []string
for _, a := range p.Algorithms {
if supportedAlgorithms[a] {
algs = append(algs, a)
}
}
return &Provider{
issuer: issuerURL,
authURL: p.AuthURL,
tokenURL: p.TokenURL,
userInfoURL: p.UserInfoURL,
algorithms: algs,
rawClaims: body,
remoteKeySet: NewRemoteKeySet(cloneContext(ctx), p.JWKSURL),
}, nil
}
// Claims unmarshals raw fields returned by the server during discovery.
//
// var claims struct {
// ScopesSupported []string `json:"scopes_supported"`
// ClaimsSupported []string `json:"claims_supported"`
// }
//
// if err := provider.Claims(&claims); err != nil {
// // handle unmarshaling error
// }
//
// For a list of fields defined by the OpenID Connect spec see:
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
func (p *Provider) Claims(v interface{}) error {
if p.rawClaims == nil {
return errors.New("oidc: claims not set")
}
return json.Unmarshal(p.rawClaims, v)
}
// Endpoint returns the OAuth2 auth and token endpoints for the given provider.
func (p *Provider) Endpoint() oauth2.Endpoint {
return oauth2.Endpoint{AuthURL: p.authURL, TokenURL: p.tokenURL}
}
// UserInfo represents the OpenID Connect userinfo claims.
type UserInfo struct {
Subject string `json:"sub"`
Profile string `json:"profile"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
claims []byte
}
type userInfoRaw struct {
Subject string `json:"sub"`
Profile string `json:"profile"`
Email string `json:"email"`
// Handle providers that return email_verified as a string
// https://forums.aws.amazon.com/thread.jspa?messageID=949441&#949441 and
// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
EmailVerified stringAsBool `json:"email_verified"`
}
// Claims unmarshals the raw JSON object claims into the provided object.
func (u *UserInfo) Claims(v interface{}) error {
if u.claims == nil {
return errors.New("oidc: claims not set")
}
return json.Unmarshal(u.claims, v)
}
// UserInfo uses the token source to query the provider's user info endpoint.
func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) {
if p.userInfoURL == "" {
return nil, errors.New("oidc: user info endpoint is not supported by this provider")
}
req, err := http.NewRequest("GET", p.userInfoURL, nil)
if err != nil {
return nil, fmt.Errorf("oidc: create GET request: %v", err)
}
token, err := tokenSource.Token()
if err != nil {
return nil, fmt.Errorf("oidc: get access token: %v", err)
}
token.SetAuthHeader(req)
resp, err := doRequest(ctx, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", resp.Status, body)
}
ct := resp.Header.Get("Content-Type")
mediaType, _, parseErr := mime.ParseMediaType(ct)
if parseErr == nil && mediaType == "application/jwt" {
payload, err := p.remoteKeySet.VerifySignature(ctx, string(body))
if err != nil {
return nil, fmt.Errorf("oidc: invalid userinfo jwt signature %v", err)
}
body = payload
}
var userInfo userInfoRaw
if err := json.Unmarshal(body, &userInfo); err != nil {
return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err)
}
return &UserInfo{
Subject: userInfo.Subject,
Profile: userInfo.Profile,
Email: userInfo.Email,
EmailVerified: bool(userInfo.EmailVerified),
claims: body,
}, nil
}
// IDToken is an OpenID Connect extension that provides a predictable representation
// of an authorization event.
//
// The ID Token only holds fields OpenID Connect requires. To access additional
// claims returned by the server, use the Claims method.
type IDToken struct {
// The URL of the server which issued this token. OpenID Connect
// requires this value always be identical to the URL used for
// initial discovery.
//
// Note: Because of a known issue with Google Accounts' implementation
// this value may differ when using Google.
//
// See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo
Issuer string
// The client ID, or set of client IDs, that this token is issued for. For
// common uses, this is the client that initialized the auth flow.
//
// This package ensures the audience contains an expected value.
Audience []string
// A unique string which identifies the end user.
Subject string
// Expiry of the token. Ths package will not process tokens that have
// expired unless that validation is explicitly turned off.
Expiry time.Time
// When the token was issued by the provider.
IssuedAt time.Time
// Initial nonce provided during the authentication redirect.
//
// This package does NOT provided verification on the value of this field
// and it's the user's responsibility to ensure it contains a valid value.
Nonce string
// at_hash claim, if set in the ID token. Callers can verify an access token
// that corresponds to the ID token using the VerifyAccessToken method.
AccessTokenHash string
// signature algorithm used for ID token, needed to compute a verification hash of an
// access token
sigAlgorithm string
// Raw payload of the id_token.
claims []byte
// Map of distributed claim names to claim sources
distributedClaims map[string]claimSource
}
// Claims unmarshals the raw JSON payload of the ID Token into a provided struct.
//
// idToken, err := idTokenVerifier.Verify(rawIDToken)
// if err != nil {
// // handle error
// }
// var claims struct {
// Email string `json:"email"`
// EmailVerified bool `json:"email_verified"`
// }
// if err := idToken.Claims(&claims); err != nil {
// // handle error
// }
//
func (i *IDToken) Claims(v interface{}) error {
if i.claims == nil {
return errors.New("oidc: claims not set")
}
return json.Unmarshal(i.claims, v)
}
// VerifyAccessToken verifies that the hash of the access token that corresponds to the iD token
// matches the hash in the id token. It returns an error if the hashes don't match.
// It is the caller's responsibility to ensure that the optional access token hash is present for the ID token
// before calling this method. See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
func (i *IDToken) VerifyAccessToken(accessToken string) error {
if i.AccessTokenHash == "" {
return errNoAtHash
}
var h hash.Hash
switch i.sigAlgorithm {
case RS256, ES256, PS256:
h = sha256.New()
case RS384, ES384, PS384:
h = sha512.New384()
case RS512, ES512, PS512:
h = sha512.New()
default:
return fmt.Errorf("oidc: unsupported signing algorithm %q", i.sigAlgorithm)
}
h.Write([]byte(accessToken)) // hash documents that Write will never return an error
sum := h.Sum(nil)[:h.Size()/2]
actual := base64.RawURLEncoding.EncodeToString(sum)
if actual != i.AccessTokenHash {
return errInvalidAtHash
}
return nil
}
type idToken struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience audience `json:"aud"`
Expiry jsonTime `json:"exp"`
IssuedAt jsonTime `json:"iat"`
NotBefore *jsonTime `json:"nbf"`
Nonce string `json:"nonce"`
AtHash string `json:"at_hash"`
ClaimNames map[string]string `json:"_claim_names"`
ClaimSources map[string]claimSource `json:"_claim_sources"`
}
type claimSource struct {
Endpoint string `json:"endpoint"`
AccessToken string `json:"access_token"`
}
type stringAsBool bool
func (sb *stringAsBool) UnmarshalJSON(b []byte) error {
switch string(b) {
case "true", `"true"`:
*sb = true
case "false", `"false"`:
*sb = false
default:
return errors.New("invalid value for boolean")
}
return nil
}
type audience []string
func (a *audience) UnmarshalJSON(b []byte) error {
var s string
if json.Unmarshal(b, &s) == nil {
*a = audience{s}
return nil
}
var auds []string
if err := json.Unmarshal(b, &auds); err != nil {
return err
}
*a = auds
return nil
}
type jsonTime time.Time
func (j *jsonTime) UnmarshalJSON(b []byte) error {
var n json.Number
if err := json.Unmarshal(b, &n); err != nil {
return err
}
var unix int64
if t, err := n.Int64(); err == nil {
unix = t
} else {
f, err := n.Float64()
if err != nil {
return err
}
unix = int64(f)
}
*j = jsonTime(time.Unix(unix, 0))
return nil
}
func unmarshalResp(r *http.Response, body []byte, v interface{}) error {
err := json.Unmarshal(body, &v)
if err == nil {
return nil
}
ct := r.Header.Get("Content-Type")
mediaType, _, parseErr := mime.ParseMediaType(ct)
if parseErr == nil && mediaType == "application/json" {
return fmt.Errorf("got Content-Type = application/json, but could not unmarshal as JSON: %v", err)
}
return fmt.Errorf("expected Content-Type = application/json, got %q: %v", ct, err)
}

View File

@@ -1,344 +0,0 @@
package oidc
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"golang.org/x/oauth2"
jose "gopkg.in/square/go-jose.v2"
)
const (
issuerGoogleAccounts = "https://accounts.google.com"
issuerGoogleAccountsNoScheme = "accounts.google.com"
)
// TokenExpiredError indicates that Verify failed because the token was expired. This
// error does NOT indicate that the token is not also invalid for other reasons. Other
// checks might have failed if the expiration check had not failed.
type TokenExpiredError struct {
// Expiry is the time when the token expired.
Expiry time.Time
}
func (e *TokenExpiredError) Error() string {
return fmt.Sprintf("oidc: token is expired (Token Expiry: %v)", e.Expiry)
}
// KeySet is a set of publc JSON Web Keys that can be used to validate the signature
// of JSON web tokens. This is expected to be backed by a remote key set through
// provider metadata discovery or an in-memory set of keys delivered out-of-band.
type KeySet interface {
// VerifySignature parses the JSON web token, verifies the signature, and returns
// the raw payload. Header and claim fields are validated by other parts of the
// package. For example, the KeySet does not need to check values such as signature
// algorithm, issuer, and audience since the IDTokenVerifier validates these values
// independently.
//
// If VerifySignature makes HTTP requests to verify the token, it's expected to
// use any HTTP client associated with the context through ClientContext.
VerifySignature(ctx context.Context, jwt string) (payload []byte, err error)
}
// IDTokenVerifier provides verification for ID Tokens.
type IDTokenVerifier struct {
keySet KeySet
config *Config
issuer string
}
// NewVerifier returns a verifier manually constructed from a key set and issuer URL.
//
// It's easier to use provider discovery to construct an IDTokenVerifier than creating
// one directly. This method is intended to be used with provider that don't support
// metadata discovery, or avoiding round trips when the key set URL is already known.
//
// This constructor can be used to create a verifier directly using the issuer URL and
// JSON Web Key Set URL without using discovery:
//
// keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs")
// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config)
//
// Or a static key set (e.g. for testing):
//
// keySet := &oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{pub1, pub2}}
// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config)
//
func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier {
return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL}
}
// Config is the configuration for an IDTokenVerifier.
type Config struct {
// Expected audience of the token. For a majority of the cases this is expected to be
// the ID of the client that initialized the login flow. It may occasionally differ if
// the provider supports the authorizing party (azp) claim.
//
// If not provided, users must explicitly set SkipClientIDCheck.
ClientID string
// If specified, only this set of algorithms may be used to sign the JWT.
//
// If the IDTokenVerifier is created from a provider with (*Provider).Verifier, this
// defaults to the set of algorithms the provider supports. Otherwise this values
// defaults to RS256.
SupportedSigningAlgs []string
// If true, no ClientID check performed. Must be true if ClientID field is empty.
SkipClientIDCheck bool
// If true, token expiry is not checked.
SkipExpiryCheck bool
// SkipIssuerCheck is intended for specialized cases where the the caller wishes to
// defer issuer validation. When enabled, callers MUST independently verify the Token's
// Issuer is a known good value.
//
// Mismatched issuers often indicate client mis-configuration. If mismatches are
// unexpected, evaluate if the provided issuer URL is incorrect instead of enabling
// this option.
SkipIssuerCheck bool
// Time function to check Token expiry. Defaults to time.Now
Now func() time.Time
// InsecureSkipSignatureCheck causes this package to skip JWT signature validation.
// It's intended for special cases where providers (such as Azure), use the "none"
// algorithm.
//
// This option can only be enabled safely when the ID Token is received directly
// from the provider after the token exchange.
//
// This option MUST NOT be used when receiving an ID Token from sources other
// than the token endpoint.
InsecureSkipSignatureCheck bool
}
// Verifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs.
func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
if len(config.SupportedSigningAlgs) == 0 && len(p.algorithms) > 0 {
// Make a copy so we don't modify the config values.
cp := &Config{}
*cp = *config
cp.SupportedSigningAlgs = p.algorithms
config = cp
}
return NewVerifier(p.issuer, p.remoteKeySet, config)
}
func parseJWT(p string) ([]byte, error) {
parts := strings.Split(p, ".")
if len(parts) < 2 {
return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
}
return payload, nil
}
func contains(sli []string, ele string) bool {
for _, s := range sli {
if s == ele {
return true
}
}
return false
}
// Returns the Claims from the distributed JWT token
func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src claimSource) ([]byte, error) {
req, err := http.NewRequest("GET", src.Endpoint, nil)
if err != nil {
return nil, fmt.Errorf("malformed request: %v", err)
}
if src.AccessToken != "" {
req.Header.Set("Authorization", "Bearer "+src.AccessToken)
}
resp, err := doRequest(ctx, req)
if err != nil {
return nil, fmt.Errorf("oidc: Request to endpoint failed: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("oidc: request failed: %v", resp.StatusCode)
}
token, err := verifier.Verify(ctx, string(body))
if err != nil {
return nil, fmt.Errorf("malformed response body: %v", err)
}
return token.claims, nil
}
// Verify parses a raw ID Token, verifies it's been signed by the provider, performs
// any additional checks depending on the Config, and returns the payload.
//
// Verify does NOT do nonce validation, which is the callers responsibility.
//
// See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//
// oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
// if err != nil {
// // handle error
// }
//
// // Extract the ID Token from oauth2 token.
// rawIDToken, ok := oauth2Token.Extra("id_token").(string)
// if !ok {
// // handle error
// }
//
// token, err := verifier.Verify(ctx, rawIDToken)
//
func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) {
// Throw out tokens with invalid claims before trying to verify the token. This lets
// us do cheap checks before possibly re-syncing keys.
payload, err := parseJWT(rawIDToken)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
var token idToken
if err := json.Unmarshal(payload, &token); err != nil {
return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
}
distributedClaims := make(map[string]claimSource)
//step through the token to map claim names to claim sources"
for cn, src := range token.ClaimNames {
if src == "" {
return nil, fmt.Errorf("oidc: failed to obtain source from claim name")
}
s, ok := token.ClaimSources[src]
if !ok {
return nil, fmt.Errorf("oidc: source does not exist")
}
distributedClaims[cn] = s
}
t := &IDToken{
Issuer: token.Issuer,
Subject: token.Subject,
Audience: []string(token.Audience),
Expiry: time.Time(token.Expiry),
IssuedAt: time.Time(token.IssuedAt),
Nonce: token.Nonce,
AccessTokenHash: token.AtHash,
claims: payload,
distributedClaims: distributedClaims,
}
// Check issuer.
if !v.config.SkipIssuerCheck && t.Issuer != v.issuer {
// Google sometimes returns "accounts.google.com" as the issuer claim instead of
// the required "https://accounts.google.com". Detect this case and allow it only
// for Google.
//
// We will not add hooks to let other providers go off spec like this.
if !(v.issuer == issuerGoogleAccounts && t.Issuer == issuerGoogleAccountsNoScheme) {
return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, t.Issuer)
}
}
// If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty.
//
// This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party).
if !v.config.SkipClientIDCheck {
if v.config.ClientID != "" {
if !contains(t.Audience, v.config.ClientID) {
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience)
}
} else {
return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
}
}
// If a SkipExpiryCheck is false, make sure token is not expired.
if !v.config.SkipExpiryCheck {
now := time.Now
if v.config.Now != nil {
now = v.config.Now
}
nowTime := now()
if t.Expiry.Before(nowTime) {
return nil, &TokenExpiredError{Expiry: t.Expiry}
}
// If nbf claim is provided in token, ensure that it is indeed in the past.
if token.NotBefore != nil {
nbfTime := time.Time(*token.NotBefore)
// Set to 5 minutes since this is what other OpenID Connect providers do to deal with clock skew.
// https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/6.12.2/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs#L149-L153
leeway := 5 * time.Minute
if nowTime.Add(leeway).Before(nbfTime) {
return nil, fmt.Errorf("oidc: current time %v before the nbf (not before) time: %v", nowTime, nbfTime)
}
}
}
if v.config.InsecureSkipSignatureCheck {
return t, nil
}
jws, err := jose.ParseSigned(rawIDToken)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
switch len(jws.Signatures) {
case 0:
return nil, fmt.Errorf("oidc: id token not signed")
case 1:
default:
return nil, fmt.Errorf("oidc: multiple signatures on id token not supported")
}
sig := jws.Signatures[0]
supportedSigAlgs := v.config.SupportedSigningAlgs
if len(supportedSigAlgs) == 0 {
supportedSigAlgs = []string{RS256}
}
if !contains(supportedSigAlgs, sig.Header.Algorithm) {
return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
}
t.sigAlgorithm = sig.Header.Algorithm
ctx = context.WithValue(ctx, parsedJWTKey, jws)
gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("failed to verify signature: %v", err)
}
// Ensure that the payload returned by the square actually matches the payload parsed earlier.
if !bytes.Equal(gotPayload, payload) {
return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
}
return t, nil
}
// Nonce returns an auth code option which requires the ID Token created by the
// OpenID Connect provider to contain the specified nonce.
func Nonce(nonce string) oauth2.AuthCodeOption {
return oauth2.SetAuthURLParam("nonce", nonce)
}

View File

@@ -1,15 +0,0 @@
ISC License
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -1,145 +0,0 @@
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// Go versions prior to 1.4 are disabled because they use a different layout
// for interfaces which make the implementation of unsafeReflectValue more complex.
// +build !js,!appengine,!safe,!disableunsafe,go1.4
package spew
import (
"reflect"
"unsafe"
)
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = false
// ptrSize is the size of a pointer on the current arch.
ptrSize = unsafe.Sizeof((*byte)(nil))
)
type flag uintptr
var (
// flagRO indicates whether the value field of a reflect.Value
// is read-only.
flagRO flag
// flagAddr indicates whether the address of the reflect.Value's
// value may be taken.
flagAddr flag
)
// flagKindMask holds the bits that make up the kind
// part of the flags field. In all the supported versions,
// it is in the lower 5 bits.
const flagKindMask = flag(0x1f)
// Different versions of Go have used different
// bit layouts for the flags type. This table
// records the known combinations.
var okFlags = []struct {
ro, addr flag
}{{
// From Go 1.4 to 1.5
ro: 1 << 5,
addr: 1 << 7,
}, {
// Up to Go tip.
ro: 1<<5 | 1<<6,
addr: 1 << 8,
}}
var flagValOffset = func() uintptr {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
return field.Offset
}()
// flagField returns a pointer to the flag field of a reflect.Value.
func flagField(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
// the typical safety restrictions preventing access to unaddressable and
// unexported data. It works by digging the raw pointer to the underlying
// value out of the protected value and generating a new unprotected (unsafe)
// reflect.Value to it.
//
// This allows us to check for implementations of the Stringer and error
// interfaces to be used for pretty printing ordinarily unaddressable and
// inaccessible values such as unexported struct fields.
func unsafeReflectValue(v reflect.Value) reflect.Value {
if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
return v
}
flagFieldPtr := flagField(&v)
*flagFieldPtr &^= flagRO
*flagFieldPtr |= flagAddr
return v
}
// Sanity checks against future reflect package changes
// to the type or semantics of the Value.flag field.
func init() {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
panic("reflect.Value flag field has changed kind")
}
type t0 int
var t struct {
A t0
// t0 will have flagEmbedRO set.
t0
// a will have flagStickyRO set
a t0
}
vA := reflect.ValueOf(t).FieldByName("A")
va := reflect.ValueOf(t).FieldByName("a")
vt0 := reflect.ValueOf(t).FieldByName("t0")
// Infer flagRO from the difference between the flags
// for the (otherwise identical) fields in t.
flagPublic := *flagField(&vA)
flagWithRO := *flagField(&va) | *flagField(&vt0)
flagRO = flagPublic ^ flagWithRO
// Infer flagAddr from the difference between a value
// taken from a pointer and not.
vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
flagNoPtr := *flagField(&vA)
flagPtr := *flagField(&vPtrA)
flagAddr = flagNoPtr ^ flagPtr
// Check that the inferred flags tally with one of the known versions.
for _, f := range okFlags {
if flagRO == f.ro && flagAddr == f.addr {
return
}
}
panic("reflect.Value read-only flag has changed semantics")
}

View File

@@ -1,38 +0,0 @@
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is running on Google App Engine, compiled by GopherJS, or
// "-tags safe" is added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// +build js appengine safe disableunsafe !go1.4
package spew
import "reflect"
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = true
)
// unsafeReflectValue typically converts the passed reflect.Value into a one
// that bypasses the typical safety restrictions preventing access to
// unaddressable and unexported data. However, doing this relies on access to
// the unsafe package. This is a stub version which simply returns the passed
// reflect.Value when the unsafe package is not available.
func unsafeReflectValue(v reflect.Value) reflect.Value {
return v
}

View File

@@ -1,341 +0,0 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"io"
"reflect"
"sort"
"strconv"
)
// Some constants in the form of bytes to avoid string overhead. This mirrors
// the technique used in the fmt package.
var (
panicBytes = []byte("(PANIC=")
plusBytes = []byte("+")
iBytes = []byte("i")
trueBytes = []byte("true")
falseBytes = []byte("false")
interfaceBytes = []byte("(interface {})")
commaNewlineBytes = []byte(",\n")
newlineBytes = []byte("\n")
openBraceBytes = []byte("{")
openBraceNewlineBytes = []byte("{\n")
closeBraceBytes = []byte("}")
asteriskBytes = []byte("*")
colonBytes = []byte(":")
colonSpaceBytes = []byte(": ")
openParenBytes = []byte("(")
closeParenBytes = []byte(")")
spaceBytes = []byte(" ")
pointerChainBytes = []byte("->")
nilAngleBytes = []byte("<nil>")
maxNewlineBytes = []byte("<max depth reached>\n")
maxShortBytes = []byte("<max>")
circularBytes = []byte("<already shown>")
circularShortBytes = []byte("<shown>")
invalidAngleBytes = []byte("<invalid>")
openBracketBytes = []byte("[")
closeBracketBytes = []byte("]")
percentBytes = []byte("%")
precisionBytes = []byte(".")
openAngleBytes = []byte("<")
closeAngleBytes = []byte(">")
openMapBytes = []byte("map[")
closeMapBytes = []byte("]")
lenEqualsBytes = []byte("len=")
capEqualsBytes = []byte("cap=")
)
// hexDigits is used to map a decimal value to a hex digit.
var hexDigits = "0123456789abcdef"
// catchPanic handles any panics that might occur during the handleMethods
// calls.
func catchPanic(w io.Writer, v reflect.Value) {
if err := recover(); err != nil {
w.Write(panicBytes)
fmt.Fprintf(w, "%v", err)
w.Write(closeParenBytes)
}
}
// handleMethods attempts to call the Error and String methods on the underlying
// type the passed reflect.Value represents and outputes the result to Writer w.
//
// It handles panics in any called methods by catching and displaying the error
// as the formatted value.
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
// We need an interface to check if the type implements the error or
// Stringer interface. However, the reflect package won't give us an
// interface on certain things like unexported struct fields in order
// to enforce visibility rules. We use unsafe, when it's available,
// to bypass these restrictions since this package does not mutate the
// values.
if !v.CanInterface() {
if UnsafeDisabled {
return false
}
v = unsafeReflectValue(v)
}
// Choose whether or not to do error and Stringer interface lookups against
// the base type or a pointer to the base type depending on settings.
// Technically calling one of these methods with a pointer receiver can
// mutate the value, however, types which choose to satisify an error or
// Stringer interface with a pointer receiver should not be mutating their
// state inside these interface methods.
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
v = unsafeReflectValue(v)
}
if v.CanAddr() {
v = v.Addr()
}
// Is it an error or Stringer?
switch iface := v.Interface().(type) {
case error:
defer catchPanic(w, v)
if cs.ContinueOnMethod {
w.Write(openParenBytes)
w.Write([]byte(iface.Error()))
w.Write(closeParenBytes)
w.Write(spaceBytes)
return false
}
w.Write([]byte(iface.Error()))
return true
case fmt.Stringer:
defer catchPanic(w, v)
if cs.ContinueOnMethod {
w.Write(openParenBytes)
w.Write([]byte(iface.String()))
w.Write(closeParenBytes)
w.Write(spaceBytes)
return false
}
w.Write([]byte(iface.String()))
return true
}
return false
}
// printBool outputs a boolean value as true or false to Writer w.
func printBool(w io.Writer, val bool) {
if val {
w.Write(trueBytes)
} else {
w.Write(falseBytes)
}
}
// printInt outputs a signed integer value to Writer w.
func printInt(w io.Writer, val int64, base int) {
w.Write([]byte(strconv.FormatInt(val, base)))
}
// printUint outputs an unsigned integer value to Writer w.
func printUint(w io.Writer, val uint64, base int) {
w.Write([]byte(strconv.FormatUint(val, base)))
}
// printFloat outputs a floating point value using the specified precision,
// which is expected to be 32 or 64bit, to Writer w.
func printFloat(w io.Writer, val float64, precision int) {
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
}
// printComplex outputs a complex value using the specified float precision
// for the real and imaginary parts to Writer w.
func printComplex(w io.Writer, c complex128, floatPrecision int) {
r := real(c)
w.Write(openParenBytes)
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
i := imag(c)
if i >= 0 {
w.Write(plusBytes)
}
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
w.Write(iBytes)
w.Write(closeParenBytes)
}
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
// prefix to Writer w.
func printHexPtr(w io.Writer, p uintptr) {
// Null pointer.
num := uint64(p)
if num == 0 {
w.Write(nilAngleBytes)
return
}
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
buf := make([]byte, 18)
// It's simpler to construct the hex string right to left.
base := uint64(16)
i := len(buf) - 1
for num >= base {
buf[i] = hexDigits[num%base]
num /= base
i--
}
buf[i] = hexDigits[num]
// Add '0x' prefix.
i--
buf[i] = 'x'
i--
buf[i] = '0'
// Strip unused leading bytes.
buf = buf[i:]
w.Write(buf)
}
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
// elements to be sorted.
type valuesSorter struct {
values []reflect.Value
strings []string // either nil or same len and values
cs *ConfigState
}
// newValuesSorter initializes a valuesSorter instance, which holds a set of
// surrogate keys on which the data should be sorted. It uses flags in
// ConfigState to decide if and how to populate those surrogate keys.
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
vs := &valuesSorter{values: values, cs: cs}
if canSortSimply(vs.values[0].Kind()) {
return vs
}
if !cs.DisableMethods {
vs.strings = make([]string, len(values))
for i := range vs.values {
b := bytes.Buffer{}
if !handleMethods(cs, &b, vs.values[i]) {
vs.strings = nil
break
}
vs.strings[i] = b.String()
}
}
if vs.strings == nil && cs.SpewKeys {
vs.strings = make([]string, len(values))
for i := range vs.values {
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
}
}
return vs
}
// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
// directly, or whether it should be considered for sorting by surrogate keys
// (if the ConfigState allows it).
func canSortSimply(kind reflect.Kind) bool {
// This switch parallels valueSortLess, except for the default case.
switch kind {
case reflect.Bool:
return true
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return true
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return true
case reflect.Float32, reflect.Float64:
return true
case reflect.String:
return true
case reflect.Uintptr:
return true
case reflect.Array:
return true
}
return false
}
// Len returns the number of values in the slice. It is part of the
// sort.Interface implementation.
func (s *valuesSorter) Len() int {
return len(s.values)
}
// Swap swaps the values at the passed indices. It is part of the
// sort.Interface implementation.
func (s *valuesSorter) Swap(i, j int) {
s.values[i], s.values[j] = s.values[j], s.values[i]
if s.strings != nil {
s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
}
}
// valueSortLess returns whether the first value should sort before the second
// value. It is used by valueSorter.Less as part of the sort.Interface
// implementation.
func valueSortLess(a, b reflect.Value) bool {
switch a.Kind() {
case reflect.Bool:
return !a.Bool() && b.Bool()
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return a.Int() < b.Int()
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return a.Uint() < b.Uint()
case reflect.Float32, reflect.Float64:
return a.Float() < b.Float()
case reflect.String:
return a.String() < b.String()
case reflect.Uintptr:
return a.Uint() < b.Uint()
case reflect.Array:
// Compare the contents of both arrays.
l := a.Len()
for i := 0; i < l; i++ {
av := a.Index(i)
bv := b.Index(i)
if av.Interface() == bv.Interface() {
continue
}
return valueSortLess(av, bv)
}
}
return a.String() < b.String()
}
// Less returns whether the value at index i should sort before the
// value at index j. It is part of the sort.Interface implementation.
func (s *valuesSorter) Less(i, j int) bool {
if s.strings == nil {
return valueSortLess(s.values[i], s.values[j])
}
return s.strings[i] < s.strings[j]
}
// sortValues is a sort function that handles both native types and any type that
// can be converted to error or Stringer. Other inputs are sorted according to
// their Value.String() value to ensure display stability.
func sortValues(values []reflect.Value, cs *ConfigState) {
if len(values) == 0 {
return
}
sort.Sort(newValuesSorter(values, cs))
}

View File

@@ -1,306 +0,0 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"io"
"os"
)
// ConfigState houses the configuration options used by spew to format and
// display values. There is a global instance, Config, that is used to control
// all top-level Formatter and Dump functionality. Each ConfigState instance
// provides methods equivalent to the top-level functions.
//
// The zero value for ConfigState provides no indentation. You would typically
// want to set it to a space or a tab.
//
// Alternatively, you can use NewDefaultConfig to get a ConfigState instance
// with default settings. See the documentation of NewDefaultConfig for default
// values.
type ConfigState struct {
// Indent specifies the string to use for each indentation level. The
// global config instance that all top-level functions use set this to a
// single space by default. If you would like more indentation, you might
// set this to a tab with "\t" or perhaps two spaces with " ".
Indent string
// MaxDepth controls the maximum number of levels to descend into nested
// data structures. The default, 0, means there is no limit.
//
// NOTE: Circular data structures are properly detected, so it is not
// necessary to set this value unless you specifically want to limit deeply
// nested data structures.
MaxDepth int
// DisableMethods specifies whether or not error and Stringer interfaces are
// invoked for types that implement them.
DisableMethods bool
// DisablePointerMethods specifies whether or not to check for and invoke
// error and Stringer interfaces on types which only accept a pointer
// receiver when the current type is not a pointer.
//
// NOTE: This might be an unsafe action since calling one of these methods
// with a pointer receiver could technically mutate the value, however,
// in practice, types which choose to satisify an error or Stringer
// interface with a pointer receiver should not be mutating their state
// inside these interface methods. As a result, this option relies on
// access to the unsafe package, so it will not have any effect when
// running in environments without access to the unsafe package such as
// Google App Engine or with the "safe" build tag specified.
DisablePointerMethods bool
// DisablePointerAddresses specifies whether to disable the printing of
// pointer addresses. This is useful when diffing data structures in tests.
DisablePointerAddresses bool
// DisableCapacities specifies whether to disable the printing of capacities
// for arrays, slices, maps and channels. This is useful when diffing
// data structures in tests.
DisableCapacities bool
// ContinueOnMethod specifies whether or not recursion should continue once
// a custom error or Stringer interface is invoked. The default, false,
// means it will print the results of invoking the custom error or Stringer
// interface and return immediately instead of continuing to recurse into
// the internals of the data type.
//
// NOTE: This flag does not have any effect if method invocation is disabled
// via the DisableMethods or DisablePointerMethods options.
ContinueOnMethod bool
// SortKeys specifies map keys should be sorted before being printed. Use
// this to have a more deterministic, diffable output. Note that only
// native types (bool, int, uint, floats, uintptr and string) and types
// that support the error or Stringer interfaces (if methods are
// enabled) are supported, with other types sorted according to the
// reflect.Value.String() output which guarantees display stability.
SortKeys bool
// SpewKeys specifies that, as a last resort attempt, map keys should
// be spewed to strings and sorted by those strings. This is only
// considered if SortKeys is true.
SpewKeys bool
}
// Config is the active configuration of the top-level functions.
// The configuration can be changed by modifying the contents of spew.Config.
var Config = ConfigState{Indent: " "}
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the formatted string as a value that satisfies error. See NewFormatter
// for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
return fmt.Errorf(format, c.convertArgs(a)...)
}
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, c.convertArgs(a)...)
}
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(w, format, c.convertArgs(a)...)
}
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
// passed with a Formatter interface returned by c.NewFormatter. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, c.convertArgs(a)...)
}
// Print is a wrapper for fmt.Print that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
return fmt.Print(c.convertArgs(a)...)
}
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Printf(format, c.convertArgs(a)...)
}
// Println is a wrapper for fmt.Println that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
return fmt.Println(c.convertArgs(a)...)
}
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprint(a ...interface{}) string {
return fmt.Sprint(c.convertArgs(a)...)
}
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
return fmt.Sprintf(format, c.convertArgs(a)...)
}
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
// were passed with a Formatter interface returned by c.NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprintln(a ...interface{}) string {
return fmt.Sprintln(c.convertArgs(a)...)
}
/*
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
interface. As a result, it integrates cleanly with standard fmt package
printing functions. The formatter is useful for inline printing of smaller data
types similar to the standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Typically this function shouldn't be called directly. It is much easier to make
use of the custom formatter by calling one of the convenience functions such as
c.Printf, c.Println, or c.Printf.
*/
func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
return newFormatter(c, v)
}
// Fdump formats and displays the passed arguments to io.Writer w. It formats
// exactly the same as Dump.
func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
fdump(c, w, a...)
}
/*
Dump displays the passed parameters to standard out with newlines, customizable
indentation, and additional debug information such as complete types and all
pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by modifying the public members
of c. See ConfigState for options documentation.
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
get the formatted result as a string.
*/
func (c *ConfigState) Dump(a ...interface{}) {
fdump(c, os.Stdout, a...)
}
// Sdump returns a string with the passed arguments formatted exactly the same
// as Dump.
func (c *ConfigState) Sdump(a ...interface{}) string {
var buf bytes.Buffer
fdump(c, &buf, a...)
return buf.String()
}
// convertArgs accepts a slice of arguments and returns a slice of the same
// length with each argument converted to a spew Formatter interface using
// the ConfigState associated with s.
func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
formatters = make([]interface{}, len(args))
for index, arg := range args {
formatters[index] = newFormatter(c, arg)
}
return formatters
}
// NewDefaultConfig returns a ConfigState with the following default settings.
//
// Indent: " "
// MaxDepth: 0
// DisableMethods: false
// DisablePointerMethods: false
// ContinueOnMethod: false
// SortKeys: false
func NewDefaultConfig() *ConfigState {
return &ConfigState{Indent: " "}
}

View File

@@ -1,211 +0,0 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
Package spew implements a deep pretty printer for Go data structures to aid in
debugging.
A quick overview of the additional features spew provides over the built-in
printing facilities for Go data types are as follows:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output (only when using
Dump style)
There are two different approaches spew allows for dumping Go data structures:
* Dump style which prints with newlines, customizable indentation,
and additional debug information such as types and all pointer addresses
used to indirect to the final value
* A custom Formatter interface that integrates cleanly with the standard fmt
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
similar to the default %v while providing the additional functionality
outlined above and passing unsupported format verbs such as %x and %q
along to fmt
Quick Start
This section demonstrates how to quickly get started with spew. See the
sections below for further details on formatting and configuration options.
To dump a variable with full newlines, indentation, type, and pointer
information use Dump, Fdump, or Sdump:
spew.Dump(myVar1, myVar2, ...)
spew.Fdump(someWriter, myVar1, myVar2, ...)
str := spew.Sdump(myVar1, myVar2, ...)
Alternatively, if you would prefer to use format strings with a compacted inline
printing style, use the convenience wrappers Printf, Fprintf, etc with
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
%#+v (adds types and pointer addresses):
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
Configuration Options
Configuration of spew is handled by fields in the ConfigState type. For
convenience, all of the top-level functions use a global state available
via the spew.Config global.
It is also possible to create a ConfigState instance that provides methods
equivalent to the top-level functions. This allows concurrent configuration
options. See the ConfigState documentation for more details.
The following configuration options are available:
* Indent
String to use for each indentation level for Dump functions.
It is a single space by default. A popular alternative is "\t".
* MaxDepth
Maximum number of levels to descend into nested data structures.
There is no limit by default.
* DisableMethods
Disables invocation of error and Stringer interface methods.
Method invocation is enabled by default.
* DisablePointerMethods
Disables invocation of error and Stringer interface methods on types
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
* ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
* SortKeys
Specifies map keys should be sorted before being printed. Use
this to have a more deterministic, diffable output. Note that
only native types (bool, int, uint, floats, uintptr and string)
and types which implement error or Stringer interfaces are
supported with other types sorted according to the
reflect.Value.String() output which guarantees display
stability. Natural map order is used by default.
* SpewKeys
Specifies that, as a last resort attempt, map keys should be
spewed to strings and sorted by those strings. This is only
considered if SortKeys is true.
Dump Usage
Simply call spew.Dump with a list of variables you want to dump:
spew.Dump(myVar1, myVar2, ...)
You may also call spew.Fdump if you would prefer to output to an arbitrary
io.Writer. For example, to dump to standard error:
spew.Fdump(os.Stderr, myVar1, myVar2, ...)
A third option is to call spew.Sdump to get the formatted output as a string:
str := spew.Sdump(myVar1, myVar2, ...)
Sample Dump Output
See the Dump example for details on the setup of the types and variables being
shown here.
(main.Foo) {
unexportedField: (*main.Bar)(0xf84002e210)({
flag: (main.Flag) flagTwo,
data: (uintptr) <nil>
}),
ExportedField: (map[interface {}]interface {}) (len=1) {
(string) (len=3) "one": (bool) true
}
}
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
command as shown.
([]uint8) (len=32 cap=32) {
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
00000020 31 32 |12|
}
Custom Formatter
Spew provides a custom formatter that implements the fmt.Formatter interface
so that it integrates cleanly with standard fmt package printing functions. The
formatter is useful for inline printing of smaller data types similar to the
standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Custom Formatter Usage
The simplest way to make use of the spew custom formatter is to call one of the
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
functions have syntax you are most likely already familiar with:
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Println(myVar, myVar2)
spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
See the Index for the full list convenience functions.
Sample Formatter Output
Double pointer to a uint8:
%v: <**>5
%+v: <**>(0xf8400420d0->0xf8400420c8)5
%#v: (**uint8)5
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
Pointer to circular struct with a uint8 field and a pointer to itself:
%v: <*>{1 <*><shown>}
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
See the Printf example for details on the setup of variables being shown
here.
Errors
Since it is possible for custom Stringer/error interfaces to panic, spew
detects them and handles them internally by printing the panic information
inline with the output. Since spew is intended to provide deep pretty printing
capabilities on structures, it intentionally does not return any errors.
*/
package spew

View File

@@ -1,509 +0,0 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"os"
"reflect"
"regexp"
"strconv"
"strings"
)
var (
// uint8Type is a reflect.Type representing a uint8. It is used to
// convert cgo types to uint8 slices for hexdumping.
uint8Type = reflect.TypeOf(uint8(0))
// cCharRE is a regular expression that matches a cgo char.
// It is used to detect character arrays to hexdump them.
cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
// char. It is used to detect unsigned character arrays to hexdump
// them.
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
// It is used to detect uint8_t arrays to hexdump them.
cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
)
// dumpState contains information about the state of a dump operation.
type dumpState struct {
w io.Writer
depth int
pointers map[uintptr]int
ignoreNextType bool
ignoreNextIndent bool
cs *ConfigState
}
// indent performs indentation according to the depth level and cs.Indent
// option.
func (d *dumpState) indent() {
if d.ignoreNextIndent {
d.ignoreNextIndent = false
return
}
d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
}
// unpackValue returns values inside of non-nil interfaces when possible.
// This is useful for data types like structs, arrays, slices, and maps which
// can contain varying types packed inside an interface.
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Interface && !v.IsNil() {
v = v.Elem()
}
return v
}
// dumpPtr handles formatting of pointers by indirecting them as necessary.
func (d *dumpState) dumpPtr(v reflect.Value) {
// Remove pointers at or below the current depth from map used to detect
// circular refs.
for k, depth := range d.pointers {
if depth >= d.depth {
delete(d.pointers, k)
}
}
// Keep list of all dereferenced pointers to show later.
pointerChain := make([]uintptr, 0)
// Figure out how many levels of indirection there are by dereferencing
// pointers and unpacking interfaces down the chain while detecting circular
// references.
nilFound := false
cycleFound := false
indirects := 0
ve := v
for ve.Kind() == reflect.Ptr {
if ve.IsNil() {
nilFound = true
break
}
indirects++
addr := ve.Pointer()
pointerChain = append(pointerChain, addr)
if pd, ok := d.pointers[addr]; ok && pd < d.depth {
cycleFound = true
indirects--
break
}
d.pointers[addr] = d.depth
ve = ve.Elem()
if ve.Kind() == reflect.Interface {
if ve.IsNil() {
nilFound = true
break
}
ve = ve.Elem()
}
}
// Display type information.
d.w.Write(openParenBytes)
d.w.Write(bytes.Repeat(asteriskBytes, indirects))
d.w.Write([]byte(ve.Type().String()))
d.w.Write(closeParenBytes)
// Display pointer information.
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
d.w.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
d.w.Write(pointerChainBytes)
}
printHexPtr(d.w, addr)
}
d.w.Write(closeParenBytes)
}
// Display dereferenced value.
d.w.Write(openParenBytes)
switch {
case nilFound:
d.w.Write(nilAngleBytes)
case cycleFound:
d.w.Write(circularBytes)
default:
d.ignoreNextType = true
d.dump(ve)
}
d.w.Write(closeParenBytes)
}
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
// reflection) arrays and slices are dumped in hexdump -C fashion.
func (d *dumpState) dumpSlice(v reflect.Value) {
// Determine whether this type should be hex dumped or not. Also,
// for types which should be hexdumped, try to use the underlying data
// first, then fall back to trying to convert them to a uint8 slice.
var buf []uint8
doConvert := false
doHexDump := false
numEntries := v.Len()
if numEntries > 0 {
vt := v.Index(0).Type()
vts := vt.String()
switch {
// C types that need to be converted.
case cCharRE.MatchString(vts):
fallthrough
case cUnsignedCharRE.MatchString(vts):
fallthrough
case cUint8tCharRE.MatchString(vts):
doConvert = true
// Try to use existing uint8 slices and fall back to converting
// and copying if that fails.
case vt.Kind() == reflect.Uint8:
// We need an addressable interface to convert the type
// to a byte slice. However, the reflect package won't
// give us an interface on certain things like
// unexported struct fields in order to enforce
// visibility rules. We use unsafe, when available, to
// bypass these restrictions since this package does not
// mutate the values.
vs := v
if !vs.CanInterface() || !vs.CanAddr() {
vs = unsafeReflectValue(vs)
}
if !UnsafeDisabled {
vs = vs.Slice(0, numEntries)
// Use the existing uint8 slice if it can be
// type asserted.
iface := vs.Interface()
if slice, ok := iface.([]uint8); ok {
buf = slice
doHexDump = true
break
}
}
// The underlying data needs to be converted if it can't
// be type asserted to a uint8 slice.
doConvert = true
}
// Copy and convert the underlying type if needed.
if doConvert && vt.ConvertibleTo(uint8Type) {
// Convert and copy each element into a uint8 byte
// slice.
buf = make([]uint8, numEntries)
for i := 0; i < numEntries; i++ {
vv := v.Index(i)
buf[i] = uint8(vv.Convert(uint8Type).Uint())
}
doHexDump = true
}
}
// Hexdump the entire slice as needed.
if doHexDump {
indent := strings.Repeat(d.cs.Indent, d.depth)
str := indent + hex.Dump(buf)
str = strings.Replace(str, "\n", "\n"+indent, -1)
str = strings.TrimRight(str, d.cs.Indent)
d.w.Write([]byte(str))
return
}
// Recursively call dump for each item.
for i := 0; i < numEntries; i++ {
d.dump(d.unpackValue(v.Index(i)))
if i < (numEntries - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
// dump is the main workhorse for dumping a value. It uses the passed reflect
// value to figure out what kind of object we are dealing with and formats it
// appropriately. It is a recursive function, however circular data structures
// are detected and handled properly.
func (d *dumpState) dump(v reflect.Value) {
// Handle invalid reflect values immediately.
kind := v.Kind()
if kind == reflect.Invalid {
d.w.Write(invalidAngleBytes)
return
}
// Handle pointers specially.
if kind == reflect.Ptr {
d.indent()
d.dumpPtr(v)
return
}
// Print type information unless already handled elsewhere.
if !d.ignoreNextType {
d.indent()
d.w.Write(openParenBytes)
d.w.Write([]byte(v.Type().String()))
d.w.Write(closeParenBytes)
d.w.Write(spaceBytes)
}
d.ignoreNextType = false
// Display length and capacity if the built-in len and cap functions
// work with the value's kind and the len/cap itself is non-zero.
valueLen, valueCap := 0, 0
switch v.Kind() {
case reflect.Array, reflect.Slice, reflect.Chan:
valueLen, valueCap = v.Len(), v.Cap()
case reflect.Map, reflect.String:
valueLen = v.Len()
}
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
d.w.Write(openParenBytes)
if valueLen != 0 {
d.w.Write(lenEqualsBytes)
printInt(d.w, int64(valueLen), 10)
}
if !d.cs.DisableCapacities && valueCap != 0 {
if valueLen != 0 {
d.w.Write(spaceBytes)
}
d.w.Write(capEqualsBytes)
printInt(d.w, int64(valueCap), 10)
}
d.w.Write(closeParenBytes)
d.w.Write(spaceBytes)
}
// Call Stringer/error interfaces if they exist and the handle methods flag
// is enabled
if !d.cs.DisableMethods {
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
if handled := handleMethods(d.cs, d.w, v); handled {
return
}
}
}
switch kind {
case reflect.Invalid:
// Do nothing. We should never get here since invalid has already
// been handled above.
case reflect.Bool:
printBool(d.w, v.Bool())
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
printInt(d.w, v.Int(), 10)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
printUint(d.w, v.Uint(), 10)
case reflect.Float32:
printFloat(d.w, v.Float(), 32)
case reflect.Float64:
printFloat(d.w, v.Float(), 64)
case reflect.Complex64:
printComplex(d.w, v.Complex(), 32)
case reflect.Complex128:
printComplex(d.w, v.Complex(), 64)
case reflect.Slice:
if v.IsNil() {
d.w.Write(nilAngleBytes)
break
}
fallthrough
case reflect.Array:
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
d.dumpSlice(v)
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.String:
d.w.Write([]byte(strconv.Quote(v.String())))
case reflect.Interface:
// The only time we should get here is for nil interfaces due to
// unpackValue calls.
if v.IsNil() {
d.w.Write(nilAngleBytes)
}
case reflect.Ptr:
// Do nothing. We should never get here since pointers have already
// been handled above.
case reflect.Map:
// nil maps should be indicated as different than empty maps
if v.IsNil() {
d.w.Write(nilAngleBytes)
break
}
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
numEntries := v.Len()
keys := v.MapKeys()
if d.cs.SortKeys {
sortValues(keys, d.cs)
}
for i, key := range keys {
d.dump(d.unpackValue(key))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(v.MapIndex(key)))
if i < (numEntries - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.Struct:
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
vt := v.Type()
numFields := v.NumField()
for i := 0; i < numFields; i++ {
d.indent()
vtf := vt.Field(i)
d.w.Write([]byte(vtf.Name))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(v.Field(i)))
if i < (numFields - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.Uintptr:
printHexPtr(d.w, uintptr(v.Uint()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
printHexPtr(d.w, v.Pointer())
// There were not any other types at the time this code was written, but
// fall back to letting the default fmt package handle it in case any new
// types are added.
default:
if v.CanInterface() {
fmt.Fprintf(d.w, "%v", v.Interface())
} else {
fmt.Fprintf(d.w, "%v", v.String())
}
}
}
// fdump is a helper function to consolidate the logic from the various public
// methods which take varying writers and config states.
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
for _, arg := range a {
if arg == nil {
w.Write(interfaceBytes)
w.Write(spaceBytes)
w.Write(nilAngleBytes)
w.Write(newlineBytes)
continue
}
d := dumpState{w: w, cs: cs}
d.pointers = make(map[uintptr]int)
d.dump(reflect.ValueOf(arg))
d.w.Write(newlineBytes)
}
}
// Fdump formats and displays the passed arguments to io.Writer w. It formats
// exactly the same as Dump.
func Fdump(w io.Writer, a ...interface{}) {
fdump(&Config, w, a...)
}
// Sdump returns a string with the passed arguments formatted exactly the same
// as Dump.
func Sdump(a ...interface{}) string {
var buf bytes.Buffer
fdump(&Config, &buf, a...)
return buf.String()
}
/*
Dump displays the passed parameters to standard out with newlines, customizable
indentation, and additional debug information such as complete types and all
pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by an exported package global,
spew.Config. See ConfigState for options documentation.
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
get the formatted result as a string.
*/
func Dump(a ...interface{}) {
fdump(&Config, os.Stdout, a...)
}

View File

@@ -1,419 +0,0 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
)
// supportedFlags is a list of all the character flags supported by fmt package.
const supportedFlags = "0-+# "
// formatState implements the fmt.Formatter interface and contains information
// about the state of a formatting operation. The NewFormatter function can
// be used to get a new Formatter which can be used directly as arguments
// in standard fmt package printing calls.
type formatState struct {
value interface{}
fs fmt.State
depth int
pointers map[uintptr]int
ignoreNextType bool
cs *ConfigState
}
// buildDefaultFormat recreates the original format string without precision
// and width information to pass in to fmt.Sprintf in the case of an
// unrecognized type. Unless new types are added to the language, this
// function won't ever be called.
func (f *formatState) buildDefaultFormat() (format string) {
buf := bytes.NewBuffer(percentBytes)
for _, flag := range supportedFlags {
if f.fs.Flag(int(flag)) {
buf.WriteRune(flag)
}
}
buf.WriteRune('v')
format = buf.String()
return format
}
// constructOrigFormat recreates the original format string including precision
// and width information to pass along to the standard fmt package. This allows
// automatic deferral of all format strings this package doesn't support.
func (f *formatState) constructOrigFormat(verb rune) (format string) {
buf := bytes.NewBuffer(percentBytes)
for _, flag := range supportedFlags {
if f.fs.Flag(int(flag)) {
buf.WriteRune(flag)
}
}
if width, ok := f.fs.Width(); ok {
buf.WriteString(strconv.Itoa(width))
}
if precision, ok := f.fs.Precision(); ok {
buf.Write(precisionBytes)
buf.WriteString(strconv.Itoa(precision))
}
buf.WriteRune(verb)
format = buf.String()
return format
}
// unpackValue returns values inside of non-nil interfaces when possible and
// ensures that types for values which have been unpacked from an interface
// are displayed when the show types flag is also set.
// This is useful for data types like structs, arrays, slices, and maps which
// can contain varying types packed inside an interface.
func (f *formatState) unpackValue(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Interface {
f.ignoreNextType = false
if !v.IsNil() {
v = v.Elem()
}
}
return v
}
// formatPtr handles formatting of pointers by indirecting them as necessary.
func (f *formatState) formatPtr(v reflect.Value) {
// Display nil if top level pointer is nil.
showTypes := f.fs.Flag('#')
if v.IsNil() && (!showTypes || f.ignoreNextType) {
f.fs.Write(nilAngleBytes)
return
}
// Remove pointers at or below the current depth from map used to detect
// circular refs.
for k, depth := range f.pointers {
if depth >= f.depth {
delete(f.pointers, k)
}
}
// Keep list of all dereferenced pointers to possibly show later.
pointerChain := make([]uintptr, 0)
// Figure out how many levels of indirection there are by derferencing
// pointers and unpacking interfaces down the chain while detecting circular
// references.
nilFound := false
cycleFound := false
indirects := 0
ve := v
for ve.Kind() == reflect.Ptr {
if ve.IsNil() {
nilFound = true
break
}
indirects++
addr := ve.Pointer()
pointerChain = append(pointerChain, addr)
if pd, ok := f.pointers[addr]; ok && pd < f.depth {
cycleFound = true
indirects--
break
}
f.pointers[addr] = f.depth
ve = ve.Elem()
if ve.Kind() == reflect.Interface {
if ve.IsNil() {
nilFound = true
break
}
ve = ve.Elem()
}
}
// Display type or indirection level depending on flags.
if showTypes && !f.ignoreNextType {
f.fs.Write(openParenBytes)
f.fs.Write(bytes.Repeat(asteriskBytes, indirects))
f.fs.Write([]byte(ve.Type().String()))
f.fs.Write(closeParenBytes)
} else {
if nilFound || cycleFound {
indirects += strings.Count(ve.Type().String(), "*")
}
f.fs.Write(openAngleBytes)
f.fs.Write([]byte(strings.Repeat("*", indirects)))
f.fs.Write(closeAngleBytes)
}
// Display pointer information depending on flags.
if f.fs.Flag('+') && (len(pointerChain) > 0) {
f.fs.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
f.fs.Write(pointerChainBytes)
}
printHexPtr(f.fs, addr)
}
f.fs.Write(closeParenBytes)
}
// Display dereferenced value.
switch {
case nilFound:
f.fs.Write(nilAngleBytes)
case cycleFound:
f.fs.Write(circularShortBytes)
default:
f.ignoreNextType = true
f.format(ve)
}
}
// format is the main workhorse for providing the Formatter interface. It
// uses the passed reflect value to figure out what kind of object we are
// dealing with and formats it appropriately. It is a recursive function,
// however circular data structures are detected and handled properly.
func (f *formatState) format(v reflect.Value) {
// Handle invalid reflect values immediately.
kind := v.Kind()
if kind == reflect.Invalid {
f.fs.Write(invalidAngleBytes)
return
}
// Handle pointers specially.
if kind == reflect.Ptr {
f.formatPtr(v)
return
}
// Print type information unless already handled elsewhere.
if !f.ignoreNextType && f.fs.Flag('#') {
f.fs.Write(openParenBytes)
f.fs.Write([]byte(v.Type().String()))
f.fs.Write(closeParenBytes)
}
f.ignoreNextType = false
// Call Stringer/error interfaces if they exist and the handle methods
// flag is enabled.
if !f.cs.DisableMethods {
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
if handled := handleMethods(f.cs, f.fs, v); handled {
return
}
}
}
switch kind {
case reflect.Invalid:
// Do nothing. We should never get here since invalid has already
// been handled above.
case reflect.Bool:
printBool(f.fs, v.Bool())
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
printInt(f.fs, v.Int(), 10)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
printUint(f.fs, v.Uint(), 10)
case reflect.Float32:
printFloat(f.fs, v.Float(), 32)
case reflect.Float64:
printFloat(f.fs, v.Float(), 64)
case reflect.Complex64:
printComplex(f.fs, v.Complex(), 32)
case reflect.Complex128:
printComplex(f.fs, v.Complex(), 64)
case reflect.Slice:
if v.IsNil() {
f.fs.Write(nilAngleBytes)
break
}
fallthrough
case reflect.Array:
f.fs.Write(openBracketBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
numEntries := v.Len()
for i := 0; i < numEntries; i++ {
if i > 0 {
f.fs.Write(spaceBytes)
}
f.ignoreNextType = true
f.format(f.unpackValue(v.Index(i)))
}
}
f.depth--
f.fs.Write(closeBracketBytes)
case reflect.String:
f.fs.Write([]byte(v.String()))
case reflect.Interface:
// The only time we should get here is for nil interfaces due to
// unpackValue calls.
if v.IsNil() {
f.fs.Write(nilAngleBytes)
}
case reflect.Ptr:
// Do nothing. We should never get here since pointers have already
// been handled above.
case reflect.Map:
// nil maps should be indicated as different than empty maps
if v.IsNil() {
f.fs.Write(nilAngleBytes)
break
}
f.fs.Write(openMapBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
keys := v.MapKeys()
if f.cs.SortKeys {
sortValues(keys, f.cs)
}
for i, key := range keys {
if i > 0 {
f.fs.Write(spaceBytes)
}
f.ignoreNextType = true
f.format(f.unpackValue(key))
f.fs.Write(colonBytes)
f.ignoreNextType = true
f.format(f.unpackValue(v.MapIndex(key)))
}
}
f.depth--
f.fs.Write(closeMapBytes)
case reflect.Struct:
numFields := v.NumField()
f.fs.Write(openBraceBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
vt := v.Type()
for i := 0; i < numFields; i++ {
if i > 0 {
f.fs.Write(spaceBytes)
}
vtf := vt.Field(i)
if f.fs.Flag('+') || f.fs.Flag('#') {
f.fs.Write([]byte(vtf.Name))
f.fs.Write(colonBytes)
}
f.format(f.unpackValue(v.Field(i)))
}
}
f.depth--
f.fs.Write(closeBraceBytes)
case reflect.Uintptr:
printHexPtr(f.fs, uintptr(v.Uint()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
printHexPtr(f.fs, v.Pointer())
// There were not any other types at the time this code was written, but
// fall back to letting the default fmt package handle it if any get added.
default:
format := f.buildDefaultFormat()
if v.CanInterface() {
fmt.Fprintf(f.fs, format, v.Interface())
} else {
fmt.Fprintf(f.fs, format, v.String())
}
}
}
// Format satisfies the fmt.Formatter interface. See NewFormatter for usage
// details.
func (f *formatState) Format(fs fmt.State, verb rune) {
f.fs = fs
// Use standard formatting for verbs that are not v.
if verb != 'v' {
format := f.constructOrigFormat(verb)
fmt.Fprintf(fs, format, f.value)
return
}
if f.value == nil {
if fs.Flag('#') {
fs.Write(interfaceBytes)
}
fs.Write(nilAngleBytes)
return
}
f.format(reflect.ValueOf(f.value))
}
// newFormatter is a helper function to consolidate the logic from the various
// public methods which take varying config states.
func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter {
fs := &formatState{value: v, cs: cs}
fs.pointers = make(map[uintptr]int)
return fs
}
/*
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
interface. As a result, it integrates cleanly with standard fmt package
printing functions. The formatter is useful for inline printing of smaller data
types similar to the standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Typically this function shouldn't be called directly. It is much easier to make
use of the custom formatter by calling one of the convenience functions such as
Printf, Println, or Fprintf.
*/
func NewFormatter(v interface{}) fmt.Formatter {
return newFormatter(&Config, v)
}

View File

@@ -1,148 +0,0 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"fmt"
"io"
)
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the formatted string as a value that satisfies error. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b))
func Errorf(format string, a ...interface{}) (err error) {
return fmt.Errorf(format, convertArgs(a)...)
}
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, convertArgs(a)...)
}
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(w, format, convertArgs(a)...)
}
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
// passed with a default Formatter interface returned by NewFormatter. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, convertArgs(a)...)
}
// Print is a wrapper for fmt.Print that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b))
func Print(a ...interface{}) (n int, err error) {
return fmt.Print(convertArgs(a)...)
}
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b))
func Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Printf(format, convertArgs(a)...)
}
// Println is a wrapper for fmt.Println that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b))
func Println(a ...interface{}) (n int, err error) {
return fmt.Println(convertArgs(a)...)
}
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b))
func Sprint(a ...interface{}) string {
return fmt.Sprint(convertArgs(a)...)
}
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b))
func Sprintf(format string, a ...interface{}) string {
return fmt.Sprintf(format, convertArgs(a)...)
}
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
// were passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b))
func Sprintln(a ...interface{}) string {
return fmt.Sprintln(convertArgs(a)...)
}
// convertArgs accepts a slice of arguments and returns a slice of the same
// length with each argument converted to a default spew Formatter interface.
func convertArgs(args []interface{}) (formatters []interface{}) {
formatters = make([]interface{}, len(args))
for index, arg := range args {
formatters[index] = NewFormatter(arg)
}
return formatters
}

View File

@@ -1,16 +0,0 @@
# https://editorconfig.org
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
[Makefile]
indent_style = tab
[*.go]
indent_style = tab

View File

@@ -1,2 +0,0 @@
/ping
/dist

View File

@@ -1,6 +0,0 @@
---
issues:
exclude-rules:
- path: _test.go
linters:
- errcheck

View File

@@ -1,46 +0,0 @@
project_name: ping
before:
hooks:
- go mod download
builds:
- binary: ping
dir: cmd/ping
goarch:
- amd64
- arm
- arm64
goarm:
- 6
- 7
goos:
- darwin
- freebsd
- linux
- windows
archives:
- files:
- LICENSE
- README.md
format_overrides:
- goos: windows
format: zip
wrap_in_directory: true
# TODO: Decide if we want packages (name conflcits with /bin/ping?)
# nfpms:
# homepage: https://github.com/go-ping/ping
# maintainer: 'Go Ping Maintainers <go-ping@example.com>'
# description: Ping written in Go.
# license: MIT
# formats:
# - deb
# - rpm
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-{{ .ShortCommit }}"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

View File

@@ -1,44 +0,0 @@
# Contributing
First off, thanks for taking the time to contribute!
Remember that this is open source software so please consider the other people who will read your code.
Make it look nice for them, document your logic in comments and add or update the unit test cases.
This library is used by various other projects, companies and individuals in live production environments so please discuss any breaking changes with us before making them.
Feel free to join us in the #go-ping channel of the [Gophers Slack](https://invite.slack.golangbridge.org/).
## Pull Requests
[Fork the repo on GitHub](https://github.com/go-ping/ping/fork) and clone it to your local machine.
```bash
git clone https://github.com/YOUR_USERNAME/ping.git && cd ping
```
Here is a guide on [how to configure a remote repository](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/configuring-a-remote-for-a-fork).
Check out a new branch, make changes, run tests, commit & sign-off, then push branch to your fork.
```bash
$ git checkout -b <BRANCH_NAME>
# edit files
$ make style vet test
$ git add <CHANGED_FILES>
$ git commit -s
$ git push <FORK> <BRANCH_NAME>
```
Open a [new pull request](https://github.com/go-ping/ping/compare) in the main `go-ping/ping` repository.
Please describe the purpose of your PR and remember link it to any related issues.
*We may ask you to rebase your feature branch or squash the commits in order to keep the history clean.*
## Development Guides
- Run `make style vet test` before committing your changes.
- Document your logic in code comments.
- Add tests for bug fixes and new features.
- Use UNIX-style (LF) line endings.
- End every file with a single blank line.
- Use the UTF-8 character set.

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 Cameron Sparr and contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,32 +0,0 @@
GO ?= go
GOFMT ?= $(GO)fmt
GOOPTS ?=
GO111MODULE :=
pkgs = ./...
all: style vet build test
.PHONY: build
build:
@echo ">> building ping"
GO111MODULE=$(GO111MODULE) $(GO) build $(GOOPTS) ./cmd/ping
.PHONY: style
style:
@echo ">> checking code style"
@fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \
if [ -n "$${fmtRes}" ]; then \
echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \
echo "Please ensure you are using $$($(GO) version) for formatting code."; \
exit 1; \
fi
.PHONY: test
test:
@echo ">> running all tests"
GO111MODULE=$(GO111MODULE) $(GO) test -race -cover $(GOOPTS) $(pkgs)
.PHONY: vet
vet:
@echo ">> vetting code"
GO111MODULE=$(GO111MODULE) $(GO) vet $(GOOPTS) $(pkgs)

View File

@@ -1,141 +0,0 @@
# go-ping
[![PkgGoDev](https://pkg.go.dev/badge/github.com/go-ping/ping)](https://pkg.go.dev/github.com/go-ping/ping)
[![Circle CI](https://circleci.com/gh/go-ping/ping.svg?style=svg)](https://circleci.com/gh/go-ping/ping)
A simple but powerful ICMP echo (ping) library for Go, inspired by
[go-fastping](https://github.com/tatsushid/go-fastping).
Here is a very simple example that sends and receives three packets:
```go
pinger, err := ping.NewPinger("www.google.com")
if err != nil {
panic(err)
}
pinger.Count = 3
err = pinger.Run() // Blocks until finished.
if err != nil {
panic(err)
}
stats := pinger.Statistics() // get send/receive/duplicate/rtt stats
```
Here is an example that emulates the traditional UNIX ping command:
```go
pinger, err := ping.NewPinger("www.google.com")
if err != nil {
panic(err)
}
// Listen for Ctrl-C.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for _ = range c {
pinger.Stop()
}
}()
pinger.OnRecv = func(pkt *ping.Packet) {
fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n",
pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt)
}
pinger.OnDuplicateRecv = func(pkt *ping.Packet) {
fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v ttl=%v (DUP!)\n",
pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt, pkt.Ttl)
}
pinger.OnFinish = func(stats *ping.Statistics) {
fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr)
fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n",
stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss)
fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n",
stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt)
}
fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr())
err = pinger.Run()
if err != nil {
panic(err)
}
```
It sends ICMP Echo Request packet(s) and waits for an Echo Reply in
response. If it receives a response, it calls the `OnRecv` callback
unless a packet with that sequence number has already been received,
in which case it calls the `OnDuplicateRecv` callback. When it's
finished, it calls the `OnFinish` callback.
For a full ping example, see
[cmd/ping/ping.go](https://github.com/go-ping/ping/blob/master/cmd/ping/ping.go).
## Installation
```
go get -u github.com/go-ping/ping
```
To install the native Go ping executable:
```bash
go get -u github.com/go-ping/ping/...
$GOPATH/bin/ping
```
## Supported Operating Systems
### Linux
This library attempts to send an "unprivileged" ping via UDP. On Linux,
this must be enabled with the following sysctl command:
```
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"
```
If you do not wish to do this, you can call `pinger.SetPrivileged(true)`
in your code and then use setcap on your binary to allow it to bind to
raw sockets (or just run it as root):
```
setcap cap_net_raw=+ep /path/to/your/compiled/binary
```
See [this blog](https://sturmflut.github.io/linux/ubuntu/2015/01/17/unprivileged-icmp-sockets-on-linux/)
and the Go [x/net/icmp](https://godoc.org/golang.org/x/net/icmp) package
for more details.
### Windows
You must use `pinger.SetPrivileged(true)`, otherwise you will receive
the following error:
```
socket: The requested protocol has not been configured into the system, or no implementation for it exists.
```
Despite the method name, this should work without the need to elevate
privileges and has been tested on Windows 10. Please note that accessing
packet TTL values is not supported due to limitations in the Go
x/net/ipv4 and x/net/ipv6 packages.
### Plan 9 from Bell Labs
There is no support for Plan 9. This is because the entire `x/net/ipv4`
and `x/net/ipv6` packages are not implemented by the Go programming
language.
## Maintainers and Getting Help:
This repo was originally in the personal account of
[sparrc](https://github.com/sparrc), but is now maintained by the
[go-ping organization](https://github.com/go-ping).
For support and help, you usually find us in the #go-ping channel of
Gophers Slack. See https://invite.slack.golangbridge.org/ for an invite
to the Gophers Slack org.
## Contributing
Refer to [CONTRIBUTING.md](https://github.com/go-ping/ping/blob/master/CONTRIBUTING.md)

Some files were not shown because too many files have changed in this diff Show More