mirror of
https://github.com/adnanh/webhook.git
synced 2025-08-02 16:00:30 +00:00
Compare commits
357 commits
Author | SHA1 | Date | |
---|---|---|---|
|
1b13355196 | ||
|
eb7e8f5ba8 | ||
|
7bb680821d | ||
|
ce08a68a13 | ||
|
f89b09bef6 | ||
|
9f725b2cb0 | ||
|
98cf5d0163 | ||
|
9cd78fca1a | ||
|
eddeb82032 | ||
|
b6f24d00a5 | ||
|
d84cc5420b | ||
|
fc0623363a | ||
|
48c76cf80d | ||
|
85f244c98e | ||
|
8ee2601081 | ||
|
0fa8bbf710 | ||
|
dbc6565c35 | ||
|
9a7986681d | ||
|
de4003a7a8 | ||
|
bd1aaab0ad | ||
|
a4eebd6005 | ||
|
56a960e3bd | ||
|
6daf4c29ac | ||
|
3944b35d39 | ||
|
dc5d09a0d7 | ||
|
f187592147 | ||
|
a79e7d2cef | ||
|
5ed642354f | ||
|
dab29e7267 | ||
|
0c0bf0b244 | ||
|
c7f7163aaa | ||
|
36e77b1c7a | ||
|
5189c62651 | ||
|
75f406845f | ||
|
105b019e2b | ||
|
4f00a26293 | ||
|
560cbaae74 | ||
|
3285288f03 | ||
|
2a36f24269 | ||
|
1ec494fb0d | ||
|
e329b6d9ff | ||
|
181672afcc | ||
|
d523af1b6c | ||
|
390e3bd772 | ||
|
21549749c0 | ||
|
6184509494 | ||
|
b1f69564a3 | ||
|
159cb4a911 | ||
|
b5af9a3968 | ||
|
2e4aea4cbc | ||
|
b6e5b11174 | ||
|
9dec52c727 | ||
|
f2b536dbad | ||
|
62f9c01cab | ||
|
6d2f26d952 | ||
|
c2ffd465c4 | ||
|
3e18a060ae | ||
|
6f5962f8f2 | ||
|
346c761ef6 | ||
|
e513eb4bf4 | ||
|
22c8a1670b | ||
|
9c7f8c1ac4 | ||
|
4fadb1171f | ||
|
dc184d2737 | ||
|
7467933680 | ||
|
fd50118712 | ||
|
67c317e741 | ||
|
ab3ff0343e | ||
|
f007fa5280 | ||
|
a904537367 | ||
|
0814b10a16 | ||
|
d279505930 | ||
|
0f4bbfac9f | ||
|
6bbf14f7d9 | ||
|
6797bf7cf7 | ||
|
c6603894c1 | ||
|
b8498c564d | ||
|
dd5fa20415 | ||
|
c7a8fbc929 | ||
|
04ca211531 | ||
|
ae5e9e7894 | ||
|
47e5ae5527 | ||
|
534e99bf13 | ||
|
0e90ccb441 | ||
|
f692da2465 | ||
|
fb9b22a118 | ||
|
eefcd7f7d5 | ||
|
c4f29b5d8b | ||
|
dd84a68483 | ||
|
c9199d62e4 | ||
|
3d824b47b7 | ||
|
cc98de88ce | ||
|
e71b45b28f | ||
|
41ac427a89 | ||
|
7b3c5fd028 | ||
|
526c9a20ac | ||
|
a75ab4f92f | ||
|
345bf3d409 | ||
|
e6e324235d | ||
|
6c8d2e6b6d | ||
|
c8ea86f6ce | ||
|
4f437e4642 | ||
|
7267733aa8 | ||
|
95bd1b3072 | ||
|
9cb199c8b3 | ||
|
4407c0190b | ||
|
4897bea79f | ||
|
38294cd0c6 | ||
|
dc4f42bb26 | ||
|
472ce4863f | ||
|
de626ab2bb | ||
|
f8c8932866 | ||
|
6d3b81fc61 | ||
|
11e0031a9f | ||
|
53f63a7614 | ||
|
8c5b2e0c17 | ||
|
7fa3a8900c | ||
|
e1634fe669 | ||
|
876c853073 | ||
|
569921cd72 | ||
|
fda328dc23 | ||
|
aa03daeff8 | ||
|
811481298a | ||
|
5af6e4d1ec | ||
|
725fda68dc | ||
|
157f468e0c | ||
|
e1249a9ddb | ||
|
a03e812615 | ||
|
c38778ba62 | ||
|
3414f34025 | ||
|
66562fdb41 | ||
|
77159d9db6 | ||
|
35d1cedc24 | ||
|
f38dfbbf78 | ||
|
78b0610218 | ||
|
c6c270c7dd | ||
|
1c779a0d75 | ||
|
8702b37430 | ||
|
5b4e60e7d7 | ||
|
93632d077c | ||
|
cc5cbae14f | ||
|
779ff0ad10 | ||
|
d8bd2662ff | ||
|
28e0012470 | ||
|
3463804a7c | ||
|
3f4520da67 | ||
|
be815d0a41 | ||
|
93ce24d3f3 | ||
|
e72a7d2e22 | ||
|
40d9dcd6d4 | ||
|
c872aae7e8 | ||
|
8ff3848ea3 | ||
|
7b87d6092f | ||
|
669414ca70 | ||
|
3f5fee20c0 | ||
|
c6e809a1a2 | ||
|
2088f61cba | ||
|
a818e29113 | ||
|
8fe6c9a05d | ||
|
7c4e6e94fc | ||
|
31e76bcd00 | ||
|
c47c06e822 | ||
|
bf3d042da6 | ||
|
d05911cdcb | ||
|
634ca84807 | ||
|
8c46a8343b | ||
|
13d5630e80 | ||
|
f1003560f1 | ||
|
997db04b9f | ||
|
769e743563 | ||
|
43f519a712 | ||
|
a617b1a6ac | ||
|
9117f4f6d6 | ||
|
b53996f175 | ||
|
154177e46a | ||
|
d4e98281d7 | ||
|
ce186487f4 | ||
|
1110f82443 | ||
|
a99abd4e6f | ||
|
8728ec4786 | ||
|
34ae132930 | ||
|
f993aaa11c | ||
|
d82e838554 | ||
|
9c35aa070c | ||
|
14ee68a06e | ||
|
e0e1dd8ade | ||
|
9852f0f0a5 | ||
|
2cf3f4e1a8 | ||
|
1cf531b1c3 | ||
|
74e55e3089 | ||
|
6c77ff0a2c | ||
|
002c332b68 | ||
|
ffba396523 | ||
|
b016e99ea6 | ||
|
e47f9afb11 | ||
|
d3fd9bddd9 | ||
|
42b72b028b | ||
|
c6939d57dc | ||
|
90f751a61d | ||
|
e86c2cf610 | ||
|
0aa7395e21 | ||
|
3f834f5c3d | ||
|
5bca86cdb2 | ||
|
896d1608ca | ||
|
e5c18aa87e | ||
|
f056f94305 | ||
|
1a17dc83fe | ||
|
753734428f | ||
|
f76426e9b0 | ||
|
5803d5e849 | ||
|
385898b21f | ||
|
ce7f8d5d28 | ||
|
b2899d1d3e | ||
|
e8628cd662 | ||
|
4e1719d966 | ||
|
98f86cf044 | ||
|
fc0544e4a2 | ||
|
537f5c21bc | ||
|
a0880ab82d | ||
|
5636ead921 | ||
|
01e0c9e972 | ||
|
54a7190113 | ||
|
b65bdbbb24 | ||
|
ef3f43f89f | ||
|
22073d8847 | ||
|
c05ca8c528 | ||
|
f9e799fea0 | ||
|
9b99452b60 | ||
|
b93cdc346e | ||
|
d59f6228ad | ||
|
78c8c61bf2 | ||
|
7ed5d4af9b | ||
|
7905c74687 | ||
|
2fb08ab579 | ||
|
681e8b6459 | ||
|
8a3770db29 | ||
|
ae54669c02 | ||
|
b449793825 | ||
|
66a9e48e39 | ||
|
d85ee5e068 | ||
|
7da4d8ba9f | ||
|
8d260c6a7e | ||
|
48061f1508 | ||
|
cfed5cfe4b | ||
|
471c849c50 | ||
|
337621998e | ||
|
0feeb945fc | ||
|
4f9ed434c5 | ||
|
0934b9414c | ||
|
356870358d | ||
|
6dc331726d | ||
|
3f8dbf09dc | ||
|
dcda096b5d | ||
|
7079128eca | ||
|
6e3ec89ce1 | ||
|
10396a5434 | ||
|
d009919755 | ||
|
a811db410b | ||
|
357c471667 | ||
|
85889fe378 | ||
|
b595694658 | ||
|
ecbba514e5 | ||
|
13555e1ef1 | ||
|
ddec431ae9 | ||
|
f84edae99d | ||
|
514388e39e | ||
|
a49364ae93 | ||
|
6c4e52d87e | ||
|
b161de13f2 | ||
|
e2f6e4eb37 | ||
|
0d3d29055b | ||
|
a40fba5e29 | ||
|
137ded093d | ||
|
24ec77cb6a | ||
|
7f3529e302 | ||
|
df781925f1 | ||
|
44d19e34a0 | ||
|
b1e0c0bf01 | ||
|
bab32db017 | ||
|
5a69eab2b3 | ||
|
01aa178bb4 | ||
|
f5f04ddaa2 | ||
|
ba0adb117a | ||
|
c107bb4539 | ||
|
36c5a52539 | ||
|
027f2d7363 | ||
|
c9abc252e8 | ||
|
6145634670 | ||
|
41b75acc70 | ||
|
83e688efd3 | ||
|
b1851784b5 | ||
|
3c0508a8d3 | ||
|
7a91168f23 | ||
|
2adf724295 | ||
|
2f7eb31e8d | ||
|
717ad779a3 | ||
|
1e67afc3de | ||
|
c094740905 | ||
|
de494d4cc6 | ||
|
65bd1cdd10 | ||
|
7af1fe8e02 | ||
|
5def341367 | ||
|
a422170c61 | ||
|
739388c0dc | ||
|
9c1efc91a2 | ||
|
b663783717 | ||
|
e748910b6a | ||
|
78ca57908c | ||
|
3f42ac2906 | ||
|
91c1675cc0 | ||
|
213e4529e8 | ||
|
8c96ffd5f9 | ||
|
f0e26bc03c | ||
|
71269d8b45 | ||
|
da39872a77 | ||
|
985d5197ed | ||
|
142e912d6a | ||
|
241061c26d | ||
|
c6febd35b5 | ||
|
78aa85e0c1 | ||
|
395fb41d23 | ||
|
1fc4445668 | ||
|
d52d7bde1c | ||
|
34c4b1c166 | ||
|
147c95dd8b | ||
|
cfd138c96d | ||
|
c19e514ee9 | ||
|
c7ec25f378 | ||
|
6916f89a2b | ||
|
ed251c3cc7 | ||
|
9db13ef649 | ||
|
98512463d8 | ||
|
53ec2e7acc | ||
|
c581e0532b | ||
|
3a75bbf4db | ||
|
2f1ea098c0 | ||
|
ccd28908db | ||
|
69a4aec1f3 | ||
|
a38875213a | ||
|
249962f002 | ||
|
ced9e01895 | ||
|
5561fa3308 | ||
|
47b82c7c4a | ||
|
3978b6687d | ||
|
81b1bd7c7b | ||
|
45cf312b8d | ||
|
2cfc1ce2ff | ||
|
6b1021925b | ||
|
af22498d1e | ||
|
5b567d1631 | ||
|
4aa81b727f | ||
|
173273b466 | ||
|
8803239e6d | ||
|
c8a83349d2 | ||
|
1da40d4634 | ||
|
79453b7a22 | ||
|
8207c6cf12 |
585 changed files with 255601 additions and 1287 deletions
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
open_collective: webhook
|
||||
github: adnanh
|
2
.github/ISSUE_TEMPLATE.md
vendored
Normal file
2
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
<!-- Love webhook? Please consider supporting our collective:
|
||||
👉 https://opencollective.com/webhook/donate -->
|
30
.github/workflows/build.yml
vendored
Normal file
30
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
name: build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
# The special value "local" tells Go to use the bundled Go
|
||||
# version rather than trying to fetch one according to a
|
||||
# `toolchain` value in `go.mod`. This ensures that we're
|
||||
# really running the Go version in the CI matrix rather than
|
||||
# one that the Go command has upgraded to automatically.
|
||||
GOTOOLCHAIN: local
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.21.x, 1.22.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Build
|
||||
run: go build -v
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,3 +2,5 @@
|
|||
.cover
|
||||
coverage
|
||||
webhook
|
||||
/test/hookecho
|
||||
build
|
||||
|
|
32
.travis.yml
Normal file
32
.travis.yml
Normal file
|
@ -0,0 +1,32 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.14.x
|
||||
- master
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- go: master
|
||||
exclude:
|
||||
- os: windows
|
||||
go: master
|
||||
- os: windows
|
||||
arch: arm64
|
||||
- os: osx
|
||||
arch: arm64
|
||||
|
||||
install:
|
||||
- go get -d -v -t ./...
|
||||
|
||||
script:
|
||||
- go test -v -race ./...
|
67
CONTRIBUTING.md
Normal file
67
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,67 @@
|
|||
# Contribute
|
||||
|
||||
## Introduction
|
||||
|
||||
First, thank you for considering contributing to webhook! It's people like you that make the open source community such a great community! 😊
|
||||
|
||||
We welcome any type of contribution, not only code. You can help with
|
||||
- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open)
|
||||
- **Marketing**: writing blog posts, howto's, printing stickers, ...
|
||||
- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ...
|
||||
- **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them.
|
||||
- **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/webhook).
|
||||
|
||||
## Your First Contribution
|
||||
|
||||
Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
|
||||
|
||||
## Submitting code
|
||||
|
||||
Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests.
|
||||
|
||||
## Code review process
|
||||
|
||||
The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge.
|
||||
It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you?
|
||||
|
||||
## Financial contributions
|
||||
|
||||
We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/webhook).
|
||||
Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
|
||||
|
||||
## Questions
|
||||
|
||||
If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!).
|
||||
You can also reach us at hello@webhook.opencollective.com.
|
||||
|
||||
## Credits
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you to all the people who have already contributed to webhook!
|
||||
<a href="graphs/contributors"><img src="https://opencollective.com/webhook/contributors.svg?width=890" /></a>
|
||||
|
||||
|
||||
### Backers
|
||||
|
||||
Thank you to all our backers! [[Become a backer](https://opencollective.com/webhook#backer)]
|
||||
|
||||
<a href="https://opencollective.com/webhook#backers" target="_blank"><img src="https://opencollective.com/webhook/backers.svg?width=890"></a>
|
||||
|
||||
|
||||
### Sponsors
|
||||
|
||||
Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/webhook#sponsor))
|
||||
|
||||
<a href="https://opencollective.com/webhook/sponsor/0/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/1/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/2/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/3/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/4/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/5/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/6/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/7/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/8/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/9/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/9/avatar.svg"></a>
|
||||
|
||||
<!-- This `CONTRIBUTING.md` is based on @nayafia's template https://github.com/nayafia/contributing-template -->
|
31
Makefile
31
Makefile
|
@ -1,29 +1,44 @@
|
|||
OS = darwin freebsd linux openbsd windows
|
||||
OS = darwin freebsd linux openbsd
|
||||
ARCHS = 386 arm amd64 arm64
|
||||
|
||||
all: build release
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
build: deps
|
||||
.PHONY: help
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-16s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
all: build release release-windows
|
||||
|
||||
build: deps ## Build the project
|
||||
go build
|
||||
|
||||
release: clean deps
|
||||
release: clean deps ## Generate releases for unix systems
|
||||
@for arch in $(ARCHS);\
|
||||
do \
|
||||
for os in $(OS);\
|
||||
do \
|
||||
echo "Building $$os-$$arch"; \
|
||||
mkdir -p build/webhook-$$os-$$arch/; \
|
||||
GOOS=$$os GOARCH=$$arch go build -o build/webhook-$$os-$$arch/webhook; \
|
||||
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch go build -o build/webhook-$$os-$$arch/webhook; \
|
||||
tar cz -C build -f build/webhook-$$os-$$arch.tar.gz webhook-$$os-$$arch; \
|
||||
done \
|
||||
done
|
||||
|
||||
test: deps
|
||||
release-windows: clean deps ## Generate release for windows
|
||||
@for arch in $(ARCHS);\
|
||||
do \
|
||||
echo "Building windows-$$arch"; \
|
||||
mkdir -p build/webhook-windows-$$arch/; \
|
||||
GOOS=windows GOARCH=$$arch go build -o build/webhook-windows-$$arch/webhook.exe; \
|
||||
tar cz -C build -f build/webhook-windows-$$arch.tar.gz webhook-windows-$$arch; \
|
||||
done
|
||||
|
||||
test: deps ## Execute tests
|
||||
go test ./...
|
||||
|
||||
deps:
|
||||
deps: ## Install dependencies using go get
|
||||
go get -d -v -t ./...
|
||||
|
||||
clean:
|
||||
clean: ## Remove building artifacts
|
||||
rm -rf build
|
||||
rm -f webhook
|
||||
|
|
182
README.md
182
README.md
|
@ -1,12 +1,14 @@
|
|||
[](https://ghit.me/repo/adnanh/webhook) [](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook) [Donate via PayPal](https://paypal.me/hookdoo) | [Patreon page](https://www.patreon.com/webhook)
|
||||
# What is webhook?
|
||||
[webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
|
||||
# What is webhook? ![build-status][badge]
|
||||
|
||||
For example, if you're using Github or Bitbucket, you can use [webhook](https://github.com/adnanh/webhook/) to set up a hook that runs a redeploy script for your project on your staging server, whenever you push changes to the master branch of your project.
|
||||
<img src="https://github.com/adnanh/webhook/raw/development/docs/logo/logo-128x128.png" alt="Webhook" align="left" />
|
||||
|
||||
If you use Slack, you can set up an "Outgoing webhook integration" to run various commands on your server, which can then report back directly to your Slack channels using the "Incoming webhook integrations".
|
||||
[webhook][w] is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook][w] also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
|
||||
|
||||
[webhook](https://github.com/adnanh/webhook/) aims to do nothing more than it should do, and that is:
|
||||
For example, if you're using Github or Bitbucket, you can use [webhook][w] to set up a hook that runs a redeploy script for your project on your staging server, whenever you push changes to the master branch of your project.
|
||||
|
||||
If you use Mattermost or Slack, you can set up an "Outgoing webhook integration" or "Slash command" to run various commands on your server, which can then report back directly to you or your channels using the "Incoming webhook integrations", or the appropriate response body.
|
||||
|
||||
[webhook][w] aims to do nothing more than it should do, and that is:
|
||||
1. receive the request,
|
||||
2. parse the headers, payload and query variables,
|
||||
3. check if the specified rules for the hook are satisfied,
|
||||
|
@ -15,25 +17,43 @@ If you use Slack, you can set up an "Outgoing webhook integration" to run variou
|
|||
|
||||
Everything else is the responsibility of the command's author.
|
||||
|
||||
---
|
||||
## Not what you're looking for?
|
||||
| <a href="https://www.hookdoo.com/?github"><img src="https://hookdoo.com/img/Hookdoo_Logo_1.png" height="48" alt="hookdoo" /></a> | <picture><source media="(prefers-color-scheme: dark)" srcset="images/hookdeck-white.svg"><img height="36" alt="hookdeck" src="images/hookdeck-black.svg"></picture></a> |
|
||||
| :-: | :-: |
|
||||
| Scriptable webhook gateway to safely run your custom builds, deploys, and proxy scripts on your servers. | An event gateway to reliably ingest, verify, queue, transform, filter, inspect, monitor, and replay webhooks. |
|
||||
|
||||
|
||||
# Getting started
|
||||
## Installation
|
||||
### Building from source
|
||||
To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the
|
||||
To get started, first make sure you've properly set up your [Go](http://golang.org/doc/install) 1.21 or newer environment and then run
|
||||
```bash
|
||||
$ go get github.com/adnanh/webhook
|
||||
$ go build github.com/adnanh/webhook
|
||||
```
|
||||
to get the latest version of the [webhook](https://github.com/adnanh/webhook/).
|
||||
to build the latest version of the [webhook][w].
|
||||
|
||||
### Using package manager
|
||||
#### Debian "sid"
|
||||
If you are using unstable version of Debian linux ("sid"), you can install webhook using `apt-get install webhook` which will install community packaged version (thanks [@freeekanayaka](https://github.com/freeekanayaka)) from https://packages.debian.org/sid/webhook
|
||||
#### Snap store
|
||||
[](https://snapcraft.io/webhook)
|
||||
|
||||
#### Ubuntu
|
||||
If you are using Ubuntu linux (17.04 or later), you can install webhook using `sudo apt-get install webhook` which will install community packaged version.
|
||||
|
||||
#### Debian
|
||||
If you are using Debian linux ("stretch" or later), you can install webhook using `sudo apt-get install webhook` which will install community packaged version (thanks [@freeekanayaka](https://github.com/freeekanayaka)) from https://packages.debian.org/sid/webhook
|
||||
|
||||
#### FreeBSD
|
||||
If you are using FreeBSD, you can install webhook using `pkg install webhook`.
|
||||
|
||||
### Download prebuilt binaries
|
||||
Prebuilt binaries for different architectures are available at [GitHub Releases](https://github.com/adnanh/webhook/releases).
|
||||
|
||||
## Configuration
|
||||
Next step is to define some hooks you want [webhook](https://github.com/adnanh/webhook/) to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook](https://github.com/adnanh/webhook/) will serve. Check [Hook definition page](https://github.com/adnanh/webhook/wiki/Hook-Definition) to see the detailed description of what properties a hook can contain, and how to use them.
|
||||
Next step is to define some hooks you want [webhook][w] to serve.
|
||||
[webhook][w] supports JSON or YAML configuration files, but we'll focus primarily on JSON in the following example.
|
||||
Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook][w] will serve. Check [Hook definition page](docs/Hook-Definition.md) to see the detailed description of what properties a hook can contain, and how to use them.
|
||||
|
||||
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`.
|
||||
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`. Make sure that your bash script has `#!/bin/sh` shebang on top.
|
||||
|
||||
Our `hooks.json` file will now look like this:
|
||||
```json
|
||||
|
@ -46,7 +66,14 @@ Our `hooks.json` file will now look like this:
|
|||
]
|
||||
```
|
||||
|
||||
You can now run [webhook](https://github.com/adnanh/webhook/) using
|
||||
**NOTE:** If you prefer YAML, the equivalent `hooks.yaml` file would be:
|
||||
```yaml
|
||||
- id: redeploy-webhook
|
||||
execute-command: "/var/scripts/redeploy.sh"
|
||||
command-working-directory: "/var/webhook"
|
||||
```
|
||||
|
||||
You can now run [webhook][w] using
|
||||
```bash
|
||||
$ /path/to/webhook -hooks hooks.json -verbose
|
||||
```
|
||||
|
@ -56,32 +83,128 @@ It will start up on default port 9000 and will provide you with one HTTP endpoin
|
|||
http://yourserver:9000/hooks/redeploy-webhook
|
||||
```
|
||||
|
||||
Check [webhook parameters page](https://github.com/adnanh/webhook/wiki/Webhook-Parameters) to see how to override the ip, port and other settings such as hook hotreload, verbose output, etc, when starting the [webhook](https://github.com/adnanh/webhook/).
|
||||
Check [webhook parameters page](docs/Webhook-Parameters.md) to see how to override the ip, port and other settings such as hook hotreload, verbose output, etc, when starting the [webhook][w].
|
||||
|
||||
By performing a simple HTTP GET or POST request to that endpoint, your specified redeploy script would be executed. Neat!
|
||||
|
||||
However, hook defined like that could pose a security threat to your system, because anyone who knows your endpoint, can send a request and execute your command. To prevent that, you can use the `"trigger-rule"` property for your hook, to specify the exact circumstances under which the hook would be triggered. For example, you can use them to add a secret that you must supply as a parameter in order to successfully trigger the hook. Please check out the [Hook rules page](https://github.com/adnanh/webhook/wiki/Hook-Rules) for detailed list of available rules and their usage.
|
||||
However, hook defined like that could pose a security threat to your system, because anyone who knows your endpoint, can send a request and execute your command. To prevent that, you can use the `"trigger-rule"` property for your hook, to specify the exact circumstances under which the hook would be triggered. For example, you can use them to add a secret that you must supply as a parameter in order to successfully trigger the hook. Please check out the [Hook rules page](docs/Hook-Rules.md) for detailed list of available rules and their usage.
|
||||
|
||||
# Using HTTPS
|
||||
[webhook](https://github.com/adnanh/webhook/) by default serves hooks using http. If you want [webhook](https://github.com/adnanh/webhook/) to serve secure content using https, you can use the `-secure` flag while starting [webhook](https://github.com/adnanh/webhook/). Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
|
||||
## Multipart Form Data
|
||||
[webhook][w] provides limited support the parsing of multipart form data.
|
||||
Multipart form data can contain two types of parts: values and files.
|
||||
All form _values_ are automatically added to the `payload` scope.
|
||||
Use the `parse-parameters-as-json` settings to parse a given value as JSON.
|
||||
All files are ignored unless they match one of the following criteria:
|
||||
|
||||
# CORS Headers
|
||||
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook](https://github.com/adnanh/webhook/) to set the appropriate CORS headers that will be returned with each response.
|
||||
1. The `Content-Type` header is `application/json`.
|
||||
1. The part is named in the `parse-parameters-as-json` setting.
|
||||
|
||||
# Interested in running webhook inside of a Docker container?
|
||||
You can use [almir/webhook](https://hub.docker.com/r/almir/webhook/) docker image, or create your own (please read [this discussion](https://github.com/adnanh/webhook/issues/63)).
|
||||
In either case, the given file part will be parsed as JSON and added to the `payload` map.
|
||||
|
||||
# Examples
|
||||
Check out [Hook examples page](https://github.com/adnanh/webhook/wiki/Hook-Examples) for more complex examples of hooks.
|
||||
## Templates
|
||||
[webhook][w] can parse the hooks configuration file as a Go template when given the `-template` [CLI parameter](docs/Webhook-Parameters.md). See the [Templates page](docs/Templates.md) for more details on template usage.
|
||||
|
||||
# Contributing
|
||||
Any form of contribution is welcome and highly appreciated.
|
||||
## Using HTTPS
|
||||
[webhook][w] by default serves hooks using http. If you want [webhook][w] to serve secure content using https, you can use the `-secure` flag while starting [webhook][w]. Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
|
||||
|
||||
Big thanks to [all the current contributors](https://github.com/adnanh/webhook/graphs/contributors) for their contributions!
|
||||
TLS version and cipher suite selection flags are available from the command line. To list available cipher suites, use the `-list-cipher-suites` flag. The `-tls-min-version` flag can be used with `-list-cipher-suites`.
|
||||
|
||||
# Community Contributions
|
||||
## Running behind a reverse proxy
|
||||
[webhook][w] may be run behind a "reverse proxy" - another web-facing server such as [Apache httpd](https://httpd.apache.org) or [Nginx](https://nginx.org) that accepts requests from clients and forwards them on to [webhook][h]. You can have [webhook][w] listen on a regular TCP port or on a Unix domain socket (with the `-socket` flag), then configure your proxy to send requests for a specific host name or sub-path over that port or socket to [webhook][w].
|
||||
|
||||
Note that when running in this mode the [`ip-whitelist`](docs/Hook-Rules.md#match-whitelisted-ip-range) trigger rule will not work as expected, since it will be checking the address of the _proxy_, not the _client_. Client IP restrictions will need to be enforced within the proxy, before it decides whether to forward the request to [webhook][w].
|
||||
|
||||
## CORS Headers
|
||||
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook][w] to set the appropriate CORS headers that will be returned with each response.
|
||||
|
||||
## Running under `systemd`
|
||||
On platforms that use [systemd](https://systemd.io), [webhook][w] supports the _socket activation_ mechanism. If [webhook][w] detects that it has been launched from a systemd-managed socket it will automatically use that instead of opening its own listening port. See [the systemd page](docs/Systemd-Activation.md) for full details.
|
||||
|
||||
## Interested in running webhook inside of a Docker container?
|
||||
You can use one of the following Docker images, or create your own (please read [this discussion](https://github.com/adnanh/webhook/issues/63)):
|
||||
- [almir/webhook](https://github.com/almir/docker-webhook)
|
||||
- [roxedus/webhook](https://github.com/Roxedus/docker-webhook)
|
||||
- [thecatlady/webhook](https://github.com/thecatlady/docker-webhook)
|
||||
- [lwlook/webhook](https://hub.docker.com/r/lwlook/webhook) - This setup allows direct access to the Docker host, providing a streamlined and efficient way to manage webhooks.
|
||||
|
||||
## Examples
|
||||
Check out [Hook examples page](docs/Hook-Examples.md) for more complex examples of hooks.
|
||||
|
||||
### Guides featuring webhook
|
||||
- [Plex 2 Telegram](https://gitlab.com/-/snippets/1972594) by [@psyhomb](https://github.com/psyhomb)
|
||||
- [Webhook & JIRA](https://sites.google.com/site/mrxpalmeiras/more/jira-webhooks) by [@perfecto25](https://github.com/perfecto25)
|
||||
- [Trigger Ansible AWX job runs on SCM (e.g. git) commit](http://jpmens.net/2017/10/23/trigger-awx-job-runs-on-scm-commit/) by [@jpmens](http://mens.de/)
|
||||
- [Deploy using GitHub webhooks](https://davidauthier.wearemd.com/blog/deploy-using-github-webhooks.html) by [@awea](https://davidauthier.wearemd.com)
|
||||
- [Setting up Automatic Deployment and Builds Using Webhooks](https://willbrowning.me/setting-up-automatic-deployment-and-builds-using-webhooks/) by [Will Browning](https://willbrowning.me/about/)
|
||||
- [Auto deploy your Node.js app on push to GitHub in 3 simple steps](https://webhookrelay.com/blog/2018/07/17/auto-deploy-on-git-push/) by Karolis Rusenas
|
||||
- [Automate Static Site Deployments with Salt, Git, and Webhooks](https://www.linode.com/docs/applications/configuration-management/automate-a-static-site-deployment-with-salt/) by [Linode](https://www.linode.com)
|
||||
- [Using Prometheus to Automatically Scale WebLogic Clusters on Kubernetes](https://blogs.oracle.com/weblogicserver/using-prometheus-to-automatically-scale-weblogic-clusters-on-kubernetes-v5) by [Marina Kogan](https://blogs.oracle.com/author/9a4fe754-1cc2-4c64-95fc-360642b62927)
|
||||
- [Github Pages and Jekyll - A New Platform for LACNIC Labs](https://labs.lacnic.net/a-new-platform-for-lacniclabs/) by [Carlos Martínez Cagnazzo](https://twitter.com/carlosm3011)
|
||||
- [How to Deploy React Apps Using Webhooks and Integrating Slack on Ubuntu](https://www.alibabacloud.com/blog/how-to-deploy-react-apps-using-webhooks-and-integrating-slack-on-ubuntu_594116) by Arslan Ud Din Shafiq
|
||||
- [Private webhooks](https://ihateithe.re/2018/01/private-webhooks/) by [Thomas](https://ihateithe.re/colophon/)
|
||||
- [Adventures in webhooks](https://medium.com/@draketech/adventures-in-webhooks-2d6584501c62) by [Drake](https://medium.com/@draketech)
|
||||
- [GitHub pro tips](http://notes.spencerlyon.com/2016/01/04/github-pro-tips/) by [Spencer Lyon](http://notes.spencerlyon.com/)
|
||||
- [XiaoMi Vacuum + Amazon Button = Dash Cleaning](https://www.instructables.com/id/XiaoMi-Vacuum-Amazon-Button-Dash-Cleaning/) by [c0mmensal](https://www.instructables.com/member/c0mmensal/)
|
||||
- [Set up Automated Deployments From Github With Webhook](https://maximorlov.com/automated-deployments-from-github-with-webhook/) by [Maxim Orlov](https://twitter.com/_maximization)
|
||||
- [Kick Me Now with Webhooks](https://freebsdfoundation.org/kick-me-now-with-webhooks/) By Dave Cottlehuber
|
||||
- VIDEO: [Gitlab CI/CD configuration using Docker and adnanh/webhook to deploy on VPS - Tutorial #1](https://www.youtube.com/watch?v=Qhn-lXjyrZA&feature=youtu.be) by [Yes! Let's Learn Software Engineering](https://www.youtube.com/channel/UCH4XJf2BZ_52fbf8fOBMF3w)
|
||||
- [Integrate automatic deployment in 20 minutes using webhooks + Nginx setup](https://anksus.me/blog/integrate-automatic-deployment-in-20-minutes-using-webhooks) by [Anksus](https://github.com/Anksus)
|
||||
- [Automatically redeploy your static blog with Gitea, Uberspace & Webhook](https://by.arran.nz/posts/code/webhook-deploy/) by [Arran](https://arran.nz)
|
||||
- [Automatically Updating My Zola Site Using a Webhook](https://osc.garden/blog/updating-site-with-webhook/) by [Óscar Fernández](https://osc.garden/)
|
||||
- ...
|
||||
- Want to add your own? Open an Issue or create a PR :-)
|
||||
|
||||
## Community Contributions
|
||||
See the [webhook-contrib][wc] repository for a collections of tools and helpers related to [webhook][w] that have been contributed by the [webhook][w] community.
|
||||
|
||||
## Need help?
|
||||
Check out [existing issues](https://github.com/adnanh/webhook/issues) to see if someone else also had the same problem, or [open a new one](https://github.com/adnanh/webhook/issues/new).
|
||||
|
||||
# Support active development
|
||||
|
||||
## Sponsors
|
||||
## <a href="https://www.digitalocean.com/?ref=webhook"><img src="http://www.hajdarevic.net/DO_Logo_Horizontal_Blue.png" alt="DigitalOcean" width="250"/></a>
|
||||
[DigitalOcean](https://www.digitalocean.com/?ref=webhook) is a simple and robust cloud computing platform, designed for developers.
|
||||
|
||||
|
||||
## <a href="https://www.browserstack.com/?ref=webhook"><img src="http://www.hajdarevic.net/browserstack.svg" alt="BrowserStack" width="250"/></a>
|
||||
[BrowserStack](https://www.browserstack.com/?ref=webhook) is a cloud-based cross-browser testing tool that enables developers to test their websites across various browsers on different operating systems and mobile devices, without requiring users to install virtual machines, devices or emulators.
|
||||
|
||||
---
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website.
|
||||
|
||||
<a href="https://opencollective.com/webhook/sponsor/0/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/1/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/2/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/3/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/4/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/5/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/6/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/7/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/8/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/9/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/9/avatar.svg"></a>
|
||||
|
||||
## By contributing
|
||||
|
||||
This project exists thanks to all the people who contribute. [Contribute!](CONTRIBUTING.md).
|
||||
<a href="graphs/contributors"><img src="https://opencollective.com/webhook/contributors.svg?width=890" /></a>
|
||||
|
||||
## By giving money
|
||||
|
||||
- [OpenCollective Backer](https://opencollective.com/webhook#backer)
|
||||
- [OpenCollective Sponsor](https://opencollective.com/webhook#sponsor)
|
||||
- [PayPal](https://paypal.me/hookdoo)
|
||||
- [Patreon](https://www.patreon.com/webhook)
|
||||
- [Faircode](https://faircode.io/product/webhook?utm_source=badge&utm_medium=badgelarge&utm_campaign=webhook)
|
||||
- [Flattr](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook)
|
||||
|
||||
---
|
||||
|
||||
Thank you to all our backers!
|
||||
|
||||
<a href="https://opencollective.com/webhook#backers" target="_blank"><img src="https://opencollective.com/webhook/backers.svg?width=890"></a>
|
||||
|
||||
# License
|
||||
|
||||
The MIT License (MIT)
|
||||
|
@ -109,3 +232,4 @@ THE SOFTWARE.
|
|||
|
||||
[w]: https://github.com/adnanh/webhook
|
||||
[wc]: https://github.com/adnanh/webhook-contrib
|
||||
[badge]: https://github.com/adnanh/webhook/workflows/build/badge.svg
|
||||
|
|
28
docs/Hook-Definition.md
Normal file
28
docs/Hook-Definition.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Hook definition
|
||||
|
||||
Hooks are defined as objects in the JSON or YAML hooks configuration file. Please note that in order to be considered valid, a hook object must contain the `id` and `execute-command` properties. All other properties are considered optional.
|
||||
|
||||
## Properties (keys)
|
||||
|
||||
* `id` - specifies the ID of your hook. This value is used to create the HTTP endpoint (http://yourserver:port/hooks/your-hook-id)
|
||||
* `execute-command` - specifies the command that should be executed when the hook is triggered
|
||||
* `command-working-directory` - specifies the working directory that will be used for the script when it's executed
|
||||
* `response-message` - specifies the string that will be returned to the hook initiator
|
||||
* `response-headers` - specifies the list of headers in format `{"name": "X-Example-Header", "value": "it works"}` that will be returned in HTTP response for the hook
|
||||
* `success-http-response-code` - specifies the HTTP status code to be returned upon success
|
||||
* `incoming-payload-content-type` - sets the `Content-Type` of the incoming HTTP request (ie. `application/json`); useful when the request lacks a `Content-Type` or sends an erroneous value
|
||||
* `http-methods` - a list of allowed HTTP methods, such as `POST` and `GET`
|
||||
* `include-command-output-in-response` - boolean whether webhook should wait for the command to finish and return the raw output as a response to the hook initiator. If the command fails to execute or encounters any errors while executing the response will result in 500 Internal Server Error HTTP status code, otherwise the 200 OK status code will be returned.
|
||||
* `include-command-output-in-response-on-error` - boolean whether webhook should include command stdout & stderror as a response in failed executions. It only works if `include-command-output-in-response` is set to `true`.
|
||||
* `parse-parameters-as-json` - specifies the list of arguments that contain JSON strings. These parameters will be decoded by webhook and you can access them like regular objects in rules and `pass-arguments-to-command`.
|
||||
* `pass-arguments-to-command` - specifies the list of arguments that will be passed to the command. Check [Referencing request values page](Referencing-Request-Values.md) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
|
||||
`{ "source": "string", "name": "argumentvalue" }`
|
||||
* `pass-environment-to-command` - specifies the list of arguments that will be passed to the command as environment variables. If you do not specify the `"envname"` field in the referenced value, the hook will be in format "HOOK_argumentname", otherwise "envname" field will be used as it's name. Check [Referencing request values page](Referencing-Request-Values.md) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
|
||||
`{ "source": "string", "envname": "SOMETHING", "name": "argumentvalue" }`
|
||||
* `pass-file-to-command` - specifies a list of entries that will be serialized as a file. Incoming [data](Referencing-Request-Values.md) will be serialized in a request-temporary-file (otherwise parallel calls of the hook would lead to concurrent overwritings of the file). The filename to be addressed within the subsequent script is provided via an environment variable. Use `envname` to specify the name of the environment variable. If `envname` is not provided `HOOK_` and the name used to reference the request value are used. Defining `command-working-directory` will store the file relative to this location, if not provided, the systems temporary file directory will be used. If `base64decode` is true, the incoming binary data will be base 64 decoded prior to storing it into the file. By default the corresponding file will be removed after the webhook exited.
|
||||
* `trigger-rule` - specifies the rule that will be evaluated in order to determine should the hook be triggered. Check [Hook rules page](Hook-Rules.md) to see the list of valid rules and their usage
|
||||
* `trigger-rule-mismatch-http-response-code` - specifies the HTTP status code to be returned when the trigger rule is not satisfied
|
||||
* `trigger-signature-soft-failures` - allow signature validation failures within Or rules; by default, signature failures are treated as errors.
|
||||
|
||||
## Examples
|
||||
Check out [Hook examples page](Hook-Examples.md) for more complex examples of hooks.
|
675
docs/Hook-Examples.md
Normal file
675
docs/Hook-Examples.md
Normal file
|
@ -0,0 +1,675 @@
|
|||
# Hook Examples
|
||||
|
||||
Hooks are defined in a hooks configuration file in either JSON or YAML format,
|
||||
although the examples on this page all use the JSON format.
|
||||
|
||||
🌱 This page is still a work in progress. Feel free to contribute!
|
||||
|
||||
### Table of Contents
|
||||
|
||||
* [Incoming Github webhook](#incoming-github-webhook)
|
||||
* [Incoming Bitbucket webhook](#incoming-bitbucket-webhook)
|
||||
* [Incoming Gitlab webhook](#incoming-gitlab-webhook)
|
||||
* [Incoming Gogs webhook](#incoming-gogs-webhook)
|
||||
* [Incoming Gitea webhook](#incoming-gitea-webhook)
|
||||
* [Slack slash command](#slack-slash-command)
|
||||
* [A simple webhook with a secret key in GET query](#a-simple-webhook-with-a-secret-key-in-get-query)
|
||||
* [JIRA Webhooks](#jira-webhooks)
|
||||
* [Pass File-to-command sample](#pass-file-to-command-sample)
|
||||
* [Incoming Scalr Webhook](#incoming-scalr-webhook)
|
||||
* [Travis CI webhook](#travis-ci-webhook)
|
||||
* [XML Payload](#xml-payload)
|
||||
* [Multipart Form Data](#multipart-form-data)
|
||||
* [Pass string arguments to command](#pass-string-arguments-to-command)
|
||||
* [Receive Synology DSM notifications](#receive-synology-notifications)
|
||||
|
||||
## Incoming Github webhook
|
||||
|
||||
This example works on 2.8+ versions of Webhook - if you are on a previous series, change `payload-hmac-sha1` to `payload-hash-sha1`.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Bitbucket webhook
|
||||
|
||||
Bitbucket does not pass any secrets back to the webhook. [Per their documentation](https://support.atlassian.com/organization-administration/docs/ip-addresses-and-domains-for-atlassian-cloud-products/#Outgoing-Connections), in order to verify that the webhook came from Bitbucket you must whitelist a set of IP ranges:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "actor.username"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"or":
|
||||
[
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "13.52.5.96/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "13.236.8.224/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "18.136.214.96/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "18.184.99.224/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "18.234.32.224/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "18.246.31.224/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "52.215.192.224/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "104.192.137.240/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "104.192.138.240/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "104.192.140.240/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "104.192.142.240/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "104.192.143.240/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "185.166.143.240/28" } },
|
||||
{ "match": { "type": "ip-whitelist", "ip-range": "185.166.142.240/28" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Gitlab Webhook
|
||||
Gitlab provides webhooks for many kinds of events.
|
||||
Refer to this URL for example request body content: [gitlab-ce/integrations/webhooks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md)
|
||||
Values in the request body can be accessed in the command or to the match rule by referencing 'payload' as the source:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "user_name"
|
||||
}
|
||||
],
|
||||
"response-message": "Executing redeploy script",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "<YOUR-GENERATED-TOKEN>",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Gitlab-Token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Gogs webhook
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha256",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Gogs-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
## Incoming Gitea webhook
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "secret"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Slack slash command
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "Executing redeploy script",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "<YOUR-GENERATED-TOKEN>",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## A simple webhook with a secret key in GET query
|
||||
|
||||
__Not recommended in production due to low security__
|
||||
|
||||
`example.com:9000/hooks/simple-one` - won't work
|
||||
`example.com:9000/hooks/simple-one?token=42` - will work
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "simple-one",
|
||||
"execute-command": "/path/to/command.sh",
|
||||
"response-message": "Executing simple webhook...",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "42",
|
||||
"parameter":
|
||||
{
|
||||
"source": "url",
|
||||
"name": "token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## JIRA Webhooks
|
||||
[Guide by @perfecto25](https://sites.google.com/site/mrxpalmeiras/more/jira-webhooks)
|
||||
|
||||
## Pass File-to-command sample
|
||||
|
||||
### Webhook configuration
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "test-file-webhook",
|
||||
"execute-command": "/bin/ls",
|
||||
"command-working-directory": "/tmp",
|
||||
"pass-file-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "binary",
|
||||
"envname": "ENV_VARIABLE", // to use $ENV_VARIABLE in execute-command
|
||||
// if not defined, $HOOK_BINARY will be provided
|
||||
"base64decode": true, // defaults to false
|
||||
}
|
||||
],
|
||||
"include-command-output-in-response": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Sample client usage
|
||||
|
||||
Store the following file as `testRequest.json`.
|
||||
|
||||
```json
|
||||
{"binary":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2lpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wUmlnaHRzPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvcmlnaHRzLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcFJpZ2h0czpNYXJrZWQ9IkZhbHNlIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjEzMTA4RDI0QzMxQjExRTBCMzYzRjY1QUQ1Njc4QzFBIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjEzMTA4RDIzQzMxQjExRTBCMzYzRjY1QUQ1Njc4QzFBIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzMgV2luZG93cyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ1dWlkOkFDMUYyRTgzMzI0QURGMTFBQUI4QzUzOTBEODVCNUIzIiBzdFJlZjpkb2N1bWVudElEPSJ1dWlkOkM5RDM0OTY2NEEzQ0REMTFCMDhBQkJCQ0ZGMTcyMTU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+IBFgEwAAAmJJREFUeNqkk89rE1EQx2d/NNq0xcYYayPYJDWC9ODBsKIgAREjBmvEg2cvHnr05KHQ9iB49SL+/BMEfxBQKHgwCEbTNNIYaqgaoanFJi+rcXezye4689jYkIMIDnx47837zrx583YFx3Hgf0xA6/dJyAkkgUy4vgryAnmNWH9L4EVmotFoKplMHgoGg6PkrFarjXQ6/bFcLj/G5W1E+3NaX4KZeDx+dX5+7kg4HBlmrC6JoiDFYrGhROLM/mp1Y6JSqdCd3/SW0GUqEAjkl5ZyHTSHKBQKnO6a9khD2m5cr91IJBJ1VVWdiM/n6LruNJtNDs3JR3ukIW03SHTHi8iVsbG9I51OG1bW16HVasHQZopDc/JZVgdIQ1o3BmTkEnJXURS/KIpgGAYPkCQJPi0u8uzDKQN0XQPbtgE1MmrHs9nsfSqAEjxCNtHxZHLy4G4smUQgyzL4LzOegDGGp1ucVqsNqKVrpJCM7F4hg6iaZvhqtZrg8XjA4xnAU3XeKLqWaRImoIZeQXVjQO5pYp4xNVirsR1erxer2O4yfa227WCwhtWoJmn7m0h270NxmemFW4706zMm8GCgxBGEASCfhnukIW03iFdQnOPz0LNKp3362JqQzSw4u2LXBe+Bs3xD+/oc1NxN55RiC9fOme0LEQiRf2rBzaKEeJJ37ZWTVunBeGN2WmQjg/DeLTVP89nzAive2dMwlo9bpFVC2xWMZr+A720FVn88fAUb3wDMOjyN7YNc6TvUSHQ4AH6TOUdLL7em68UtWPsJqxgTpgeiLu1EBt1R+Me/mF7CQPTfAgwAGxY2vOTrR3oAAAAASUVORK5CYII="}
|
||||
```
|
||||
|
||||
use then the curl tool to execute a request to the webhook.
|
||||
|
||||
```sh
|
||||
#!/bin/bash
|
||||
curl -H "Content-Type:application/json" -X POST -d @testRequest.json \
|
||||
http://localhost:9000/hooks/test-file-webhook
|
||||
```
|
||||
|
||||
or in a single line, using https://github.com/jpmens/jo to generate the JSON code
|
||||
```console
|
||||
jo binary=%filename.zip | curl -H "Content-Type:application/json" -X POST -d @- \
|
||||
http://localhost:9000/hooks/test-file-webhook
|
||||
```
|
||||
|
||||
|
||||
## Incoming Scalr Webhook
|
||||
[Guide by @hassanbabaie]
|
||||
Scalr makes webhook calls based on an event to a configured webhook endpoint (for example Host Down, Host Up). Webhook endpoints are URLs where Scalr will deliver Webhook notifications.
|
||||
Scalr assigns a unique signing key for every configured webhook endpoint.
|
||||
Refer to this URL for information on how to setup the webhook call on the Scalr side: [Scalr Wiki Webhooks](https://scalr-wiki.atlassian.net/wiki/spaces/docs/pages/6193173/Webhooks)
|
||||
In order to leverage the Signing Key for additional authentication/security you must configure the trigger rule with a match type of "scalr-signature".
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "scalr-signature",
|
||||
"secret": "Scalr-provided signing key"
|
||||
}
|
||||
},
|
||||
"pass-environment-to-command":
|
||||
[
|
||||
{
|
||||
"envname": "EVENT_NAME",
|
||||
"source": "payload",
|
||||
"name": "eventName"
|
||||
},
|
||||
{
|
||||
"envname": "SERVER_HOSTNAME",
|
||||
"source": "payload",
|
||||
"name": "data.SCALR_SERVER_HOSTNAME"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
## Travis CI webhook
|
||||
Travis sends webhooks as `payload=<JSON_STRING>`, so the payload needs to be parsed as JSON. Here is an example to run on successful builds of the master branch.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "deploy",
|
||||
"execute-command": "/root/my-server/deployment.sh",
|
||||
"command-working-directory": "/root/my-server",
|
||||
"parse-parameters-as-json": [
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "payload"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "passed",
|
||||
"parameter": {
|
||||
"name": "payload.state",
|
||||
"source": "payload"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "master",
|
||||
"parameter": {
|
||||
"name": "payload.branch",
|
||||
"source": "payload"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## JSON Array Payload
|
||||
|
||||
If the JSON payload is an array instead of an object, `webhook` will process the payload and place it into a "root" object.
|
||||
Therefore, references to payload values must begin with `root.`.
|
||||
|
||||
For example, given the following payload (taken from the Sendgrid Event Webhook documentation):
|
||||
```json
|
||||
[
|
||||
{
|
||||
"email": "example@test.com",
|
||||
"timestamp": 1513299569,
|
||||
"smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
|
||||
"event": "processed",
|
||||
"category": "cat facts",
|
||||
"sg_event_id": "sg_event_id",
|
||||
"sg_message_id": "sg_message_id"
|
||||
},
|
||||
{
|
||||
"email": "example@test.com",
|
||||
"timestamp": 1513299569,
|
||||
"smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
|
||||
"event": "deferred",
|
||||
"category": "cat facts",
|
||||
"sg_event_id": "sg_event_id",
|
||||
"sg_message_id": "sg_message_id",
|
||||
"response": "400 try again later",
|
||||
"attempt": "5"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
A reference to the second item in the array would look like this:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "sendgrid",
|
||||
"execute-command": "/root/my-server/deployment.sh",
|
||||
"command-working-directory": "/root/my-server",
|
||||
"trigger-rule": {
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "root.1.event"
|
||||
},
|
||||
"value": "deferred"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## XML Payload
|
||||
|
||||
Given the following payload:
|
||||
|
||||
```xml
|
||||
<app>
|
||||
<users>
|
||||
<user id="1" name="Jeff" />
|
||||
<user id="2" name="Sally" />
|
||||
</users>
|
||||
<messages>
|
||||
<message id="1" from_user="1" to_user="2">Hello!!</message>
|
||||
</messages>
|
||||
</app>
|
||||
```
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "deploy",
|
||||
"execute-command": "/root/my-server/deployment.sh",
|
||||
"command-working-directory": "/root/my-server",
|
||||
"trigger-rule": {
|
||||
"and": [
|
||||
{
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "app.users.user.0.-name"
|
||||
},
|
||||
"value": "Jeff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "app.messages.message.#text"
|
||||
},
|
||||
"value": "Hello!!"
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Multipart Form Data
|
||||
|
||||
Example of a [Plex Media Server webhook](https://support.plex.tv/articles/115002267687-webhooks/).
|
||||
The Plex Media Server will send two parts: payload and thumb.
|
||||
We only care about the payload part.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "plex",
|
||||
"execute-command": "play-command.sh",
|
||||
"parse-parameters-as-json": [
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "payload"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "payload.event"
|
||||
},
|
||||
"value": "media.play"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Each part of a multipart form data body will have a `Content-Disposition` header.
|
||||
Some example headers:
|
||||
|
||||
```
|
||||
Content-Disposition: form-data; name="payload"
|
||||
Content-Disposition: form-data; name="thumb"; filename="thumb.jpg"
|
||||
```
|
||||
|
||||
We key off of the `name` attribute in the `Content-Disposition` value.
|
||||
|
||||
## Pass string arguments to command
|
||||
|
||||
To pass simple string arguments to a command, use the `string` parameter source.
|
||||
The following example will pass two static string parameters ("-e 123123") to the
|
||||
`execute-command` before appending the `pusher.email` value from the payload:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "string",
|
||||
"name": "-e"
|
||||
},
|
||||
{
|
||||
"source": "string",
|
||||
"name": "123123"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Receive Synology DSM notifications
|
||||
|
||||
It's possible to securely receive Synology push notifications via webhooks.
|
||||
Webhooks feature introduced in DSM 7.x seems to be incomplete & broken, but you can use Synology SMS notification service to push webhooks. To configure SMS notifications on DSM follow instructions found here: https://github.com/ryancurrah/synology-notifications this will allow you to set up everything needed for webhook to accept any and all notifications sent by Synology. During setup an 'api_key' is specified - you can generate your own 32-char string and use it as an authentication mechanism to secure your webhook. Additionally, you can specify what notifications to receive via this method by going and selecting the "SMS" checkboxes under topics of interes in DSM: Control Panel -> Notification -> Rules
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "synology",
|
||||
"execute-command": "do-something.sh",
|
||||
"command-working-directory": "/opt/webhook-linux-amd64/synology",
|
||||
"response-message": "Request accepted",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "message"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "PUT_YOUR_API_KEY_HERE",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "api_key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
288
docs/Hook-Rules.md
Normal file
288
docs/Hook-Rules.md
Normal file
|
@ -0,0 +1,288 @@
|
|||
# Hook rules
|
||||
|
||||
### Table of Contents
|
||||
|
||||
* [And](#and)
|
||||
* [Or](#or)
|
||||
* [Not](#not)
|
||||
* [Multi-level](#multi-level)
|
||||
* [Match](#match)
|
||||
* [Match value](#match-value)
|
||||
* [Match regex](#match-regex)
|
||||
* [Match payload-hmac-sha1](#match-payload-hmac-sha1)
|
||||
* [Match payload-hmac-sha256](#match-payload-hmac-sha256)
|
||||
* [Match payload-hmac-sha512](#match-payload-hmac-sha512)
|
||||
* [Match Whitelisted IP range](#match-whitelisted-ip-range)
|
||||
* [Match scalr-signature](#match-scalr-signature)
|
||||
|
||||
## And
|
||||
*And rule* will evaluate to _true_, if and only if all of the sub rules evaluate to _true_.
|
||||
```json
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "regex",
|
||||
"regex": ".*",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "repository.owner.name"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
## Or
|
||||
*Or rule* will evaluate to _true_, if any of the sub rules evaluate to _true_.
|
||||
```json
|
||||
{
|
||||
"or":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/development",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
## Not
|
||||
*Not rule* will evaluate to _true_, if and only if the sub rule evaluates to _false_.
|
||||
```json
|
||||
{
|
||||
"not":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/development",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
## Multi-level
|
||||
```json
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"match": {
|
||||
"parameter": {
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
},
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret"
|
||||
}
|
||||
},
|
||||
{
|
||||
"or": [
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
},
|
||||
"type": "value",
|
||||
"value": "refs/heads/master"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-GitHub-Event"
|
||||
},
|
||||
"type": "value",
|
||||
"value": "ping"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
## Match
|
||||
*Match rule* will evaluate to _true_, if and only if the referenced value in the `parameter` field satisfies the `type`-specific rule.
|
||||
|
||||
*Please note:* Due to technical reasons, _number_ and _boolean_ values in the _match rule_ must be wrapped around with a pair of quotes.
|
||||
|
||||
### Match value
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/development",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Match regex
|
||||
For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "regex",
|
||||
"regex": ".*",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Match payload-hmac-sha1
|
||||
Validate the HMAC of the payload using the SHA1 hash and the given *secret*.
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that if multiple signatures were passed via a comma separated string, each
|
||||
will be tried unless a match is found. For example:
|
||||
|
||||
```
|
||||
X-Hub-Signature: sha1=the-first-signature,sha1=the-second-signature
|
||||
```
|
||||
|
||||
### Match payload-hmac-sha256
|
||||
Validate the HMAC of the payload using the SHA256 hash and the given *secret*.
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha256",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that if multiple signatures were passed via a comma separated string, each
|
||||
will be tried unless a match is found. For example:
|
||||
|
||||
```
|
||||
X-Hub-Signature: sha256=the-first-signature,sha256=the-second-signature
|
||||
```
|
||||
|
||||
### Match payload-hmac-sha512
|
||||
Validate the HMAC of the payload using the SHA512 hash and the given *secret*.
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha512",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that if multiple signatures were passed via a comma separated string, each
|
||||
will be tried unless a match is found. For example:
|
||||
|
||||
```
|
||||
X-Hub-Signature: sha512=the-first-signature,sha512=the-second-signature
|
||||
```
|
||||
|
||||
### Match Whitelisted IP range
|
||||
|
||||
The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_blocks). To match a single IP address only, use `/32`.
|
||||
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "ip-whitelist",
|
||||
"ip-range": "192.168.0.1/24"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note this does not work if webhook is running behind a reverse proxy, as the "client IP" will either not be available at all (if webhook is using a Unix socket or named pipe) or it will be the address of the _proxy_, not of the real client. You will probably need to enforce client IP restrictions in the reverse proxy itself, before forwarding the requests to webhook.
|
||||
|
||||
### Match scalr-signature
|
||||
|
||||
The trigger rule checks the scalr signature and also checks that the request was signed less than 5 minutes before it was received.
|
||||
A unique signing key is generated for each webhook endpoint URL you register in Scalr.
|
||||
Given the time check make sure that NTP is enabled on both your Scalr and webhook server to prevent any issues
|
||||
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "scalr-signature",
|
||||
"secret": "Scalr-provided signing key"
|
||||
}
|
||||
}
|
||||
```
|
134
docs/Referencing-Request-Values.md
Normal file
134
docs/Referencing-Request-Values.md
Normal file
|
@ -0,0 +1,134 @@
|
|||
# Referencing request values
|
||||
There are four types of request values:
|
||||
|
||||
1. HTTP Request Header values
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "header",
|
||||
"name": "Header-Name"
|
||||
}
|
||||
```
|
||||
|
||||
2. HTTP Query parameters
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "url",
|
||||
"name": "parameter-name"
|
||||
}
|
||||
```
|
||||
|
||||
3. HTTP Request parameters
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "request",
|
||||
"name": "method"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "request",
|
||||
"name": "remote-addr"
|
||||
}
|
||||
```
|
||||
|
||||
4. Payload (JSON or form-value encoded)
|
||||
```json
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "parameter-name"
|
||||
}
|
||||
```
|
||||
|
||||
*Note:* For JSON encoded payload, you can reference nested values using the dot-notation.
|
||||
For example, if you have following JSON payload
|
||||
|
||||
```json
|
||||
{
|
||||
"commits": [
|
||||
{
|
||||
"commit": {
|
||||
"id": 1
|
||||
}
|
||||
}, {
|
||||
"commit": {
|
||||
"id": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can reference the first commit id as
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "commits.0.commit.id"
|
||||
}
|
||||
```
|
||||
|
||||
If the payload contains a key with the specified name "commits.0.commit.id", then the value of that key has priority over the dot-notation referencing.
|
||||
|
||||
4. XML Payload
|
||||
|
||||
Referencing XML payload parameters is much like the JSON examples above, but XML is more complex.
|
||||
Element attributes are prefixed by a hyphen (`-`).
|
||||
Element values are prefixed by a pound (`#`).
|
||||
|
||||
Take the following XML payload:
|
||||
|
||||
```xml
|
||||
<app>
|
||||
<users>
|
||||
<user id="1" name="Jeff" />
|
||||
<user id="2" name="Sally" />
|
||||
</users>
|
||||
<messages>
|
||||
<message id="1" from_user="1" to_user="2">Hello!!</message>
|
||||
</messages>
|
||||
</app>
|
||||
```
|
||||
|
||||
To access a given `user` element, you must treat them as an array.
|
||||
So `app.users.user.0.name` yields `Jeff`.
|
||||
|
||||
Since there's only one `message` tag, it's not treated as an array.
|
||||
So `app.messages.message.id` yields `1`.
|
||||
|
||||
To access the text within the `message` tag, you would use: `app.messages.message.#text`.
|
||||
|
||||
If you are referencing values for environment, you can use `envname` property to set the name of the environment variable like so
|
||||
```json
|
||||
{
|
||||
"source": "url",
|
||||
"name": "q",
|
||||
"envname": "QUERY"
|
||||
}
|
||||
```
|
||||
to get the QUERY environment variable set to the `q` parameter passed in the query string.
|
||||
|
||||
# Special cases
|
||||
If you want to pass the entire payload as JSON string to your command you can use
|
||||
```json
|
||||
{
|
||||
"source": "entire-payload"
|
||||
}
|
||||
```
|
||||
|
||||
for headers you can use
|
||||
```json
|
||||
{
|
||||
"source": "entire-headers"
|
||||
}
|
||||
```
|
||||
|
||||
and for query variables you can use
|
||||
```json
|
||||
{
|
||||
"source": "entire-query"
|
||||
}
|
||||
```
|
61
docs/Systemd-Activation.md
Normal file
61
docs/Systemd-Activation.md
Normal file
|
@ -0,0 +1,61 @@
|
|||
# Using systemd socket activation
|
||||
|
||||
_New in v2.8.2_
|
||||
|
||||
On platforms that use [systemd](https://systemd.io), [webhook][w]
|
||||
supports the _socket activation_ mechanism. In this mode, systemd itself is responsible for managing the listening socket, and it launches [webhook][w] the first time it receives a request on the socket. This has a number of advantages over the standard mode:
|
||||
|
||||
- [webhook][w] can run as a normal user while still being able to use a port number like 80 or 443 that would normally require root privilege
|
||||
- if the [webhook][w] process dies and is restarted, pending connections are not dropped - they just keep waiting until the restarted [webhook][w] is ready
|
||||
|
||||
No special configuration is necessary to tell [webhook][w] that socket activation is being used - socket activation sets specific environment variables when launching the activated service, if [webhook][w] detects these variables it will ignore the `-port` and `-socket` options and simply use the systemd-provided socket instead of opening its own.
|
||||
|
||||
## Configuration
|
||||
To run [webhook][w] with socket activation you need to create _two_ separate unit files in your systemd configuration directory (typically `/etc/systemd/system`), one for the socket and one for the service. They must have matching names; in this example we use `webhook.socket` and `webhook.service`. At their simplest, these files should look like:
|
||||
|
||||
**webhook.socket**
|
||||
```
|
||||
[Unit]
|
||||
Description=Webhook server socket
|
||||
|
||||
[Socket]
|
||||
# Listen on all network interfaces, port 9000
|
||||
ListenStream=9000
|
||||
|
||||
# Alternatives:
|
||||
|
||||
## Listen on one specific interface only
|
||||
# ListenStream=10.0.0.1:9000
|
||||
# FreeBind=true
|
||||
|
||||
## Listen on a Unix domain socket
|
||||
# ListenStream=/tmp/webhook.sock
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
**webhook.service**
|
||||
```
|
||||
[Unit]
|
||||
Description=Webhook server
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=webhook -nopanic -hooks /etc/webhook/hooks.yml
|
||||
|
||||
# Which user should the webhooks run as?
|
||||
User=nobody
|
||||
Group=nogroup
|
||||
```
|
||||
|
||||
You should enable and start the _socket_, but it is not necessary to enable the _service_ - this will be started automatically when the socket receives its first request.
|
||||
|
||||
```sh
|
||||
sudo systemctl enable webhook.socket
|
||||
sudo systemctl start webhook.socket
|
||||
```
|
||||
|
||||
Systemd unit files support many other options, see the [systemd.socket](https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html) and [systemd.service](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html) manual pages for full details.
|
||||
|
||||
[w]: https://github.com/adnanh/webhook
|
77
docs/Templates.md
Normal file
77
docs/Templates.md
Normal file
|
@ -0,0 +1,77 @@
|
|||
# Templates in Webhook
|
||||
|
||||
[`webhook`][w] can parse a hooks configuration file as a Go template when given the `-template` [CLI parameter](Webhook-Parameters.md).
|
||||
|
||||
In additional to the [built-in Go template functions and features][tt], `webhook` provides a `getenv` template function for inserting environment variables into a templated configuration file.
|
||||
|
||||
## Example Usage
|
||||
|
||||
In the example JSON template file below (YAML is also supported), the `payload-hmac-sha1` matching rule looks up the HMAC secret from the environment using the `getenv` template function.
|
||||
Additionally, the result is piped through the built-in Go template function `js` to ensure that the result is a well-formed Javascript/JSON string.
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "I got the payload!",
|
||||
"response-headers":
|
||||
[
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
[w]: https://github.com/adnanh/webhook
|
||||
[tt]: https://golang.org/pkg/text/template/
|
66
docs/Webhook-Parameters.md
Normal file
66
docs/Webhook-Parameters.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
# Webhook parameters
|
||||
```
|
||||
Usage of webhook:
|
||||
-cert string
|
||||
path to the HTTPS certificate pem file (default "cert.pem")
|
||||
-cipher-suites string
|
||||
comma-separated list of supported TLS cipher suites
|
||||
-debug
|
||||
show debug output
|
||||
-header value
|
||||
response header to return, specified in format name=value, use multiple times to set multiple headers
|
||||
-hooks value
|
||||
path to the json file containing defined hooks the webhook should serve, use multiple times to load from different files
|
||||
-hotreload
|
||||
watch hooks file for changes and reload them automatically
|
||||
-http-methods string
|
||||
set default allowed HTTP methods (ie. "POST"); separate methods with comma
|
||||
-ip string
|
||||
ip the webhook should serve hooks on (default "0.0.0.0")
|
||||
-key string
|
||||
path to the HTTPS certificate private key pem file (default "key.pem")
|
||||
-list-cipher-suites
|
||||
list available TLS cipher suites
|
||||
-logfile string
|
||||
send log output to a file; implicitly enables verbose logging
|
||||
-max-multipart-mem int
|
||||
maximum memory in bytes for parsing multipart form data before disk caching (default 1048576)
|
||||
-nopanic
|
||||
do not panic if hooks cannot be loaded when webhook is not running in verbose mode
|
||||
-pidfile string
|
||||
create PID file at the given path
|
||||
-port int
|
||||
port the webhook should serve hooks on (default 9000)
|
||||
-secure
|
||||
use HTTPS instead of HTTP
|
||||
-setgid int
|
||||
set group ID after opening listening port; must be used with setuid
|
||||
-setuid int
|
||||
set user ID after opening listening port; must be used with setgid
|
||||
-socket string
|
||||
path to a Unix socket (e.g. /tmp/webhook.sock) or Windows named pipe (e.g. \\.\pipe\webhook) to use instead of listening on an ip and port; if specified, the ip and port options are ignored
|
||||
-template
|
||||
parse hooks file as a Go template
|
||||
-tls-min-version string
|
||||
minimum TLS version (1.0, 1.1, 1.2, 1.3) (default "1.2")
|
||||
-urlprefix string
|
||||
url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id) (default "hooks")
|
||||
-verbose
|
||||
show verbose output
|
||||
-version
|
||||
display webhook version and quit
|
||||
-x-request-id
|
||||
use X-Request-Id header, if present, as request ID
|
||||
-x-request-id-limit int
|
||||
truncate X-Request-Id header to limit; default no limit
|
||||
```
|
||||
|
||||
Use any of the above specified flags to override their default behavior.
|
||||
|
||||
# Live reloading hooks
|
||||
If you are running an OS that supports the HUP or USR1 signal, you can use it to trigger hooks reload from hooks file, without restarting the webhook instance.
|
||||
```bash
|
||||
kill -USR1 webhookpid
|
||||
|
||||
kill -HUP webhookpid
|
||||
```
|
BIN
docs/logo/logo-128x128.png
Normal file
BIN
docs/logo/logo-128x128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 912 B |
BIN
docs/logo/logo-256x256.png
Normal file
BIN
docs/logo/logo-256x256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
12
droppriv_nope.go
Normal file
12
droppriv_nope.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func dropPrivileges(uid, gid int) error {
|
||||
return errors.New("setuid and setgid not supported on " + runtime.GOOS)
|
||||
}
|
26
droppriv_unix.go
Normal file
26
droppriv_unix.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
// +build linux !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func dropPrivileges(uid, gid int) error {
|
||||
err := syscall.Setgroups([]int{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = syscall.Setgid(gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = syscall.Setuid(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
25
go.mod
Normal file
25
go.mod
Normal file
|
@ -0,0 +1,25 @@
|
|||
module github.com/adnanh/webhook
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.0
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/clbanning/mxj/v2 v2.7.0
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/gofrs/uuid/v5 v5.0.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
golang.org/x/sys v0.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
33
go.sum
Normal file
33
go.sum
Normal file
|
@ -0,0 +1,33 @@
|
|||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
||||
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
550
hook/hook.go
550
hook/hook.go
|
@ -1,550 +0,0 @@
|
|||
package hook
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/textproto"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Constants used to specify the parameter source
|
||||
const (
|
||||
SourceHeader string = "header"
|
||||
SourceQuery string = "url"
|
||||
SourceQueryAlias string = "query"
|
||||
SourcePayload string = "payload"
|
||||
SourceString string = "string"
|
||||
SourceEntirePayload string = "entire-payload"
|
||||
SourceEntireQuery string = "entire-query"
|
||||
SourceEntireHeaders string = "entire-headers"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvNamespace is the prefix used for passing arguments into the command
|
||||
// environment.
|
||||
EnvNamespace string = "HOOK_"
|
||||
)
|
||||
|
||||
// SignatureError describes an invalid payload signature passed to Hook.
|
||||
type SignatureError struct {
|
||||
Signature string
|
||||
}
|
||||
|
||||
func (e *SignatureError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("invalid payload signature %s", e.Signature)
|
||||
}
|
||||
|
||||
// ArgumentError describes an invalid argument passed to Hook.
|
||||
type ArgumentError struct {
|
||||
Argument Argument
|
||||
}
|
||||
|
||||
func (e *ArgumentError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("couldn't retrieve argument for %+v", e.Argument)
|
||||
}
|
||||
|
||||
// SourceError describes an invalid source passed to Hook.
|
||||
type SourceError struct {
|
||||
Argument Argument
|
||||
}
|
||||
|
||||
func (e *SourceError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("invalid source for argument %+v", e.Argument)
|
||||
}
|
||||
|
||||
// ParseError describes an error parsing user input.
|
||||
type ParseError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ParseError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
||||
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
|
||||
if strings.HasPrefix(signature, "sha1=") {
|
||||
signature = signature[5:]
|
||||
}
|
||||
|
||||
mac := hmac.New(sha1.New, []byte(secret))
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, &SignatureError{signature}
|
||||
}
|
||||
return expectedMAC, err
|
||||
}
|
||||
|
||||
// ReplaceParameter replaces parameter value with the passed value in the passed map
|
||||
// (please note you should pass pointer to the map, because we're modifying it)
|
||||
// based on the passed string
|
||||
func ReplaceParameter(s string, params interface{}, value interface{}) bool {
|
||||
if params == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ReplaceParameter(p[1], params.([]interface{})[index], value)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
return ReplaceParameter(p[1], pValue, value)
|
||||
}
|
||||
} else {
|
||||
if _, ok := (*params.(*map[string]interface{}))[p[0]]; ok {
|
||||
(*params.(*map[string]interface{}))[p[0]] = value
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetParameter extracts interface{} value based on the passed string
|
||||
func GetParameter(s string, params interface{}) (interface{}, bool) {
|
||||
if params == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return GetParameter(p[1], params.([]interface{})[index])
|
||||
}
|
||||
|
||||
index, err := strconv.ParseUint(s, 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return params.([]interface{})[index], true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Map {
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
return GetParameter(p[1], pValue)
|
||||
}
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
} else {
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
return pValue, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ExtractParameterAsString extracts value from interface{} as string based on the passed string
|
||||
func ExtractParameterAsString(s string, params interface{}) (string, bool) {
|
||||
if pValue, ok := GetParameter(s, params); ok {
|
||||
return fmt.Sprintf("%v", pValue), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Argument type specifies the parameter key name and the source it should
|
||||
// be extracted from
|
||||
type Argument struct {
|
||||
Source string `json:"source,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
EnvName string `json:"envname,omitempty"`
|
||||
}
|
||||
|
||||
// Get Argument method returns the value for the Argument's key name
|
||||
// based on the Argument's source
|
||||
func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string, bool) {
|
||||
var source *map[string]interface{}
|
||||
key := ha.Name
|
||||
|
||||
switch ha.Source {
|
||||
case SourceHeader:
|
||||
source = headers
|
||||
key = textproto.CanonicalMIMEHeaderKey(ha.Name)
|
||||
case SourceQuery, SourceQueryAlias:
|
||||
source = query
|
||||
case SourcePayload:
|
||||
source = payload
|
||||
case SourceString:
|
||||
return ha.Name, true
|
||||
case SourceEntirePayload:
|
||||
r, err := json.Marshal(payload)
|
||||
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
case SourceEntireHeaders:
|
||||
r, err := json.Marshal(headers)
|
||||
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
case SourceEntireQuery:
|
||||
r, err := json.Marshal(query)
|
||||
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
}
|
||||
|
||||
if source != nil {
|
||||
return ExtractParameterAsString(key, *source)
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Header is a structure containing header name and it's value
|
||||
type Header struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ResponseHeaders is a slice of Header objects
|
||||
type ResponseHeaders []Header
|
||||
|
||||
func (h *ResponseHeaders) String() string {
|
||||
// a 'hack' to display name=value in flag usage listing
|
||||
if len(*h) == 0 {
|
||||
return "name=value"
|
||||
}
|
||||
|
||||
result := make([]string, len(*h))
|
||||
|
||||
for idx, responseHeader := range *h {
|
||||
result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value)
|
||||
}
|
||||
|
||||
return fmt.Sprint(strings.Join(result, ", "))
|
||||
}
|
||||
|
||||
// Set method appends new Header object from header=value notation
|
||||
func (h *ResponseHeaders) Set(value string) error {
|
||||
splitResult := strings.SplitN(value, "=", 2)
|
||||
|
||||
if len(splitResult) != 2 {
|
||||
return errors.New("header flag must be in name=value format")
|
||||
}
|
||||
|
||||
*h = append(*h, Header{Name: splitResult[0], Value: splitResult[1]})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hook type is a structure containing details for a single hook
|
||||
type Hook struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
ExecuteCommand string `json:"execute-command,omitempty"`
|
||||
CommandWorkingDirectory string `json:"command-working-directory,omitempty"`
|
||||
ResponseMessage string `json:"response-message,omitempty"`
|
||||
ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
|
||||
CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
|
||||
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
|
||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
|
||||
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
|
||||
TriggerRule *Rules `json:"trigger-rule,omitempty"`
|
||||
TriggerRuleMismatchHttpResponseCode int `json:"trigger-rule-mismatch-http-response-code,omitempty"`
|
||||
}
|
||||
|
||||
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||
// string with the newly created object
|
||||
func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) []error {
|
||||
var errors = make([]error, 0)
|
||||
|
||||
for i := range h.JSONStringParameters {
|
||||
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
|
||||
var newArg map[string]interface{}
|
||||
|
||||
decoder := json.NewDecoder(strings.NewReader(string(arg)))
|
||||
decoder.UseNumber()
|
||||
|
||||
err := decoder.Decode(&newArg)
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, &ParseError{err})
|
||||
continue
|
||||
}
|
||||
|
||||
var source *map[string]interface{}
|
||||
|
||||
switch h.JSONStringParameters[i].Source {
|
||||
case SourceHeader:
|
||||
source = headers
|
||||
case SourcePayload:
|
||||
source = payload
|
||||
case SourceQuery, SourceQueryAlias:
|
||||
source = query
|
||||
}
|
||||
|
||||
if source != nil {
|
||||
key := h.JSONStringParameters[i].Name
|
||||
|
||||
if h.JSONStringParameters[i].Source == SourceHeader {
|
||||
key = textproto.CanonicalMIMEHeaderKey(h.JSONStringParameters[i].Name)
|
||||
}
|
||||
|
||||
ReplaceParameter(key, source, newArg)
|
||||
} else {
|
||||
errors = append(errors, &SourceError{h.JSONStringParameters[i]})
|
||||
}
|
||||
} else {
|
||||
errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return errors
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractCommandArguments creates a list of arguments, based on the
|
||||
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
||||
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||
var args = make([]string, 0)
|
||||
var errors = make([]error, 0)
|
||||
|
||||
args = append(args, h.ExecuteCommand)
|
||||
|
||||
for i := range h.PassArgumentsToCommand {
|
||||
if arg, ok := h.PassArgumentsToCommand[i].Get(headers, query, payload); ok {
|
||||
args = append(args, arg)
|
||||
} else {
|
||||
args = append(args, "")
|
||||
errors = append(errors, &ArgumentError{h.PassArgumentsToCommand[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return args, errors
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// ExtractCommandArgumentsForEnv creates a list of arguments in key=value
|
||||
// format, based on the PassEnvironmentToCommand property that is ready to be used
|
||||
// with exec.Command().
|
||||
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||
var args = make([]string, 0)
|
||||
var errors = make([]error, 0)
|
||||
for i := range h.PassEnvironmentToCommand {
|
||||
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
||||
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
||||
// first try to use the EnvName if specified
|
||||
args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
|
||||
} else {
|
||||
// then fallback on the name
|
||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||
}
|
||||
} else {
|
||||
errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return args, errors
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// Hooks is an array of Hook objects
|
||||
type Hooks []Hook
|
||||
|
||||
// LoadFromFile attempts to load hooks from specified JSON file
|
||||
func (h *Hooks) LoadFromFile(path string) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parse hook file for hooks
|
||||
file, e := ioutil.ReadFile(path)
|
||||
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
e = json.Unmarshal(file, h)
|
||||
return e
|
||||
}
|
||||
|
||||
// Match iterates through Hooks and returns first one that matches the given ID,
|
||||
// if no hook matches the given ID, nil is returned
|
||||
func (h *Hooks) Match(id string) *Hook {
|
||||
for i := range *h {
|
||||
if (*h)[i].ID == id {
|
||||
return &(*h)[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rules is a structure that contains one of the valid rule types
|
||||
type Rules struct {
|
||||
And *AndRule `json:"and,omitempty"`
|
||||
Or *OrRule `json:"or,omitempty"`
|
||||
Not *NotRule `json:"not,omitempty"`
|
||||
Match *MatchRule `json:"match,omitempty"`
|
||||
}
|
||||
|
||||
// Evaluate finds the first rule property that is not nil and returns the value
|
||||
// it evaluates to
|
||||
func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) {
|
||||
switch {
|
||||
case r.And != nil:
|
||||
return r.And.Evaluate(headers, query, payload, body)
|
||||
case r.Or != nil:
|
||||
return r.Or.Evaluate(headers, query, payload, body)
|
||||
case r.Not != nil:
|
||||
return r.Not.Evaluate(headers, query, payload, body)
|
||||
case r.Match != nil:
|
||||
return r.Match.Evaluate(headers, query, payload, body)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// AndRule will evaluate to true if and only if all of the ChildRules evaluate to true
|
||||
type AndRule []Rules
|
||||
|
||||
// Evaluate AndRule will return true if and only if all of ChildRules evaluate to true
|
||||
func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) {
|
||||
res := true
|
||||
|
||||
for _, v := range r {
|
||||
rv, err := v.Evaluate(headers, query, payload, body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
res = res && rv
|
||||
if res == false {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// OrRule will evaluate to true if any of the ChildRules evaluate to true
|
||||
type OrRule []Rules
|
||||
|
||||
// Evaluate OrRule will return true if any of ChildRules evaluate to true
|
||||
func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) {
|
||||
res := false
|
||||
|
||||
for _, v := range r {
|
||||
rv, err := v.Evaluate(headers, query, payload, body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
res = res || rv
|
||||
if res == true {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// NotRule will evaluate to true if any and only if the ChildRule evaluates to false
|
||||
type NotRule Rules
|
||||
|
||||
// Evaluate NotRule will return true if and only if ChildRule evaluates to false
|
||||
func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) {
|
||||
rv, err := Rules(r).Evaluate(headers, query, payload, body)
|
||||
return !rv, err
|
||||
}
|
||||
|
||||
// MatchRule will evaluate to true based on the type
|
||||
type MatchRule struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Regex string `json:"regex,omitempty"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Parameter Argument `json:"parameter,omitempty"`
|
||||
}
|
||||
|
||||
// Constants for the MatchRule type
|
||||
const (
|
||||
MatchValue string = "value"
|
||||
MatchRegex string = "regex"
|
||||
MatchHashSHA1 string = "payload-hash-sha1"
|
||||
)
|
||||
|
||||
// Evaluate MatchRule will return based on the type
|
||||
func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) {
|
||||
if arg, ok := r.Parameter.Get(headers, query, payload); ok {
|
||||
switch r.Type {
|
||||
case MatchValue:
|
||||
return arg == r.Value, nil
|
||||
case MatchRegex:
|
||||
return regexp.MatchString(r.Regex, arg)
|
||||
case MatchHashSHA1:
|
||||
_, err := CheckPayloadSignature(*body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
|
@ -1,418 +0,0 @@
|
|||
package hook
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var checkPayloadSignatureTests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignatureTests {
|
||||
mac, err := CheckPayloadSignature(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var extractParameterTests = []struct {
|
||||
s string
|
||||
params interface{}
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"a", map[string]interface{}{"a": "z"}, "z", true},
|
||||
{"a.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "z", true},
|
||||
{"a.b.c", map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}, "z", true},
|
||||
{"a.b.0", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "x", true},
|
||||
{"a.1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "z", true},
|
||||
{"a.1.b.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": map[string]interface{}{"c": "y"}}, map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}}, "z", true},
|
||||
// failures
|
||||
{"check_nil", nil, "", false},
|
||||
{"a.X", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent parameter reference
|
||||
{"a.X.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-integer slice index
|
||||
{"a.-1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // negative slice index
|
||||
{"a.500.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent slice
|
||||
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
|
||||
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
|
||||
{"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index
|
||||
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
|
||||
}
|
||||
|
||||
func TestExtractParameter(t *testing.T) {
|
||||
for _, tt := range extractParameterTests {
|
||||
value, ok := ExtractParameterAsString(tt.s, tt.params)
|
||||
if ok != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.s, tt.value, tt.ok, value, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var argumentGetTests = []struct {
|
||||
source, name string
|
||||
headers, query, payload *map[string]interface{}
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"header", "a", &map[string]interface{}{"A": "z"}, nil, nil, "z", true},
|
||||
{"url", "a", nil, &map[string]interface{}{"a": "z"}, nil, "z", true},
|
||||
{"payload", "a", nil, nil, &map[string]interface{}{"a": "z"}, "z", true},
|
||||
{"string", "a", nil, nil, &map[string]interface{}{"a": "z"}, "a", true},
|
||||
// failures
|
||||
{"header", "a", nil, &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, "", false}, // nil headers
|
||||
{"url", "a", &map[string]interface{}{"A": "z"}, nil, &map[string]interface{}{"a": "z"}, "", false}, // nil query
|
||||
{"payload", "a", &map[string]interface{}{"A": "z"}, &map[string]interface{}{"a": "z"}, nil, "", false}, // nil payload
|
||||
{"foo", "a", &map[string]interface{}{"A": "z"}, nil, nil, "", false}, // invalid source
|
||||
}
|
||||
|
||||
func TestArgumentGet(t *testing.T) {
|
||||
for _, tt := range argumentGetTests {
|
||||
a := Argument{tt.source, tt.name, ""}
|
||||
value, ok := a.Get(tt.headers, tt.query, tt.payload)
|
||||
if ok != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hookParseJSONParametersTests = []struct {
|
||||
params []Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
rheaders, rquery, rpayload *map[string]interface{}
|
||||
ok bool
|
||||
}{
|
||||
{[]Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
||||
{[]Argument{Argument{"url", "a", ""}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
||||
{[]Argument{Argument{"payload", "a", ""}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
||||
{[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"Z": `{}`}, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
|
||||
// failures
|
||||
{[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
|
||||
{[]Argument{Argument{"header", "y", ""}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
||||
{[]Argument{Argument{"string", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
|
||||
}
|
||||
|
||||
func TestHookParseJSONParameters(t *testing.T) {
|
||||
for _, tt := range hookParseJSONParametersTests {
|
||||
h := &Hook{JSONStringParameters: tt.params}
|
||||
err := h.ParseJSONParameters(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) {
|
||||
t.Errorf("failed to parse %v:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.params, *tt.rheaders, tt.ok, *tt.headers, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hookExtractCommandArgumentsTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
{"test", []Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||
// failures
|
||||
{"fail", []Argument{Argument{"payload", "a", ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArguments(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassArgumentsToCommand: tt.args}
|
||||
value, err := h.ExtractCommandArguments(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here we test the extraction of env variables when the user defined a hook
|
||||
// with the "pass-environment-to-command" directive
|
||||
// we test both cases where the name of the data is used as the name of the
|
||||
// env key & the case where the hook definition sets the env var name to a
|
||||
// fixed value using the envname construct like so::
|
||||
// [
|
||||
// {
|
||||
// "id": "push",
|
||||
// "execute-command": "bb2mm",
|
||||
// "command-working-directory": "/tmp",
|
||||
// "pass-environment-to-command":
|
||||
// [
|
||||
// {
|
||||
// "source": "entire-payload",
|
||||
// "envname": "PAYLOAD"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
// successes
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", ""}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"HOOK_a=z"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "MYKEY"}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"MYKEY=z"},
|
||||
true,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"fail",
|
||||
[]Argument{Argument{"payload", "a", ""}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsForEnvTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassEnvironmentToCommand: tt.args}
|
||||
value, err := h.ExtractCommandArgumentsForEnv(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args for env {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksLoadFromFileTests = []struct {
|
||||
path string
|
||||
ok bool
|
||||
}{
|
||||
{"../hooks.json.example", true},
|
||||
{"", true},
|
||||
// failures
|
||||
{"missing.json", false},
|
||||
}
|
||||
|
||||
func TestHooksLoadFromFile(t *testing.T) {
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksMatchTests = []struct {
|
||||
id string
|
||||
hooks Hooks
|
||||
value *Hook
|
||||
}{
|
||||
{"a", Hooks{Hook{ID: "a"}}, &Hook{ID: "a"}},
|
||||
{"X", Hooks{Hook{ID: "a"}}, new(Hook)},
|
||||
}
|
||||
|
||||
func TestHooksMatch(t *testing.T) {
|
||||
for _, tt := range hooksMatchTests {
|
||||
value := tt.hooks.Match(tt.id)
|
||||
if reflect.DeepEqual(reflect.ValueOf(value), reflect.ValueOf(tt.value)) {
|
||||
t.Errorf("failed to match %q:\nexpected %#v,\ngot %#v", tt.id, tt.value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var matchRuleTests = []struct {
|
||||
typ, regex, secret, value string
|
||||
param Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"value", "", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"regex", "^z", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false},
|
||||
// failures
|
||||
{"value", "", "", "X", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
{"regex", "^X", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
{"value", "", "2", "X", Argument{"header", "a", ""}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, false, false}, // reference invalid header
|
||||
// errors
|
||||
{"regex", "*", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, true}, // invalid regex
|
||||
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
|
||||
}
|
||||
|
||||
func TestMatchRule(t *testing.T) {
|
||||
for i, tt := range matchRuleTests {
|
||||
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param}
|
||||
ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, (err != nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var andRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule AndRule
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"(a=z, b=y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "y"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=z, b=Y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
// Complex test to cover Rules.Evaluate
|
||||
{
|
||||
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{
|
||||
And: &AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", ""}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Or: &OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", ""}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Not: &NotRule{
|
||||
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", ""}},
|
||||
},
|
||||
},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}},
|
||||
&map[string]interface{}{"Y": "z"}, nil, nil, nil,
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestAndRule(t *testing.T) {
|
||||
for _, tt := range andRuleTests {
|
||||
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var orRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule OrRule
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"(a=z, b=X): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "X"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=X, b=y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"A": "X", "B": "y"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=Z, b=Y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"Y": "Z"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestOrRule(t *testing.T) {
|
||||
for _, tt := range orRuleTests {
|
||||
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var notRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule NotRule
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
}
|
||||
|
||||
func TestNotRule(t *testing.T) {
|
||||
for _, tt := range notRuleTests {
|
||||
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@
|
|||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
|
|
60
hooks.json.tmpl.example
Normal file
60
hooks.json.tmpl.example
Normal file
|
@ -0,0 +1,60 @@
|
|||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "I got the payload!",
|
||||
"response-headers":
|
||||
[
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
28
hooks.yaml.example
Normal file
28
hooks.yaml.example
Normal file
|
@ -0,0 +1,28 @@
|
|||
- id: webhook
|
||||
execute-command: /home/adnan/redeploy-go-webhook.sh
|
||||
command-working-directory: /home/adnan/go
|
||||
response-message: I got the payload!
|
||||
response-headers:
|
||||
- name: Access-Control-Allow-Origin
|
||||
value: '*'
|
||||
pass-arguments-to-command:
|
||||
- source: payload
|
||||
name: head_commit.id
|
||||
- source: payload
|
||||
name: pusher.name
|
||||
- source: payload
|
||||
name: pusher.email
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
type: payload-hmac-sha1
|
||||
secret: mysecret
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
- match:
|
||||
type: value
|
||||
value: refs/heads/master
|
||||
parameter:
|
||||
source: payload
|
||||
name: ref
|
28
hooks.yaml.tmpl.example
Normal file
28
hooks.yaml.tmpl.example
Normal file
|
@ -0,0 +1,28 @@
|
|||
- id: webhook
|
||||
execute-command: /home/adnan/redeploy-go-webhook.sh
|
||||
command-working-directory: /home/adnan/go
|
||||
response-message: I got the payload!
|
||||
response-headers:
|
||||
- name: Access-Control-Allow-Origin
|
||||
value: '*'
|
||||
pass-arguments-to-command:
|
||||
- source: payload
|
||||
name: head_commit.id
|
||||
- source: payload
|
||||
name: pusher.name
|
||||
- source: payload
|
||||
name: pusher.email
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
type: payload-hmac-sha1
|
||||
secret: "{{ getenv "XXXTEST_SECRET" | js }}"
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
- match:
|
||||
type: value
|
||||
value: refs/heads/master
|
||||
parameter:
|
||||
source: payload
|
||||
name: ref
|
11
images/hookdeck-black.svg
Normal file
11
images/hookdeck-black.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg width="355" height="57" viewBox="0 0 355 57" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.475 21.1C54.875 21.1 55.275 21.4 55.475 21.8C55.675 22.2 55.575 22.7 55.275 23L30.275 48C23.375 54.9 12.075 54.9 5.175 48C-1.725 41.1 -1.725 29.8 5.175 22.9L19.575 8.50003C23.675 4.40003 29.275 2.80003 34.575 3.50003C34.975 3.60003 35.275 3.80002 35.475 4.20002C35.575 4.60002 35.475 5.00003 35.175 5.30003L31.975 8.50003C31.475 9.00003 30.875 9.30002 30.275 9.40002C27.875 9.80002 25.575 10.9 23.775 12.7L9.375 27.1C4.775 31.7 4.775 39.2 9.375 43.8C13.975 48.4 21.475 48.4 26.075 43.8L43.975 25.9L41.075 23C40.775 22.7 40.675 22.2 40.875 21.8C41.075 21.4 41.475 21.1 41.875 21.1H54.475ZM28.675 35.6C29.075 35.6 29.475 35.3 29.675 34.9C29.875 34.5 29.775 34 29.475 33.7L26.575 30.8L44.475 12.9C49.075 8.30002 56.575 8.30002 61.175 12.9C65.775 17.5 65.775 25 61.175 29.6L46.675 44C44.875 45.8 42.575 46.9 40.175 47.3C39.475 47.4 38.875 47.7 38.475 48.2L35.275 51.4C34.975 51.7 34.875 52.1 34.975 52.5C35.075 52.9 35.475 53.2 35.875 53.2C41.175 53.9 46.775 52.3 50.875 48.2L65.275 33.8C72.175 26.9 72.175 15.6 65.275 8.70002C58.375 1.80002 47.075 1.80002 40.175 8.70002L15.175 33.7C14.875 34 14.775 34.5 14.975 34.9C15.175 35.3 15.575 35.6 15.975 35.6H28.675Z" fill="#0044CC"/>
|
||||
<path d="M114.784 48V8.09997H122.593V48H114.784ZM88.507 48V8.04297H96.316V48H88.507ZM94.321 31.185V24.003H118.489V31.185H94.321Z" fill="#141412"/>
|
||||
<path d="M144.139 48.684C141.251 48.684 138.667 48.057 136.387 46.803C134.145 45.511 132.378 43.744 131.086 41.502C129.832 39.26 129.205 36.676 129.205 33.75C129.205 30.824 129.832 28.24 131.086 25.998C132.34 23.756 134.088 22.008 136.33 20.754C138.572 19.462 141.137 18.816 144.025 18.816C146.951 18.816 149.535 19.462 151.777 20.754C154.019 22.008 155.767 23.756 157.021 25.998C158.275 28.24 158.902 30.824 158.902 33.75C158.902 36.676 158.275 39.26 157.021 41.502C155.767 43.744 154.019 45.511 151.777 46.803C149.573 48.057 147.027 48.684 144.139 48.684ZM144.139 41.73C145.545 41.73 146.78 41.388 147.844 40.704C148.908 40.02 149.725 39.089 150.295 37.911C150.903 36.695 151.207 35.308 151.207 33.75C151.207 32.192 150.903 30.824 150.295 29.646C149.687 28.43 148.832 27.48 147.73 26.796C146.666 26.112 145.431 25.77 144.025 25.77C142.657 25.77 141.422 26.112 140.32 26.796C139.256 27.48 138.42 28.43 137.812 29.646C137.204 30.824 136.9 32.192 136.9 33.75C136.9 35.308 137.204 36.695 137.812 37.911C138.42 39.089 139.275 40.02 140.377 40.704C141.479 41.388 142.733 41.73 144.139 41.73Z" fill="#141412"/>
|
||||
<path d="M177.829 48.684C174.941 48.684 172.357 48.057 170.077 46.803C167.835 45.511 166.068 43.744 164.776 41.502C163.522 39.26 162.895 36.676 162.895 33.75C162.895 30.824 163.522 28.24 164.776 25.998C166.03 23.756 167.778 22.008 170.02 20.754C172.262 19.462 174.827 18.816 177.715 18.816C180.641 18.816 183.225 19.462 185.467 20.754C187.709 22.008 189.457 23.756 190.711 25.998C191.965 28.24 192.592 30.824 192.592 33.75C192.592 36.676 191.965 39.26 190.711 41.502C189.457 43.744 187.709 45.511 185.467 46.803C183.263 48.057 180.717 48.684 177.829 48.684ZM177.829 41.73C179.235 41.73 180.47 41.388 181.534 40.704C182.598 40.02 183.415 39.089 183.985 37.911C184.593 36.695 184.897 35.308 184.897 33.75C184.897 32.192 184.593 30.824 183.985 29.646C183.377 28.43 182.522 27.48 181.42 26.796C180.356 26.112 179.121 25.77 177.715 25.77C176.347 25.77 175.112 26.112 174.01 26.796C172.946 27.48 172.11 28.43 171.502 29.646C170.894 30.824 170.59 32.192 170.59 33.75C170.59 35.308 170.894 36.695 171.502 37.911C172.11 39.089 172.965 40.02 174.067 40.704C175.169 41.388 176.423 41.73 177.829 41.73Z" fill="#141412"/>
|
||||
<path d="M205.306 40.761L200.803 35.574L215.395 19.5H224.344L205.306 40.761ZM198.124 48V8.09997H205.648V48H198.124ZM216.364 48L206.902 34.206L211.804 29.019L225.199 48H216.364Z" fill="#141412"/>
|
||||
<path d="M248.154 48L247.755 42.699V8.09997H255.279V48H248.154ZM239.205 48.684C236.583 48.684 234.303 48.076 232.365 46.86C230.465 45.644 228.983 43.915 227.919 41.673C226.855 39.431 226.323 36.79 226.323 33.75C226.323 30.672 226.855 28.031 227.919 25.827C228.983 23.585 230.465 21.856 232.365 20.64C234.303 19.424 236.583 18.816 239.205 18.816C241.599 18.816 243.613 19.424 245.247 20.64C246.919 21.856 248.192 23.585 249.066 25.827C249.94 28.031 250.377 30.672 250.377 33.75C250.377 36.79 249.94 39.431 249.066 41.673C248.192 43.915 246.919 45.644 245.247 46.86C243.613 48.076 241.599 48.684 239.205 48.684ZM241.143 41.787C242.435 41.787 243.575 41.445 244.563 40.761C245.589 40.077 246.387 39.127 246.957 37.911C247.527 36.695 247.812 35.308 247.812 33.75C247.812 32.192 247.527 30.805 246.957 29.589C246.387 28.373 245.589 27.442 244.563 26.796C243.575 26.112 242.416 25.77 241.086 25.77C239.718 25.77 238.521 26.112 237.495 26.796C236.469 27.442 235.652 28.373 235.044 29.589C234.436 30.805 234.132 32.192 234.132 33.75C234.132 35.308 234.436 36.695 235.044 37.911C235.652 39.127 236.469 40.077 237.495 40.761C238.559 41.445 239.775 41.787 241.143 41.787Z" fill="#141412"/>
|
||||
<path d="M275.214 48.684C272.402 48.684 269.913 48.057 267.747 46.803C265.581 45.511 263.871 43.744 262.617 41.502C261.401 39.26 260.793 36.676 260.793 33.75C260.793 30.824 261.42 28.24 262.674 25.998C263.966 23.756 265.714 22.008 267.918 20.754C270.16 19.462 272.725 18.816 275.613 18.816C278.121 18.816 280.42 19.481 282.51 20.811C284.638 22.103 286.329 24.022 287.583 26.568C288.875 29.076 289.521 32.135 289.521 35.745H267.918L268.602 35.061C268.602 36.467 268.944 37.702 269.628 38.766C270.35 39.792 271.262 40.59 272.364 41.16C273.504 41.692 274.72 41.958 276.012 41.958C277.57 41.958 278.843 41.635 279.831 40.989C280.819 40.305 281.579 39.431 282.111 38.367L288.837 40.989C288.039 42.585 286.994 43.972 285.702 45.15C284.448 46.29 282.947 47.164 281.199 47.772C279.451 48.38 277.456 48.684 275.214 48.684ZM269.001 30.843L268.317 30.159H282.054L281.427 30.843C281.427 29.475 281.104 28.392 280.458 27.594C279.812 26.758 279.014 26.15 278.064 25.77C277.152 25.39 276.259 25.2 275.385 25.2C274.511 25.2 273.58 25.39 272.592 25.77C271.604 26.15 270.749 26.758 270.027 27.594C269.343 28.392 269.001 29.475 269.001 30.843Z" fill="#141412"/>
|
||||
<path d="M308.2 48.684C305.236 48.684 302.595 48.057 300.277 46.803C297.959 45.511 296.154 43.744 294.862 41.502C293.57 39.222 292.924 36.638 292.924 33.75C292.924 30.824 293.57 28.24 294.862 25.998C296.154 23.756 297.94 22.008 300.22 20.754C302.5 19.462 305.122 18.816 308.086 18.816C310.936 18.816 313.539 19.519 315.895 20.925C318.251 22.331 319.961 24.364 321.025 27.024L313.957 29.532C313.425 28.43 312.589 27.556 311.449 26.91C310.347 26.226 309.112 25.884 307.744 25.884C306.338 25.884 305.103 26.226 304.039 26.91C302.975 27.556 302.139 28.468 301.531 29.646C300.923 30.824 300.619 32.192 300.619 33.75C300.619 35.308 300.923 36.676 301.531 37.854C302.139 38.994 302.994 39.906 304.096 40.59C305.198 41.274 306.452 41.616 307.858 41.616C309.226 41.616 310.461 41.255 311.563 40.533C312.703 39.811 313.539 38.861 314.071 37.683L321.196 40.191C320.094 42.889 318.365 44.979 316.009 46.461C313.691 47.943 311.088 48.684 308.2 48.684Z" fill="#141412"/>
|
||||
<path d="M333.331 40.761L328.828 35.574L343.42 19.5H352.369L333.331 40.761ZM326.149 48V8.09997H333.673V48H326.149ZM344.389 48L334.927 34.206L339.829 29.019L353.224 48H344.389Z" fill="#141412"/>
|
||||
</svg>
|
After Width: | Height: | Size: 7.2 KiB |
1
images/hookdeck-white.svg
Normal file
1
images/hookdeck-white.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg fill="none" height="57" viewBox="0 0 355 57" width="355" xmlns="http://www.w3.org/2000/svg"><path d="m54.475 21.1c.4 0 .8.3 1 .7s.1.9-.2 1.2l-25 25c-6.9 6.9-18.2 6.9-25.1 0s-6.9-18.2 0-25.1l14.4-14.39997c4.1-4.1 9.7-5.7 15-5 .4.1.7.29999.9.69999.1.4 0 .80001-.3 1.10001l-3.2 3.2c-.5.5-1.1.79999-1.7.89999-2.4.4-4.7 1.49998-6.5 3.29998l-14.4 14.4c-4.6 4.6-4.6 12.1 0 16.7s12.1 4.6 16.7 0l17.9-17.9-2.9-2.9c-.3-.3-.4-.8-.2-1.2s.6-.7 1-.7zm-25.8 14.5c.4 0 .8-.3 1-.7s.1-.9-.2-1.2l-2.9-2.9 17.9-17.9c4.6-4.59998 12.1-4.59998 16.7 0 4.6 4.6 4.6 12.1 0 16.7l-14.5 14.4c-1.8 1.8-4.1 2.9-6.5 3.3-.7.1-1.3.4-1.7.9l-3.2 3.2c-.3.3-.4.7-.3 1.1s.5.7.9.7c5.3.7 10.9-.9 15-5l14.4-14.4c6.9-6.9 6.9-18.2 0-25.09998-6.9-6.9-18.2-6.9-25.1 0l-25 24.99998c-.3.3-.4.8-.2 1.2s.6.7 1 .7z" fill="#04c"/><g fill="#fff"><path d="m114.784 48v-39.90003h7.809v39.90003zm-26.277 0v-39.95703h7.809v39.95703zm5.814-16.815v-7.182h24.168v7.182z"/><path d="m144.139 48.684c-2.888 0-5.472-.627-7.752-1.881-2.242-1.292-4.009-3.059-5.301-5.301-1.254-2.242-1.881-4.826-1.881-7.752s.627-5.51 1.881-7.752 3.002-3.99 5.244-5.244c2.242-1.292 4.807-1.938 7.695-1.938 2.926 0 5.51.646 7.752 1.938 2.242 1.254 3.99 3.002 5.244 5.244s1.881 4.826 1.881 7.752-.627 5.51-1.881 7.752-3.002 4.009-5.244 5.301c-2.204 1.254-4.75 1.881-7.638 1.881zm0-6.954c1.406 0 2.641-.342 3.705-1.026s1.881-1.615 2.451-2.793c.608-1.216.912-2.603.912-4.161s-.304-2.926-.912-4.104c-.608-1.216-1.463-2.166-2.565-2.85-1.064-.684-2.299-1.026-3.705-1.026-1.368 0-2.603.342-3.705 1.026-1.064.684-1.9 1.634-2.508 2.85-.608 1.178-.912 2.546-.912 4.104s.304 2.945.912 4.161c.608 1.178 1.463 2.109 2.565 2.793s2.356 1.026 3.762 1.026z"/><path d="m177.829 48.684c-2.888 0-5.472-.627-7.752-1.881-2.242-1.292-4.009-3.059-5.301-5.301-1.254-2.242-1.881-4.826-1.881-7.752s.627-5.51 1.881-7.752 3.002-3.99 5.244-5.244c2.242-1.292 4.807-1.938 7.695-1.938 2.926 0 5.51.646 7.752 1.938 2.242 1.254 3.99 3.002 5.244 5.244s1.881 4.826 1.881 7.752-.627 5.51-1.881 7.752-3.002 4.009-5.244 5.301c-2.204 1.254-4.75 1.881-7.638 1.881zm0-6.954c1.406 0 2.641-.342 3.705-1.026s1.881-1.615 2.451-2.793c.608-1.216.912-2.603.912-4.161s-.304-2.926-.912-4.104c-.608-1.216-1.463-2.166-2.565-2.85-1.064-.684-2.299-1.026-3.705-1.026-1.368 0-2.603.342-3.705 1.026-1.064.684-1.9 1.634-2.508 2.85-.608 1.178-.912 2.546-.912 4.104s.304 2.945.912 4.161c.608 1.178 1.463 2.109 2.565 2.793s2.356 1.026 3.762 1.026z"/><path d="m205.306 40.761-4.503-5.187 14.592-16.074h8.949zm-7.182 7.239v-39.90003h7.524v39.90003zm18.24 0-9.462-13.794 4.902-5.187 13.395 18.981z"/><path d="m248.154 48-.399-5.301v-34.59903h7.524v39.90003zm-8.949.684c-2.622 0-4.902-.608-6.84-1.824-1.9-1.216-3.382-2.945-4.446-5.187s-1.596-4.883-1.596-7.923c0-3.078.532-5.719 1.596-7.923 1.064-2.242 2.546-3.971 4.446-5.187 1.938-1.216 4.218-1.824 6.84-1.824 2.394 0 4.408.608 6.042 1.824 1.672 1.216 2.945 2.945 3.819 5.187.874 2.204 1.311 4.845 1.311 7.923 0 3.04-.437 5.681-1.311 7.923s-2.147 3.971-3.819 5.187c-1.634 1.216-3.648 1.824-6.042 1.824zm1.938-6.897c1.292 0 2.432-.342 3.42-1.026 1.026-.684 1.824-1.634 2.394-2.85s.855-2.603.855-4.161-.285-2.945-.855-4.161-1.368-2.147-2.394-2.793c-.988-.684-2.147-1.026-3.477-1.026-1.368 0-2.565.342-3.591 1.026-1.026.646-1.843 1.577-2.451 2.793s-.912 2.603-.912 4.161.304 2.945.912 4.161 1.425 2.166 2.451 2.85c1.064.684 2.28 1.026 3.648 1.026z"/><path d="m275.214 48.684c-2.812 0-5.301-.627-7.467-1.881-2.166-1.292-3.876-3.059-5.13-5.301-1.216-2.242-1.824-4.826-1.824-7.752s.627-5.51 1.881-7.752c1.292-2.242 3.04-3.99 5.244-5.244 2.242-1.292 4.807-1.938 7.695-1.938 2.508 0 4.807.665 6.897 1.995 2.128 1.292 3.819 3.211 5.073 5.757 1.292 2.508 1.938 5.567 1.938 9.177h-21.603l.684-.684c0 1.406.342 2.641 1.026 3.705.722 1.026 1.634 1.824 2.736 2.394 1.14.532 2.356.798 3.648.798 1.558 0 2.831-.323 3.819-.969.988-.684 1.748-1.558 2.28-2.622l6.726 2.622c-.798 1.596-1.843 2.983-3.135 4.161-1.254 1.14-2.755 2.014-4.503 2.622s-3.743.912-5.985.912zm-6.213-17.841-.684-.684h13.737l-.627.684c0-1.368-.323-2.451-.969-3.249-.646-.836-1.444-1.444-2.394-1.824-.912-.38-1.805-.57-2.679-.57s-1.805.19-2.793.57-1.843.988-2.565 1.824c-.684.798-1.026 1.881-1.026 3.249z"/><path d="m308.2 48.684c-2.964 0-5.605-.627-7.923-1.881-2.318-1.292-4.123-3.059-5.415-5.301-1.292-2.28-1.938-4.864-1.938-7.752 0-2.926.646-5.51 1.938-7.752s3.078-3.99 5.358-5.244c2.28-1.292 4.902-1.938 7.866-1.938 2.85 0 5.453.703 7.809 2.109s4.066 3.439 5.13 6.099l-7.068 2.508c-.532-1.102-1.368-1.976-2.508-2.622-1.102-.684-2.337-1.026-3.705-1.026-1.406 0-2.641.342-3.705 1.026-1.064.646-1.9 1.558-2.508 2.736s-.912 2.546-.912 4.104.304 2.926.912 4.104c.608 1.14 1.463 2.052 2.565 2.736s2.356 1.026 3.762 1.026c1.368 0 2.603-.361 3.705-1.083 1.14-.722 1.976-1.672 2.508-2.85l7.125 2.508c-1.102 2.698-2.831 4.788-5.187 6.27-2.318 1.482-4.921 2.223-7.809 2.223z"/><path d="m333.331 40.761-4.503-5.187 14.592-16.074h8.949zm-7.182 7.239v-39.90003h7.524v39.90003zm18.24 0-9.462-13.794 4.902-5.187 13.395 18.981z"/></g></svg>
|
After Width: | Height: | Size: 4.9 KiB |
986
internal/hook/hook.go
Normal file
986
internal/hook/hook.go
Normal file
|
@ -0,0 +1,986 @@
|
|||
package hook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
// Constants used to specify the parameter source
|
||||
const (
|
||||
SourceHeader string = "header"
|
||||
SourceQuery string = "url"
|
||||
SourceQueryAlias string = "query"
|
||||
SourcePayload string = "payload"
|
||||
SourceRawRequestBody string = "raw-request-body"
|
||||
SourceRequest string = "request"
|
||||
SourceString string = "string"
|
||||
SourceEntirePayload string = "entire-payload"
|
||||
SourceEntireQuery string = "entire-query"
|
||||
SourceEntireHeaders string = "entire-headers"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvNamespace is the prefix used for passing arguments into the command
|
||||
// environment.
|
||||
EnvNamespace string = "HOOK_"
|
||||
)
|
||||
|
||||
// ParameterNodeError describes an error walking a parameter node.
|
||||
type ParameterNodeError struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (e *ParameterNodeError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("parameter node not found: %s", e.key)
|
||||
}
|
||||
|
||||
// IsParameterNodeError returns whether err is of type ParameterNodeError.
|
||||
func IsParameterNodeError(err error) bool {
|
||||
switch err.(type) {
|
||||
case *ParameterNodeError:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// SignatureError describes an invalid payload signature passed to Hook.
|
||||
type SignatureError struct {
|
||||
Signature string
|
||||
Signatures []string
|
||||
|
||||
emptyPayload bool
|
||||
}
|
||||
|
||||
func (e *SignatureError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
var empty string
|
||||
if e.emptyPayload {
|
||||
empty = " on empty payload"
|
||||
}
|
||||
|
||||
if e.Signatures != nil {
|
||||
return fmt.Sprintf("invalid payload signatures %s%s", e.Signatures, empty)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("invalid payload signature %s%s", e.Signature, empty)
|
||||
}
|
||||
|
||||
// IsSignatureError returns whether err is of type SignatureError.
|
||||
func IsSignatureError(err error) bool {
|
||||
switch err.(type) {
|
||||
case *SignatureError:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ArgumentError describes an invalid argument passed to Hook.
|
||||
type ArgumentError struct {
|
||||
Argument Argument
|
||||
}
|
||||
|
||||
func (e *ArgumentError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("couldn't retrieve argument for %+v", e.Argument)
|
||||
}
|
||||
|
||||
// SourceError describes an invalid source passed to Hook.
|
||||
type SourceError struct {
|
||||
Argument Argument
|
||||
}
|
||||
|
||||
func (e *SourceError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("invalid source for argument %+v", e.Argument)
|
||||
}
|
||||
|
||||
// ParseError describes an error parsing user input.
|
||||
type ParseError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ParseError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// ExtractCommaSeparatedValues will extract the values matching the key.
|
||||
func ExtractCommaSeparatedValues(source, prefix string) []string {
|
||||
parts := strings.Split(source, ",")
|
||||
values := make([]string, 0)
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, prefix) {
|
||||
values = append(values, strings.TrimPrefix(part, prefix))
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// ExtractSignatures will extract all the signatures from the source.
|
||||
func ExtractSignatures(source, prefix string) []string {
|
||||
// If there are multiple possible matches, let the comma separated extractor
|
||||
// do it's work.
|
||||
if strings.Contains(source, ",") {
|
||||
return ExtractCommaSeparatedValues(source, prefix)
|
||||
}
|
||||
|
||||
// There were no commas, so just trim the prefix (if it even exists) and
|
||||
// pass it back.
|
||||
return []string{
|
||||
strings.TrimPrefix(source, prefix),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateMAC will verify that the expected mac for the given hash will match
|
||||
// the one provided.
|
||||
func ValidateMAC(payload []byte, mac hash.Hash, signatures []string) (string, error) {
|
||||
// Write the payload to the provided hash.
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
actualMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
for _, signature := range signatures {
|
||||
if hmac.Equal([]byte(signature), []byte(actualMAC)) {
|
||||
return actualMAC, err
|
||||
}
|
||||
}
|
||||
|
||||
e := &SignatureError{Signatures: signatures}
|
||||
if len(payload) == 0 {
|
||||
e.emptyPayload = true
|
||||
}
|
||||
|
||||
return actualMAC, e
|
||||
}
|
||||
|
||||
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
||||
func CheckPayloadSignature(payload []byte, secret, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
// Extract the signatures.
|
||||
signatures := ExtractSignatures(signature, "sha1=")
|
||||
|
||||
// Validate the MAC.
|
||||
return ValidateMAC(payload, hmac.New(sha1.New, []byte(secret)), signatures)
|
||||
}
|
||||
|
||||
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
|
||||
func CheckPayloadSignature256(payload []byte, secret, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
// Extract the signatures.
|
||||
signatures := ExtractSignatures(signature, "sha256=")
|
||||
|
||||
// Validate the MAC.
|
||||
return ValidateMAC(payload, hmac.New(sha256.New, []byte(secret)), signatures)
|
||||
}
|
||||
|
||||
// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload
|
||||
func CheckPayloadSignature512(payload []byte, secret, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
// Extract the signatures.
|
||||
signatures := ExtractSignatures(signature, "sha512=")
|
||||
|
||||
// Validate the MAC.
|
||||
return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures)
|
||||
}
|
||||
|
||||
func CheckScalrSignature(r *Request, signingKey string, checkDate bool) (bool, error) {
|
||||
if r.Headers == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check for the signature and date headers
|
||||
if _, ok := r.Headers["X-Signature"]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
if _, ok := r.Headers["Date"]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
if signingKey == "" {
|
||||
return false, errors.New("signature validation signing key can not be empty")
|
||||
}
|
||||
|
||||
providedSignature := r.Headers["X-Signature"].(string)
|
||||
dateHeader := r.Headers["Date"].(string)
|
||||
mac := hmac.New(sha1.New, []byte(signingKey))
|
||||
mac.Write(r.Body)
|
||||
mac.Write([]byte(dateHeader))
|
||||
expectedSignature := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
|
||||
return false, &SignatureError{Signature: providedSignature}
|
||||
}
|
||||
|
||||
if !checkDate {
|
||||
return true, nil
|
||||
}
|
||||
// Example format: Fri 08 Sep 2017 11:24:32 UTC
|
||||
date, err := time.Parse("Mon 02 Jan 2006 15:04:05 MST", dateHeader)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
now := time.Now()
|
||||
delta := math.Abs(now.Sub(date).Seconds())
|
||||
|
||||
if delta > 300 {
|
||||
return false, &SignatureError{Signature: "outdated"}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckIPWhitelist makes sure the provided remote address (of the form IP:port) falls within the provided IP range
|
||||
// (in CIDR form or a single IP address).
|
||||
func CheckIPWhitelist(remoteAddr, ipRange string) (bool, error) {
|
||||
// Extract IP address from remote address.
|
||||
|
||||
// IPv6 addresses will likely be surrounded by [].
|
||||
ip := strings.Trim(remoteAddr, " []")
|
||||
|
||||
if i := strings.LastIndex(ip, ":"); i != -1 {
|
||||
ip = ip[:i]
|
||||
ip = strings.Trim(ip, " []")
|
||||
}
|
||||
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return false, fmt.Errorf("invalid IP address found in remote address '%s'", remoteAddr)
|
||||
}
|
||||
|
||||
for _, r := range strings.Fields(ipRange) {
|
||||
// Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form.
|
||||
|
||||
if !strings.Contains(r, "/") {
|
||||
r = r + "/32"
|
||||
}
|
||||
|
||||
_, cidr, err := net.ParseCIDR(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if cidr.Contains(parsedIP) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ReplaceParameter replaces parameter value with the passed value in the passed map
|
||||
// (please note you should pass pointer to the map, because we're modifying it)
|
||||
// based on the passed string
|
||||
func ReplaceParameter(s string, params, value interface{}) bool {
|
||||
if params == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ReplaceParameter(p[1], params.([]interface{})[index], value)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
return ReplaceParameter(p[1], pValue, value)
|
||||
}
|
||||
} else {
|
||||
if _, ok := (*params.(*map[string]interface{}))[p[0]]; ok {
|
||||
(*params.(*map[string]interface{}))[p[0]] = value
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetParameter extracts interface{} value based on the passed string
|
||||
func GetParameter(s string, params interface{}) (interface{}, error) {
|
||||
if params == nil {
|
||||
return nil, errors.New("no parameters")
|
||||
}
|
||||
|
||||
paramsValue := reflect.ValueOf(params)
|
||||
|
||||
switch paramsValue.Kind() {
|
||||
case reflect.Slice:
|
||||
paramsValueSliceLength := paramsValue.Len()
|
||||
if paramsValueSliceLength > 0 {
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return nil, &ParameterNodeError{s}
|
||||
}
|
||||
|
||||
return GetParameter(p[1], params.([]interface{})[index])
|
||||
}
|
||||
|
||||
index, err := strconv.ParseUint(s, 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return nil, &ParameterNodeError{s}
|
||||
}
|
||||
|
||||
return params.([]interface{})[index], nil
|
||||
}
|
||||
|
||||
return nil, &ParameterNodeError{s}
|
||||
|
||||
case reflect.Map:
|
||||
// Check for raw key
|
||||
if v, ok := params.(map[string]interface{})[s]; ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Checked for dotted references
|
||||
p := strings.SplitN(s, ".", 2)
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
if len(p) > 1 {
|
||||
return GetParameter(p[1], pValue)
|
||||
}
|
||||
|
||||
return pValue, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, &ParameterNodeError{s}
|
||||
}
|
||||
|
||||
// ExtractParameterAsString extracts value from interface{} as string based on
|
||||
// the passed string. Complex data types are rendered as JSON instead of the Go
|
||||
// Stringer format.
|
||||
func ExtractParameterAsString(s string, params interface{}) (string, error) {
|
||||
pValue, err := GetParameter(s, params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch v := reflect.ValueOf(pValue); v.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice:
|
||||
r, err := json.Marshal(pValue)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(r), nil
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("%v", pValue), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Argument type specifies the parameter key name and the source it should
|
||||
// be extracted from
|
||||
type Argument struct {
|
||||
Source string `json:"source,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
EnvName string `json:"envname,omitempty"`
|
||||
Base64Decode bool `json:"base64decode,omitempty"`
|
||||
}
|
||||
|
||||
// Get Argument method returns the value for the Argument's key name
|
||||
// based on the Argument's source
|
||||
func (ha *Argument) Get(r *Request) (string, error) {
|
||||
var source *map[string]interface{}
|
||||
key := ha.Name
|
||||
|
||||
switch ha.Source {
|
||||
case SourceHeader:
|
||||
source = &r.Headers
|
||||
key = textproto.CanonicalMIMEHeaderKey(ha.Name)
|
||||
|
||||
case SourceQuery, SourceQueryAlias:
|
||||
source = &r.Query
|
||||
|
||||
case SourcePayload:
|
||||
source = &r.Payload
|
||||
|
||||
case SourceString:
|
||||
return ha.Name, nil
|
||||
|
||||
case SourceRawRequestBody:
|
||||
return string(r.Body), nil
|
||||
|
||||
case SourceRequest:
|
||||
if r == nil || r.RawRequest == nil {
|
||||
return "", errors.New("request is nil")
|
||||
}
|
||||
|
||||
switch strings.ToLower(ha.Name) {
|
||||
case "remote-addr":
|
||||
return r.RawRequest.RemoteAddr, nil
|
||||
case "method":
|
||||
return r.RawRequest.Method, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported request key: %q", ha.Name)
|
||||
}
|
||||
|
||||
case SourceEntirePayload:
|
||||
res, err := json.Marshal(&r.Payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(res), nil
|
||||
|
||||
case SourceEntireHeaders:
|
||||
res, err := json.Marshal(&r.Headers)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(res), nil
|
||||
|
||||
case SourceEntireQuery:
|
||||
res, err := json.Marshal(&r.Query)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(res), nil
|
||||
}
|
||||
|
||||
if source != nil {
|
||||
return ExtractParameterAsString(key, *source)
|
||||
}
|
||||
|
||||
return "", errors.New("no source for value retrieval")
|
||||
}
|
||||
|
||||
// Header is a structure containing header name and it's value
|
||||
type Header struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ResponseHeaders is a slice of Header objects
|
||||
type ResponseHeaders []Header
|
||||
|
||||
func (h *ResponseHeaders) String() string {
|
||||
// a 'hack' to display name=value in flag usage listing
|
||||
if len(*h) == 0 {
|
||||
return "name=value"
|
||||
}
|
||||
|
||||
result := make([]string, len(*h))
|
||||
|
||||
for idx, responseHeader := range *h {
|
||||
result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value)
|
||||
}
|
||||
|
||||
return strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
// Set method appends new Header object from header=value notation
|
||||
func (h *ResponseHeaders) Set(value string) error {
|
||||
splitResult := strings.SplitN(value, "=", 2)
|
||||
|
||||
if len(splitResult) != 2 {
|
||||
return errors.New("header flag must be in name=value format")
|
||||
}
|
||||
|
||||
*h = append(*h, Header{Name: splitResult[0], Value: splitResult[1]})
|
||||
return nil
|
||||
}
|
||||
|
||||
// HooksFiles is a slice of String
|
||||
type HooksFiles []string
|
||||
|
||||
func (h *HooksFiles) String() string {
|
||||
if len(*h) == 0 {
|
||||
return "hooks.json"
|
||||
}
|
||||
|
||||
return strings.Join(*h, ", ")
|
||||
}
|
||||
|
||||
// Set method appends new string
|
||||
func (h *HooksFiles) Set(value string) error {
|
||||
*h = append(*h, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hook type is a structure containing details for a single hook
|
||||
type Hook struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
ExecuteCommand string `json:"execute-command,omitempty"`
|
||||
CommandWorkingDirectory string `json:"command-working-directory,omitempty"`
|
||||
ResponseMessage string `json:"response-message,omitempty"`
|
||||
ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
|
||||
CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
|
||||
CaptureCommandOutputOnError bool `json:"include-command-output-in-response-on-error,omitempty"`
|
||||
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
|
||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
|
||||
PassFileToCommand []Argument `json:"pass-file-to-command,omitempty"`
|
||||
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
|
||||
TriggerRule *Rules `json:"trigger-rule,omitempty"`
|
||||
TriggerRuleMismatchHttpResponseCode int `json:"trigger-rule-mismatch-http-response-code,omitempty"`
|
||||
TriggerSignatureSoftFailures bool `json:"trigger-signature-soft-failures,omitempty"`
|
||||
IncomingPayloadContentType string `json:"incoming-payload-content-type,omitempty"`
|
||||
SuccessHttpResponseCode int `json:"success-http-response-code,omitempty"`
|
||||
HTTPMethods []string `json:"http-methods"`
|
||||
}
|
||||
|
||||
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||
// string with the newly created object
|
||||
func (h *Hook) ParseJSONParameters(r *Request) []error {
|
||||
errors := make([]error, 0)
|
||||
|
||||
for i := range h.JSONStringParameters {
|
||||
arg, err := h.JSONStringParameters[i].Get(r)
|
||||
if err != nil {
|
||||
errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
|
||||
} else {
|
||||
var newArg map[string]interface{}
|
||||
|
||||
decoder := json.NewDecoder(strings.NewReader(string(arg)))
|
||||
decoder.UseNumber()
|
||||
|
||||
err := decoder.Decode(&newArg)
|
||||
if err != nil {
|
||||
errors = append(errors, &ParseError{err})
|
||||
continue
|
||||
}
|
||||
|
||||
var source *map[string]interface{}
|
||||
|
||||
switch h.JSONStringParameters[i].Source {
|
||||
case SourceHeader:
|
||||
source = &r.Headers
|
||||
case SourcePayload:
|
||||
source = &r.Payload
|
||||
case SourceQuery, SourceQueryAlias:
|
||||
source = &r.Query
|
||||
}
|
||||
|
||||
if source != nil {
|
||||
key := h.JSONStringParameters[i].Name
|
||||
|
||||
if h.JSONStringParameters[i].Source == SourceHeader {
|
||||
key = textproto.CanonicalMIMEHeaderKey(h.JSONStringParameters[i].Name)
|
||||
}
|
||||
|
||||
ReplaceParameter(key, source, newArg)
|
||||
} else {
|
||||
errors = append(errors, &SourceError{h.JSONStringParameters[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return errors
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractCommandArguments creates a list of arguments, based on the
|
||||
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
||||
func (h *Hook) ExtractCommandArguments(r *Request) ([]string, []error) {
|
||||
args := make([]string, 0)
|
||||
errors := make([]error, 0)
|
||||
|
||||
args = append(args, h.ExecuteCommand)
|
||||
|
||||
for i := range h.PassArgumentsToCommand {
|
||||
arg, err := h.PassArgumentsToCommand[i].Get(r)
|
||||
if err != nil {
|
||||
args = append(args, "")
|
||||
errors = append(errors, &ArgumentError{h.PassArgumentsToCommand[i]})
|
||||
continue
|
||||
}
|
||||
|
||||
args = append(args, arg)
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return args, errors
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// ExtractCommandArgumentsForEnv creates a list of arguments in key=value
|
||||
// format, based on the PassEnvironmentToCommand property that is ready to be used
|
||||
// with exec.Command().
|
||||
func (h *Hook) ExtractCommandArgumentsForEnv(r *Request) ([]string, []error) {
|
||||
args := make([]string, 0)
|
||||
errors := make([]error, 0)
|
||||
for i := range h.PassEnvironmentToCommand {
|
||||
arg, err := h.PassEnvironmentToCommand[i].Get(r)
|
||||
if err != nil {
|
||||
errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
|
||||
continue
|
||||
}
|
||||
|
||||
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
||||
// first try to use the EnvName if specified
|
||||
args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
|
||||
} else {
|
||||
// then fallback on the name
|
||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return args, errors
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// FileParameter describes a pass-file-to-command instance to be stored as file
|
||||
type FileParameter struct {
|
||||
File *os.File
|
||||
EnvName string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// ExtractCommandArgumentsForFile creates a list of arguments in key=value
|
||||
// format, based on the PassFileToCommand property that is ready to be used
|
||||
// with exec.Command().
|
||||
func (h *Hook) ExtractCommandArgumentsForFile(r *Request) ([]FileParameter, []error) {
|
||||
args := make([]FileParameter, 0)
|
||||
errors := make([]error, 0)
|
||||
for i := range h.PassFileToCommand {
|
||||
arg, err := h.PassFileToCommand[i].Get(r)
|
||||
if err != nil {
|
||||
errors = append(errors, &ArgumentError{h.PassFileToCommand[i]})
|
||||
continue
|
||||
}
|
||||
|
||||
if h.PassFileToCommand[i].EnvName == "" {
|
||||
// if no environment-variable name is set, fall-back on the name
|
||||
log.Printf("no ENVVAR name specified, falling back to [%s]", EnvNamespace+strings.ToUpper(h.PassFileToCommand[i].Name))
|
||||
h.PassFileToCommand[i].EnvName = EnvNamespace + strings.ToUpper(h.PassFileToCommand[i].Name)
|
||||
}
|
||||
|
||||
var fileContent []byte
|
||||
if h.PassFileToCommand[i].Base64Decode {
|
||||
dec, err := base64.StdEncoding.DecodeString(arg)
|
||||
if err != nil {
|
||||
log.Printf("error decoding string [%s]", err)
|
||||
}
|
||||
fileContent = []byte(dec)
|
||||
} else {
|
||||
fileContent = []byte(arg)
|
||||
}
|
||||
|
||||
args = append(args, FileParameter{EnvName: h.PassFileToCommand[i].EnvName, Data: fileContent})
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return args, errors
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// Hooks is an array of Hook objects
|
||||
type Hooks []Hook
|
||||
|
||||
// LoadFromFile attempts to load hooks from the specified file, which
|
||||
// can be either JSON or YAML. The asTemplate parameter causes the file
|
||||
// contents to be parsed as a Go text/template prior to unmarshalling.
|
||||
func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parse hook file for hooks
|
||||
file, e := os.ReadFile(path)
|
||||
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if asTemplate {
|
||||
funcMap := template.FuncMap{
|
||||
"cat": cat,
|
||||
"credential": credential,
|
||||
"getenv": getenv,
|
||||
}
|
||||
|
||||
tmpl, err := template.New("hooks").Funcs(funcMap).Parse(string(file))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
err = tmpl.Execute(&buf, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file = buf.Bytes()
|
||||
}
|
||||
|
||||
return yaml.Unmarshal(file, h)
|
||||
}
|
||||
|
||||
// Append appends hooks unless the new hooks contain a hook with an ID that already exists
|
||||
func (h *Hooks) Append(other *Hooks) error {
|
||||
for _, hook := range *other {
|
||||
if h.Match(hook.ID) != nil {
|
||||
return fmt.Errorf("hook with ID %s is already defined", hook.ID)
|
||||
}
|
||||
|
||||
*h = append(*h, hook)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match iterates through Hooks and returns first one that matches the given ID,
|
||||
// if no hook matches the given ID, nil is returned
|
||||
func (h *Hooks) Match(id string) *Hook {
|
||||
for i := range *h {
|
||||
if (*h)[i].ID == id {
|
||||
return &(*h)[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rules is a structure that contains one of the valid rule types
|
||||
type Rules struct {
|
||||
And *AndRule `json:"and,omitempty"`
|
||||
Or *OrRule `json:"or,omitempty"`
|
||||
Not *NotRule `json:"not,omitempty"`
|
||||
Match *MatchRule `json:"match,omitempty"`
|
||||
}
|
||||
|
||||
// Evaluate finds the first rule property that is not nil and returns the value
|
||||
// it evaluates to
|
||||
func (r Rules) Evaluate(req *Request) (bool, error) {
|
||||
switch {
|
||||
case r.And != nil:
|
||||
return r.And.Evaluate(req)
|
||||
case r.Or != nil:
|
||||
return r.Or.Evaluate(req)
|
||||
case r.Not != nil:
|
||||
return r.Not.Evaluate(req)
|
||||
case r.Match != nil:
|
||||
return r.Match.Evaluate(req)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// AndRule will evaluate to true if and only if all of the ChildRules evaluate to true
|
||||
type AndRule []Rules
|
||||
|
||||
// Evaluate AndRule will return true if and only if all of ChildRules evaluate to true
|
||||
func (r AndRule) Evaluate(req *Request) (bool, error) {
|
||||
res := true
|
||||
|
||||
for _, v := range r {
|
||||
rv, err := v.Evaluate(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
res = res && rv
|
||||
if !res {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// OrRule will evaluate to true if any of the ChildRules evaluate to true
|
||||
type OrRule []Rules
|
||||
|
||||
// Evaluate OrRule will return true if any of ChildRules evaluate to true
|
||||
func (r OrRule) Evaluate(req *Request) (bool, error) {
|
||||
res := false
|
||||
|
||||
for _, v := range r {
|
||||
rv, err := v.Evaluate(req)
|
||||
if err != nil {
|
||||
if !IsParameterNodeError(err) {
|
||||
if !req.AllowSignatureErrors || (req.AllowSignatureErrors && !IsSignatureError(err)) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res = res || rv
|
||||
if res {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// NotRule will evaluate to true if any and only if the ChildRule evaluates to false
|
||||
type NotRule Rules
|
||||
|
||||
// Evaluate NotRule will return true if and only if ChildRule evaluates to false
|
||||
func (r NotRule) Evaluate(req *Request) (bool, error) {
|
||||
rv, err := Rules(r).Evaluate(req)
|
||||
return !rv, err
|
||||
}
|
||||
|
||||
// MatchRule will evaluate to true based on the type
|
||||
type MatchRule struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Regex string `json:"regex,omitempty"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Parameter Argument `json:"parameter,omitempty"`
|
||||
IPRange string `json:"ip-range,omitempty"`
|
||||
}
|
||||
|
||||
// Constants for the MatchRule type
|
||||
const (
|
||||
MatchValue string = "value"
|
||||
MatchRegex string = "regex"
|
||||
MatchHMACSHA1 string = "payload-hmac-sha1"
|
||||
MatchHMACSHA256 string = "payload-hmac-sha256"
|
||||
MatchHMACSHA512 string = "payload-hmac-sha512"
|
||||
MatchHashSHA1 string = "payload-hash-sha1"
|
||||
MatchHashSHA256 string = "payload-hash-sha256"
|
||||
MatchHashSHA512 string = "payload-hash-sha512"
|
||||
IPWhitelist string = "ip-whitelist"
|
||||
ScalrSignature string = "scalr-signature"
|
||||
)
|
||||
|
||||
// Evaluate MatchRule will return based on the type
|
||||
func (r MatchRule) Evaluate(req *Request) (bool, error) {
|
||||
if r.Type == IPWhitelist {
|
||||
return CheckIPWhitelist(req.RawRequest.RemoteAddr, r.IPRange)
|
||||
}
|
||||
if r.Type == ScalrSignature {
|
||||
return CheckScalrSignature(req, r.Secret, true)
|
||||
}
|
||||
|
||||
arg, err := r.Parameter.Get(req)
|
||||
if err == nil {
|
||||
switch r.Type {
|
||||
case MatchValue:
|
||||
return compare(arg, r.Value), nil
|
||||
case MatchRegex:
|
||||
return regexp.MatchString(r.Regex, arg)
|
||||
case MatchHashSHA1:
|
||||
log.Print(`warn: use of deprecated option payload-hash-sha1; use payload-hmac-sha1 instead`)
|
||||
fallthrough
|
||||
case MatchHMACSHA1:
|
||||
_, err := CheckPayloadSignature(req.Body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
case MatchHashSHA256:
|
||||
log.Print(`warn: use of deprecated option payload-hash-sha256: use payload-hmac-sha256 instead`)
|
||||
fallthrough
|
||||
case MatchHMACSHA256:
|
||||
_, err := CheckPayloadSignature256(req.Body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
case MatchHashSHA512:
|
||||
log.Print(`warn: use of deprecated option payload-hash-sha512: use payload-hmac-sha512 instead`)
|
||||
fallthrough
|
||||
case MatchHMACSHA512:
|
||||
_, err := CheckPayloadSignature512(req.Body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
}
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// compare is a helper function for constant time string comparisons.
|
||||
func compare(a, b string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
// getenv provides a template function to retrieve OS environment variables.
|
||||
func getenv(s string) string {
|
||||
return os.Getenv(s)
|
||||
}
|
||||
|
||||
// cat provides a template function to retrieve content of files
|
||||
// Similarly to getenv, if no file is found, it returns the empty string
|
||||
func cat(s string) string {
|
||||
data, e := os.ReadFile(s)
|
||||
|
||||
if e != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(string(data), "\n")
|
||||
}
|
||||
|
||||
// credential provides a template function to retreive secrets using systemd's LoadCredential mechanism
|
||||
func credential(s string) string {
|
||||
dir := getenv("CREDENTIALS_DIRECTORY")
|
||||
|
||||
// If no credential directory is found, fallback to the env variable
|
||||
if dir == "" {
|
||||
return getenv(s)
|
||||
}
|
||||
|
||||
return cat(path.Join(dir, s))
|
||||
}
|
728
internal/hook/hook_test.go
Normal file
728
internal/hook/hook_test.go
Normal file
|
@ -0,0 +1,728 @@
|
|||
package hook
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetParameter(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
key string
|
||||
val interface{}
|
||||
expect interface{}
|
||||
ok bool
|
||||
}{
|
||||
// True
|
||||
{"a", map[string]interface{}{"a": "1"}, "1", true},
|
||||
{"a.b", map[string]interface{}{"a.b": "1"}, "1", true},
|
||||
{"a.c", map[string]interface{}{"a": map[string]interface{}{"c": 2}}, 2, true},
|
||||
{"a.1", map[string]interface{}{"a": map[string]interface{}{"1": 3}}, 3, true},
|
||||
{"a.1", map[string]interface{}{"a": []interface{}{"a", "b"}}, "b", true},
|
||||
{"0", []interface{}{"a", "b"}, "a", true},
|
||||
|
||||
// False
|
||||
{"z", map[string]interface{}{"a": "1"}, nil, false},
|
||||
{"a.z", map[string]interface{}{"a": map[string]interface{}{"b": 2}}, nil, false},
|
||||
{"z.b", map[string]interface{}{"a": map[string]interface{}{"z": 2}}, nil, false},
|
||||
{"a.2", map[string]interface{}{"a": []interface{}{"a", "b"}}, nil, false},
|
||||
} {
|
||||
res, err := GetParameter(test.key, test.val)
|
||||
if (err == nil) != test.ok {
|
||||
t.Errorf("unexpected result given {%q, %q}: %s\n", test.key, test.val, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res, test.expect) {
|
||||
t.Errorf("failed given {%q, %q}:\nexpected {%#v}\ngot {%#v}\n", test.key, test.val, test.expect, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignatureTests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(``), "secret", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
|
||||
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
|
||||
{[]byte(``), "secret", "XXXf6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignatureTests {
|
||||
mac, err := CheckPayloadSignature(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignature256Tests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
{[]byte(``), "secret", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
|
||||
{[]byte(``), "secret", "XXX66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature256(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignature256Tests {
|
||||
mac, err := CheckPayloadSignature256(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignature512Tests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha512=4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
||||
{[]byte(``), "secret", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false},
|
||||
{[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", false},
|
||||
{[]byte(``), "secret", "XXX9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature512(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignature512Tests {
|
||||
mac, err := CheckPayloadSignature512(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkScalrSignatureTests = []struct {
|
||||
description string
|
||||
headers map[string]interface{}
|
||||
body []byte
|
||||
secret string
|
||||
expectedSignature string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
"Valid signature",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", true,
|
||||
},
|
||||
{
|
||||
"Wrong signature",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing Date header",
|
||||
map[string]interface{}{"X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing X-Signature header",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing signing key",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestCheckScalrSignature(t *testing.T) {
|
||||
for _, testCase := range checkScalrSignatureTests {
|
||||
r := &Request{
|
||||
Headers: testCase.headers,
|
||||
Body: testCase.body,
|
||||
}
|
||||
valid, err := CheckScalrSignature(r, testCase.secret, false)
|
||||
if valid != testCase.ok {
|
||||
t.Errorf("failed to check scalr signature for test case: %s\nexpected ok:%#v, got ok:%#v}",
|
||||
testCase.description, testCase.ok, valid)
|
||||
}
|
||||
|
||||
if err != nil && testCase.secret != "" && strings.Contains(err.Error(), testCase.expectedSignature) {
|
||||
t.Errorf("error message should not disclose expected mac: %s on test case %s", err, testCase.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkIPWhitelistTests = []struct {
|
||||
addr string
|
||||
ipRange string
|
||||
expect bool
|
||||
ok bool
|
||||
}{
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 ", true, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0 ", false, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 10.0.0.1 ", true, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0/31 ", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 ", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 2001:db8:1::/64", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/64 ", false, true},
|
||||
}
|
||||
|
||||
func TestCheckIPWhitelist(t *testing.T) {
|
||||
for _, tt := range checkIPWhitelistTests {
|
||||
result, err := CheckIPWhitelist(tt.addr, tt.ipRange)
|
||||
if (err == nil) != tt.ok || result != tt.expect {
|
||||
t.Errorf("ip whitelist test failed {%q, %q}:\nwant {expect:%#v, ok:%#v},\ngot {result:%#v, ok:%#v}", tt.addr, tt.ipRange, tt.expect, tt.ok, result, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var extractParameterTests = []struct {
|
||||
s string
|
||||
params interface{}
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"a", map[string]interface{}{"a": "z"}, "z", true},
|
||||
{"a.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "z", true},
|
||||
{"a.b.c", map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}, "z", true},
|
||||
{"a.b.0", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "x", true},
|
||||
{"a.1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "z", true},
|
||||
{"a.1.b.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": map[string]interface{}{"c": "y"}}, map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}}, "z", true},
|
||||
{"b", map[string]interface{}{"b": map[string]interface{}{"z": 1}}, `{"z":1}`, true},
|
||||
{"c", map[string]interface{}{"c": []interface{}{"y", "z"}}, `["y","z"]`, true},
|
||||
{"d", map[string]interface{}{"d": [2]interface{}{"y", "z"}}, `["y","z"]`, true},
|
||||
// failures
|
||||
{"check_nil", nil, "", false},
|
||||
{"a.X", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent parameter reference
|
||||
{"a.X.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-integer slice index
|
||||
{"a.-1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // negative slice index
|
||||
{"a.500.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent slice
|
||||
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
|
||||
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
|
||||
{"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index
|
||||
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
|
||||
}
|
||||
|
||||
func TestExtractParameter(t *testing.T) {
|
||||
for _, tt := range extractParameterTests {
|
||||
value, err := ExtractParameterAsString(tt.s, tt.params)
|
||||
if (err == nil) != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%v}", tt.s, tt.value, tt.ok, value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var argumentGetTests = []struct {
|
||||
source, name string
|
||||
headers, query, payload map[string]interface{}
|
||||
request *http.Request
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"header", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "z", true},
|
||||
{"url", "a", nil, map[string]interface{}{"a": "z"}, nil, nil, "z", true},
|
||||
{"payload", "a", nil, nil, map[string]interface{}{"a": "z"}, nil, "z", true},
|
||||
{"request", "METHOD", nil, nil, map[string]interface{}{"a": "z"}, &http.Request{Method: "POST", RemoteAddr: "127.0.0.1:1234"}, "POST", true},
|
||||
{"request", "remote-addr", nil, nil, map[string]interface{}{"a": "z"}, &http.Request{Method: "POST", RemoteAddr: "127.0.0.1:1234"}, "127.0.0.1:1234", true},
|
||||
{"string", "a", nil, nil, map[string]interface{}{"a": "z"}, nil, "a", true},
|
||||
// failures
|
||||
{"header", "a", nil, map[string]interface{}{"a": "z"}, map[string]interface{}{"a": "z"}, nil, "", false}, // nil headers
|
||||
{"url", "a", map[string]interface{}{"A": "z"}, nil, map[string]interface{}{"a": "z"}, nil, "", false}, // nil query
|
||||
{"payload", "a", map[string]interface{}{"A": "z"}, map[string]interface{}{"a": "z"}, nil, nil, "", false}, // nil payload
|
||||
{"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "", false}, // invalid source
|
||||
}
|
||||
|
||||
func TestArgumentGet(t *testing.T) {
|
||||
for _, tt := range argumentGetTests {
|
||||
a := Argument{tt.source, tt.name, "", false}
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
RawRequest: tt.request,
|
||||
}
|
||||
value, err := a.Get(r)
|
||||
if (err == nil) != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%v}", tt.source, tt.name, tt.value, tt.ok, value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hookParseJSONParametersTests = []struct {
|
||||
params []Argument
|
||||
headers, query, payload map[string]interface{}
|
||||
rheaders, rquery, rpayload map[string]interface{}
|
||||
ok bool
|
||||
}{
|
||||
{[]Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
||||
{[]Argument{Argument{"url", "a", "", false}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
||||
{[]Argument{Argument{"payload", "a", "", false}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
||||
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": `{}`}, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
|
||||
// failures
|
||||
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
|
||||
{[]Argument{Argument{"header", "y", "", false}}, map[string]interface{}{"X": `{}`}, nil, nil, map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
||||
{[]Argument{Argument{"string", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
|
||||
}
|
||||
|
||||
func TestHookParseJSONParameters(t *testing.T) {
|
||||
for _, tt := range hookParseJSONParametersTests {
|
||||
h := &Hook{JSONStringParameters: tt.params}
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
}
|
||||
err := h.ParseJSONParameters(r)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) {
|
||||
t.Errorf("failed to parse %v:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.params, tt.rheaders, tt.ok, tt.headers, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hookExtractCommandArgumentsTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
{"test", []Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||
// failures
|
||||
{"fail", []Argument{Argument{"payload", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArguments(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassArgumentsToCommand: tt.args}
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
}
|
||||
value, err := h.ExtractCommandArguments(r)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here we test the extraction of env variables when the user defined a hook
|
||||
// with the "pass-environment-to-command" directive
|
||||
// we test both cases where the name of the data is used as the name of the
|
||||
// env key & the case where the hook definition sets the env var name to a
|
||||
// fixed value using the envname construct like so::
|
||||
// [
|
||||
// {
|
||||
// "id": "push",
|
||||
// "execute-command": "bb2mm",
|
||||
// "command-working-directory": "/tmp",
|
||||
// "pass-environment-to-command":
|
||||
// [
|
||||
// {
|
||||
// "source": "entire-payload",
|
||||
// "envname": "PAYLOAD"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
// successes
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "", false}},
|
||||
map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"HOOK_a=z"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "MYKEY", false}},
|
||||
map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"MYKEY=z"},
|
||||
true,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"fail",
|
||||
[]Argument{Argument{"payload", "a", "", false}},
|
||||
map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsForEnvTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassEnvironmentToCommand: tt.args}
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
}
|
||||
value, err := h.ExtractCommandArgumentsForEnv(r)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args for env {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksLoadFromFileTests = []struct {
|
||||
path string
|
||||
asTemplate bool
|
||||
ok bool
|
||||
}{
|
||||
{"../../hooks.json.example", false, true},
|
||||
{"../../hooks.yaml.example", false, true},
|
||||
{"../../hooks.json.tmpl.example", true, true},
|
||||
{"../../hooks.yaml.tmpl.example", true, true},
|
||||
{"", false, true},
|
||||
// failures
|
||||
{"missing.json", false, false},
|
||||
}
|
||||
|
||||
func TestHooksLoadFromFile(t *testing.T) {
|
||||
secret := `foo"123`
|
||||
os.Setenv("XXXTEST_SECRET", secret)
|
||||
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHooksTemplateLoadFromFile(t *testing.T) {
|
||||
secret := `foo"123`
|
||||
os.Setenv("XXXTEST_SECRET", secret)
|
||||
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
if !tt.asTemplate {
|
||||
continue
|
||||
}
|
||||
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
s := (*h.Match("webhook").TriggerRule.And)[0].Match.Secret
|
||||
if s != secret {
|
||||
t.Errorf("Expected secret of %q, got %q", secret, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksMatchTests = []struct {
|
||||
id string
|
||||
hooks Hooks
|
||||
value *Hook
|
||||
}{
|
||||
{"a", Hooks{Hook{ID: "a"}}, &Hook{ID: "a"}},
|
||||
{"X", Hooks{Hook{ID: "a"}}, new(Hook)},
|
||||
}
|
||||
|
||||
func TestHooksMatch(t *testing.T) {
|
||||
for _, tt := range hooksMatchTests {
|
||||
value := tt.hooks.Match(tt.id)
|
||||
if reflect.DeepEqual(reflect.ValueOf(value), reflect.ValueOf(tt.value)) {
|
||||
t.Errorf("failed to match %q:\nexpected %#v,\ngot %#v", tt.id, tt.value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var matchRuleTests = []struct {
|
||||
typ, regex, secret, value, ipRange string
|
||||
param Argument
|
||||
headers, query, payload map[string]interface{}
|
||||
body []byte
|
||||
remoteAddr string
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"value", "", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
// failures
|
||||
{"value", "", "", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, true}, // reference invalid header
|
||||
// errors
|
||||
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
|
||||
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hmac-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hash-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
// IP whitelisting, valid cases
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1", Argument{}, nil, nil, nil, []byte{}, "192.168.0.1:9000", true, false}, // valid IPv4, no range
|
||||
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, with range
|
||||
{"ip-whitelist", "", "", "", "::1", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, no range
|
||||
// IP whitelisting, invalid cases
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, no range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.a:9000", false, true}, // invalid IPv4 address
|
||||
{"ip-whitelist", "", "", "", "::1/a", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, with range
|
||||
{"ip-whitelist", "", "", "", "::z", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, no range
|
||||
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::z]:9000", false, true}, // invalid IPv6 address
|
||||
}
|
||||
|
||||
func TestMatchRule(t *testing.T) {
|
||||
for i, tt := range matchRuleTests {
|
||||
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param, tt.ipRange}
|
||||
req := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
Body: tt.body,
|
||||
RawRequest: &http.Request{
|
||||
RemoteAddr: tt.remoteAddr,
|
||||
},
|
||||
}
|
||||
ok, err := r.Evaluate(req)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var andRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule AndRule
|
||||
headers, query, payload map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"(a=z, b=y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"A": "z", "B": "y"}, nil, nil,
|
||||
[]byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=z, b=Y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"A": "z", "B": "Y"}, nil, nil,
|
||||
[]byte{},
|
||||
false, false,
|
||||
},
|
||||
// Complex test to cover Rules.Evaluate
|
||||
{
|
||||
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{
|
||||
And: &AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", false}, ""}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Or: &OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", false}, ""}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Not: &NotRule{
|
||||
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", false}, ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil,
|
||||
[]byte{},
|
||||
true, false,
|
||||
},
|
||||
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
|
||||
map[string]interface{}{"Y": "z"}, nil, nil, nil,
|
||||
false, true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestAndRule(t *testing.T) {
|
||||
for _, tt := range andRuleTests {
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
Body: tt.body,
|
||||
}
|
||||
ok, err := tt.rule.Evaluate(r)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var orRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule OrRule
|
||||
headers, query, payload map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"(a=z, b=X): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"A": "z", "B": "X"}, nil, nil,
|
||||
[]byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=X, b=y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"A": "X", "B": "y"}, nil, nil,
|
||||
[]byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=Z, b=Y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil,
|
||||
[]byte{},
|
||||
false, false,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"missing parameter node",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
},
|
||||
map[string]interface{}{"Y": "Z"}, nil, nil,
|
||||
[]byte{},
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestOrRule(t *testing.T) {
|
||||
for _, tt := range orRuleTests {
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
Body: tt.body,
|
||||
}
|
||||
ok, err := tt.rule.Evaluate(r)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var notRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule NotRule
|
||||
headers, query, payload map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
}
|
||||
|
||||
func TestNotRule(t *testing.T) {
|
||||
for _, tt := range notRuleTests {
|
||||
r := &Request{
|
||||
Headers: tt.headers,
|
||||
Query: tt.query,
|
||||
Payload: tt.payload,
|
||||
Body: tt.body,
|
||||
}
|
||||
ok, err := tt.rule.Evaluate(r)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
a, b string
|
||||
ok bool
|
||||
}{
|
||||
{"abcd", "abcd", true},
|
||||
{"zyxw", "abcd", false},
|
||||
} {
|
||||
if ok := compare(tt.a, tt.b); ok != tt.ok {
|
||||
t.Errorf("compare failed for %q and %q: got %v\n", tt.a, tt.b, ok)
|
||||
}
|
||||
}
|
||||
}
|
119
internal/hook/request.go
Normal file
119
internal/hook/request.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package hook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"unicode"
|
||||
|
||||
"github.com/clbanning/mxj/v2"
|
||||
)
|
||||
|
||||
// Request represents a webhook request.
|
||||
type Request struct {
|
||||
// The request ID set by the RequestID middleware.
|
||||
ID string
|
||||
|
||||
// The Content-Type of the request.
|
||||
ContentType string
|
||||
|
||||
// The raw request body.
|
||||
Body []byte
|
||||
|
||||
// Headers is a map of the parsed headers.
|
||||
Headers map[string]interface{}
|
||||
|
||||
// Query is a map of the parsed URL query values.
|
||||
Query map[string]interface{}
|
||||
|
||||
// Payload is a map of the parsed payload.
|
||||
Payload map[string]interface{}
|
||||
|
||||
// The underlying HTTP request.
|
||||
RawRequest *http.Request
|
||||
|
||||
// Treat signature errors as simple validate failures.
|
||||
AllowSignatureErrors bool
|
||||
}
|
||||
|
||||
func (r *Request) ParseJSONPayload() error {
|
||||
decoder := json.NewDecoder(bytes.NewReader(r.Body))
|
||||
decoder.UseNumber()
|
||||
|
||||
var firstChar byte
|
||||
for i := 0; i < len(r.Body); i++ {
|
||||
if unicode.IsSpace(rune(r.Body[i])) {
|
||||
continue
|
||||
}
|
||||
firstChar = r.Body[i]
|
||||
break
|
||||
}
|
||||
|
||||
if firstChar == byte('[') {
|
||||
var arrayPayload interface{}
|
||||
err := decoder.Decode(&arrayPayload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing JSON array payload %+v", err)
|
||||
}
|
||||
|
||||
r.Payload = make(map[string]interface{}, 1)
|
||||
r.Payload["root"] = arrayPayload
|
||||
} else {
|
||||
err := decoder.Decode(&r.Payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing JSON payload %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Request) ParseHeaders(headers map[string][]string) {
|
||||
r.Headers = make(map[string]interface{}, len(headers))
|
||||
|
||||
for k, v := range headers {
|
||||
if len(v) > 0 {
|
||||
r.Headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Request) ParseQuery(query map[string][]string) {
|
||||
r.Query = make(map[string]interface{}, len(query))
|
||||
|
||||
for k, v := range query {
|
||||
if len(v) > 0 {
|
||||
r.Query[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Request) ParseFormPayload() error {
|
||||
fd, err := url.ParseQuery(string(r.Body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing form payload %+v", err)
|
||||
}
|
||||
|
||||
r.Payload = make(map[string]interface{}, len(fd))
|
||||
|
||||
for k, v := range fd {
|
||||
if len(v) > 0 {
|
||||
r.Payload[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Request) ParseXMLPayload() error {
|
||||
var err error
|
||||
|
||||
r.Payload, err = mxj.NewMapXmlReader(bytes.NewReader(r.Body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing XML payload: %+v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
105
internal/middleware/dumper.go
Normal file
105
internal/middleware/dumper.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package middleware
|
||||
|
||||
// Derived from the Goa project, MIT Licensed
|
||||
// https://github.com/goadesign/goa/blob/v3/http/middleware/debug.go
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// responseDupper tees the response to a buffer and a response writer.
|
||||
type responseDupper struct {
|
||||
http.ResponseWriter
|
||||
Buffer *bytes.Buffer
|
||||
Status int
|
||||
}
|
||||
|
||||
// Dumper returns a debug middleware which prints detailed information about
|
||||
// incoming requests and outgoing responses including all headers, parameters
|
||||
// and bodies.
|
||||
func Dumper(w io.Writer) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
buf := &bytes.Buffer{}
|
||||
// Request ID
|
||||
rid := r.Context().Value(RequestIDKey)
|
||||
|
||||
// Dump request
|
||||
|
||||
bd, err := httputil.DumpRequest(r, true)
|
||||
if err != nil {
|
||||
buf.WriteString(fmt.Sprintf("[%s] Error dumping request for debugging: %s\n", rid, err))
|
||||
}
|
||||
|
||||
sc := bufio.NewScanner(bytes.NewBuffer(bd))
|
||||
sc.Split(bufio.ScanLines)
|
||||
for sc.Scan() {
|
||||
buf.WriteString(fmt.Sprintf("> [%s] ", rid))
|
||||
buf.WriteString(sc.Text() + "\n")
|
||||
}
|
||||
|
||||
w.Write(buf.Bytes())
|
||||
buf.Reset()
|
||||
|
||||
// Dump Response
|
||||
|
||||
dupper := &responseDupper{ResponseWriter: rw, Buffer: &bytes.Buffer{}}
|
||||
h.ServeHTTP(dupper, r)
|
||||
|
||||
// Response Status
|
||||
buf.WriteString(fmt.Sprintf("< [%s] %d %s\n", rid, dupper.Status, http.StatusText(dupper.Status)))
|
||||
|
||||
// Response Headers
|
||||
keys := make([]string, len(dupper.Header()))
|
||||
i := 0
|
||||
for k := range dupper.Header() {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
buf.WriteString(fmt.Sprintf("< [%s] %s: %s\n", rid, k, strings.Join(dupper.Header()[k], ", ")))
|
||||
}
|
||||
|
||||
// Response Body
|
||||
if dupper.Buffer.Len() > 0 {
|
||||
buf.WriteString(fmt.Sprintf("< [%s]\n", rid))
|
||||
sc = bufio.NewScanner(dupper.Buffer)
|
||||
sc.Split(bufio.ScanLines)
|
||||
for sc.Scan() {
|
||||
buf.WriteString(fmt.Sprintf("< [%s] ", rid))
|
||||
buf.WriteString(sc.Text() + "\n")
|
||||
}
|
||||
}
|
||||
w.Write(buf.Bytes())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes the data to the buffer and connection as part of an HTTP reply.
|
||||
func (r *responseDupper) Write(b []byte) (int, error) {
|
||||
r.Buffer.Write(b)
|
||||
return r.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// WriteHeader records the status and sends an HTTP response header with status code.
|
||||
func (r *responseDupper) WriteHeader(s int) {
|
||||
r.Status = s
|
||||
r.ResponseWriter.WriteHeader(s)
|
||||
}
|
||||
|
||||
// Hijack supports the http.Hijacker interface.
|
||||
func (r *responseDupper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hijacker, ok := r.ResponseWriter.(http.Hijacker); ok {
|
||||
return hijacker.Hijack()
|
||||
}
|
||||
return nil, nil, fmt.Errorf("dumper middleware: inner ResponseWriter cannot be hijacked: %T", r.ResponseWriter)
|
||||
}
|
59
internal/middleware/logger.go
Normal file
59
internal/middleware/logger.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
// Logger is a middleware that logs useful data about each HTTP request.
|
||||
type Logger struct {
|
||||
Logger middleware.LoggerInterface
|
||||
}
|
||||
|
||||
// NewLogger creates a new RequestLogger Handler.
|
||||
func NewLogger() func(next http.Handler) http.Handler {
|
||||
return middleware.RequestLogger(&Logger{})
|
||||
}
|
||||
|
||||
// NewLogEntry creates a new LogEntry for the request.
|
||||
func (l *Logger) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
e := &LogEntry{
|
||||
req: r,
|
||||
buf: &bytes.Buffer{},
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// LogEntry represents an individual log entry.
|
||||
type LogEntry struct {
|
||||
*Logger
|
||||
req *http.Request
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
// Write constructs and writes the final log entry.
|
||||
func (l *LogEntry) Write(status, totalBytes int, header http.Header, elapsed time.Duration, extra interface{}) {
|
||||
rid := GetReqID(l.req.Context())
|
||||
if rid != "" {
|
||||
fmt.Fprintf(l.buf, "[%s] ", rid)
|
||||
}
|
||||
|
||||
fmt.Fprintf(l.buf, "%03d | %s | %s | ", status, humanize.IBytes(uint64(totalBytes)), elapsed)
|
||||
l.buf.WriteString(l.req.Host + " | " + l.req.Method + " " + l.req.RequestURI)
|
||||
log.Print(l.buf.String())
|
||||
}
|
||||
|
||||
// Panic prints the call stack for a panic.
|
||||
func (l *LogEntry) Panic(v interface{}, stack []byte) {
|
||||
e := l.NewLogEntry(l.req).(*LogEntry)
|
||||
fmt.Fprintf(e.buf, "panic: %#v", v)
|
||||
log.Print(e.buf.String())
|
||||
log.Print(string(stack))
|
||||
}
|
98
internal/middleware/request_id.go
Normal file
98
internal/middleware/request_id.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package middleware
|
||||
|
||||
// Derived from Goa project, MIT Licensed
|
||||
// https://github.com/goadesign/goa/blob/v3/http/middleware/requestid.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
// Key to use when setting the request ID.
|
||||
type ctxKeyRequestID int
|
||||
|
||||
// RequestIDKey is the key that holds the unique request ID in a request context.
|
||||
const RequestIDKey ctxKeyRequestID = 0
|
||||
|
||||
// RequestID is a middleware that injects a request ID into the context of each
|
||||
// request.
|
||||
func RequestID(options ...RequestIDOption) func(http.Handler) http.Handler {
|
||||
o := newRequestIDOptions(options...)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var id string
|
||||
|
||||
if o.UseRequestID() {
|
||||
id = r.Header.Get("X-Request-Id")
|
||||
if o.requestIDLimit > 0 && len(id) > o.requestIDLimit {
|
||||
id = id[:o.requestIDLimit]
|
||||
}
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
id = uuid.Must(uuid.NewV4()).String()[:6]
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, RequestIDKey, id)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetReqID returns a request ID from the given context if one is present.
|
||||
// Returns the empty string if a request ID cannot be found.
|
||||
func GetReqID(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
|
||||
return reqID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func UseXRequestIDHeaderOption(f bool) RequestIDOption {
|
||||
return func(o *RequestIDOptions) *RequestIDOptions {
|
||||
o.useXRequestID = f
|
||||
return o
|
||||
}
|
||||
}
|
||||
|
||||
func XRequestIDLimitOption(limit int) RequestIDOption {
|
||||
return func(o *RequestIDOptions) *RequestIDOptions {
|
||||
o.requestIDLimit = limit
|
||||
return o
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
RequestIDOption func(*RequestIDOptions) *RequestIDOptions
|
||||
|
||||
RequestIDOptions struct {
|
||||
// useXRequestID enabled the use of the X-Request-Id request header as
|
||||
// the request ID.
|
||||
useXRequestID bool
|
||||
|
||||
// requestIDLimit is the maximum length of the X-Request-Id header
|
||||
// allowed. Values longer than this value are truncated. Zero value
|
||||
// means no limit.
|
||||
requestIDLimit int
|
||||
}
|
||||
)
|
||||
|
||||
func newRequestIDOptions(options ...RequestIDOption) *RequestIDOptions {
|
||||
o := new(RequestIDOptions)
|
||||
for _, opt := range options {
|
||||
o = opt(o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *RequestIDOptions) UseRequestID() bool {
|
||||
return o.useXRequestID
|
||||
}
|
4
internal/pidfile/README.md
Normal file
4
internal/pidfile/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
Package pidfile is derived from github.com/moby/moby/pkg/pidfile.
|
||||
|
||||
Moby is licensed under the Apache License, Version 2.0.
|
||||
Copyright 2012-2017 Docker, Inc.
|
11
internal/pidfile/mkdirall.go
Normal file
11
internal/pidfile/mkdirall.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
// +build !windows
|
||||
|
||||
package pidfile
|
||||
|
||||
import "os"
|
||||
|
||||
// MkdirAll creates a directory named path along with any necessary parents,
|
||||
// with permission specified by attribute perm for all dir created.
|
||||
func MkdirAll(path string, perm os.FileMode) error {
|
||||
return os.MkdirAll(path, perm)
|
||||
}
|
109
internal/pidfile/mkdirall_windows.go
Normal file
109
internal/pidfile/mkdirall_windows.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
// +build windows
|
||||
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// MkdirAll implementation that is volume path aware for Windows. It can be used
|
||||
// as a drop-in replacement for os.MkdirAll()
|
||||
func MkdirAll(path string, _ os.FileMode) error {
|
||||
return mkdirall(path, false, "")
|
||||
}
|
||||
|
||||
// mkdirall is a custom version of os.MkdirAll modified for use on Windows
|
||||
// so that it is both volume path aware, and can create a directory with
|
||||
// a DACL.
|
||||
func mkdirall(path string, applyACL bool, sddl string) error {
|
||||
if re := regexp.MustCompile(`^\\\\\?\\Volume{[a-z0-9-]+}$`); re.MatchString(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The rest of this method is largely copied from os.MkdirAll and should be kept
|
||||
// as-is to ensure compatibility.
|
||||
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
dir, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return &os.PathError{
|
||||
Op: "mkdir",
|
||||
Path: path,
|
||||
Err: syscall.ENOTDIR,
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
i := len(path)
|
||||
for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator.
|
||||
i--
|
||||
}
|
||||
|
||||
j := i
|
||||
for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element.
|
||||
j--
|
||||
}
|
||||
|
||||
if j > 1 {
|
||||
// Create parent
|
||||
err = mkdirall(path[0:j-1], false, sddl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parent now exists; invoke os.Mkdir or mkdirWithACL and use its result.
|
||||
if applyACL {
|
||||
err = mkdirWithACL(path, sddl)
|
||||
} else {
|
||||
err = os.Mkdir(path, 0)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := os.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mkdirWithACL creates a new directory. If there is an error, it will be of
|
||||
// type *PathError. .
|
||||
//
|
||||
// This is a modified and combined version of os.Mkdir and windows.Mkdir
|
||||
// in golang to cater for creating a directory am ACL permitting full
|
||||
// access, with inheritance, to any subfolder/file for Built-in Administrators
|
||||
// and Local System.
|
||||
func mkdirWithACL(name string, sddl string) error {
|
||||
sa := windows.SecurityAttributes{Length: 0}
|
||||
sd, err := windows.SecurityDescriptorFromString(sddl)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "mkdir", Path: name, Err: err}
|
||||
}
|
||||
sa.Length = uint32(unsafe.Sizeof(sa))
|
||||
sa.InheritHandle = 1
|
||||
sa.SecurityDescriptor = sd
|
||||
|
||||
namep, err := windows.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "mkdir", Path: name, Err: err}
|
||||
}
|
||||
|
||||
e := windows.CreateDirectory(namep, &sa)
|
||||
if e != nil {
|
||||
return &os.PathError{Op: "mkdir", Path: name, Err: e}
|
||||
}
|
||||
return nil
|
||||
}
|
51
internal/pidfile/pidfile.go
Normal file
51
internal/pidfile/pidfile.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
// Package pidfile provides structure and helper functions to create and remove
|
||||
// PID file. A PID file is usually a file used to store the process ID of a
|
||||
// running process.
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PIDFile is a file used to store the process ID of a running process.
|
||||
type PIDFile struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func checkPIDFileAlreadyExists(path string) error {
|
||||
if pidByte, err := ioutil.ReadFile(path); err == nil {
|
||||
pidString := strings.TrimSpace(string(pidByte))
|
||||
if pid, err := strconv.Atoi(pidString); err == nil {
|
||||
if processExists(pid) {
|
||||
return fmt.Errorf("pid file found, ensure webhook is not running or delete %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// New creates a PIDfile using the specified path.
|
||||
func New(path string) (*PIDFile, error) {
|
||||
if err := checkPIDFileAlreadyExists(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Note MkdirAll returns nil if a directory already exists
|
||||
if err := MkdirAll(filepath.Dir(path), os.FileMode(0o755)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", os.Getpid())), 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PIDFile{path: path}, nil
|
||||
}
|
||||
|
||||
// Remove removes the PIDFile.
|
||||
func (file PIDFile) Remove() error {
|
||||
return os.Remove(file.path)
|
||||
}
|
14
internal/pidfile/pidfile_darwin.go
Normal file
14
internal/pidfile/pidfile_darwin.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
// +build darwin
|
||||
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func processExists(pid int) bool {
|
||||
// OS X does not have a proc filesystem.
|
||||
// Use kill -0 pid to judge if the process exists.
|
||||
err := unix.Kill(pid, 0)
|
||||
return err == nil
|
||||
}
|
38
internal/pidfile/pidfile_test.go
Normal file
38
internal/pidfile/pidfile_test.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package pidfile
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewAndRemove(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "test-pidfile")
|
||||
if err != nil {
|
||||
t.Fatal("Could not create test directory")
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, "testfile")
|
||||
file, err := New(path)
|
||||
if err != nil {
|
||||
t.Fatal("Could not create test file", err)
|
||||
}
|
||||
|
||||
_, err = New(path)
|
||||
if err == nil {
|
||||
t.Fatal("Test file creation not blocked")
|
||||
}
|
||||
|
||||
if err := file.Remove(); err != nil {
|
||||
t.Fatal("Could not delete created test file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveInvalidPath(t *testing.T) {
|
||||
file := PIDFile{path: filepath.Join("foo", "bar")}
|
||||
|
||||
if err := file.Remove(); err == nil {
|
||||
t.Fatal("Non-existing file doesn't give an error on delete")
|
||||
}
|
||||
}
|
16
internal/pidfile/pidfile_unix.go
Normal file
16
internal/pidfile/pidfile_unix.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// +build !windows,!darwin
|
||||
|
||||
package pidfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func processExists(pid int) bool {
|
||||
if _, err := os.Stat(filepath.Join("/proc", strconv.Itoa(pid))); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
25
internal/pidfile/pidfile_windows.go
Normal file
25
internal/pidfile/pidfile_windows.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package pidfile
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
processQueryLimitedInformation = 0x1000
|
||||
|
||||
stillActive = 259
|
||||
)
|
||||
|
||||
func processExists(pid int) bool {
|
||||
h, err := windows.OpenProcess(processQueryLimitedInformation, false, uint32(pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var c uint32
|
||||
err = windows.GetExitCodeProcess(h, &c)
|
||||
windows.Close(h)
|
||||
if err != nil {
|
||||
return c == stillActive
|
||||
}
|
||||
return true
|
||||
}
|
47
platform_unix.go
Normal file
47
platform_unix.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/coreos/go-systemd/v22/activation"
|
||||
"net"
|
||||
)
|
||||
|
||||
func platformFlags() {
|
||||
flag.StringVar(&socket, "socket", "", "path to a Unix socket (e.g. /tmp/webhook.sock) to use instead of listening on an ip and port; if specified, the ip and port options are ignored")
|
||||
flag.IntVar(&setGID, "setgid", 0, "set group ID after opening listening port; must be used with setuid, not permitted with -socket")
|
||||
flag.IntVar(&setUID, "setuid", 0, "set user ID after opening listening port; must be used with setgid, not permitted with -socket")
|
||||
}
|
||||
|
||||
func trySocketListener() (net.Listener, error) {
|
||||
// first check whether we have any sockets from systemd
|
||||
listeners, err := activation.Listeners()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve sockets from systemd: %w", err)
|
||||
}
|
||||
numListeners := len(listeners)
|
||||
if numListeners > 1 {
|
||||
return nil, fmt.Errorf("received %d sockets from systemd, but only 1 is supported", numListeners)
|
||||
}
|
||||
if numListeners == 1 {
|
||||
sockAddr := listeners[0].Addr()
|
||||
if sockAddr.Network() == "tcp" {
|
||||
addr = sockAddr.String()
|
||||
} else {
|
||||
addr = fmt.Sprintf("{%s:%s}", sockAddr.Network(), sockAddr.String())
|
||||
}
|
||||
return listeners[0], nil
|
||||
}
|
||||
// if we get to here, we got no sockets from systemd, so check -socket flag
|
||||
if socket != "" {
|
||||
if setGID != 0 || setUID != 0 {
|
||||
return nil, fmt.Errorf("-setuid and -setgid options are not compatible with -socket. If you need to bind a socket as root but run webhook as a different user, consider using systemd activation")
|
||||
}
|
||||
addr = fmt.Sprintf("{unix:%s}", socket)
|
||||
return net.Listen("unix", socket)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
23
platform_windows.go
Normal file
23
platform_windows.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/Microsoft/go-winio"
|
||||
"net"
|
||||
)
|
||||
|
||||
func platformFlags() {
|
||||
flag.StringVar(&socket, "socket", "", "path to a Windows named pipe (e.g. \\\\.\\pipe\\webhook) to use instead of listening on an ip and port; if specified, the ip and port options are ignored")
|
||||
}
|
||||
|
||||
func trySocketListener() (net.Listener, error) {
|
||||
if socket != "" {
|
||||
addr = fmt.Sprintf("{pipe:%s}", socket)
|
||||
return winio.ListenPipe(socket, nil)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
35
signals.go
35
signals.go
|
@ -1,3 +1,4 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
@ -6,6 +7,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
|
@ -14,6 +16,9 @@ func setupSignals() {
|
|||
|
||||
signals = make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGUSR1)
|
||||
signal.Notify(signals, syscall.SIGHUP)
|
||||
signal.Notify(signals, syscall.SIGTERM)
|
||||
signal.Notify(signals, os.Interrupt)
|
||||
|
||||
go watchForSignals()
|
||||
}
|
||||
|
@ -23,11 +28,35 @@ func watchForSignals() {
|
|||
|
||||
for {
|
||||
sig := <-signals
|
||||
if sig == syscall.SIGUSR1 {
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Println("caught USR1 signal")
|
||||
reloadAllHooks()
|
||||
|
||||
reloadHooks()
|
||||
} else {
|
||||
case syscall.SIGHUP:
|
||||
log.Println("caught HUP signal")
|
||||
reloadAllHooks()
|
||||
|
||||
case os.Interrupt, syscall.SIGTERM:
|
||||
log.Printf("caught %s signal; exiting\n", sig)
|
||||
if pidFile != nil {
|
||||
err := pidFile.Remove()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
if socket != "" && !strings.HasPrefix(socket, "@") {
|
||||
// we've been listening on a named Unix socket, delete it
|
||||
// before we exit so subsequent runs can re-bind the same
|
||||
// socket path
|
||||
err := os.Remove(socket)
|
||||
if err != nil {
|
||||
log.Printf("Failed to remove socket file %s: %v", socket, err)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
default:
|
||||
log.Printf("caught unhandled signal %+v\n", sig)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -23,4 +24,14 @@ func main() {
|
|||
if len(env) > 0 {
|
||||
fmt.Printf("env: %s\n", strings.Join(env, " "))
|
||||
}
|
||||
|
||||
if (len(os.Args) > 1) && (strings.HasPrefix(os.Args[1], "exit=")) {
|
||||
exit_code_str := os.Args[1][5:]
|
||||
exit_code, err := strconv.Atoi(exit_code_str)
|
||||
if err != nil {
|
||||
fmt.Printf("Exit code %s not an int!", exit_code_str)
|
||||
os.Exit(-1)
|
||||
}
|
||||
os.Exit(exit_code)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"id": "github",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"http-methods": ["Post "],
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule-mismatch-http-response-code": 400,
|
||||
"pass-environment-to-command":
|
||||
|
@ -30,7 +31,7 @@
|
|||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
|
@ -54,6 +55,149 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "github-multi-sig",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"http-methods": ["Post "],
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule-mismatch-http-response-code": 400,
|
||||
"trigger-signature-soft-failures": true,
|
||||
"pass-environment-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.timestamp"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.author.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
"or":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecretFAIL",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "github-multi-sig-fail",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"http-methods": ["Post "],
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule-mismatch-http-response-code": 400,
|
||||
"pass-environment-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.timestamp"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.author.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
"or":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecretFAIL",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bitbucket",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
|
@ -136,5 +280,279 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "xml",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"response-message": "success",
|
||||
"trigger-rule": {
|
||||
"and": [
|
||||
{
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "app.users.user.0.-name"
|
||||
},
|
||||
"value": "Jeff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "app.messages.message.#text"
|
||||
},
|
||||
"value": "Hello!!"
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "txt-raw",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"include-command-output-in-response": true,
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "raw-request-body"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sendgrid",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"response-message": "success",
|
||||
"trigger-rule": {
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "root.0.event"
|
||||
},
|
||||
"value": "processed"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sendgrid/dir",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"response-message": "success",
|
||||
"trigger-rule": {
|
||||
"match": {
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "root.0.event"
|
||||
},
|
||||
"value": "it worked!"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "plex",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"response-message": "success",
|
||||
"parse-parameters-as-json": [
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "payload"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"parameter": {
|
||||
"source": "payload",
|
||||
"name": "payload.event"
|
||||
},
|
||||
"value": "media.play"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "capture-command-output-on-success-not-by-default",
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "exit=0"
|
||||
}
|
||||
],
|
||||
"execute-command": "{{ .Hookecho }}"
|
||||
},
|
||||
{
|
||||
"id": "capture-command-output-on-success-yes-with-flag",
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "exit=0"
|
||||
}
|
||||
],
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"include-command-output-in-response": true
|
||||
},
|
||||
{
|
||||
"id": "capture-command-output-on-error-not-by-default",
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "exit=1"
|
||||
}
|
||||
],
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"include-command-output-in-response": true
|
||||
},
|
||||
{
|
||||
"id": "capture-command-output-on-error-yes-with-extra-flag",
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "exit=1"
|
||||
}
|
||||
],
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"include-command-output-in-response": true,
|
||||
"include-command-output-in-response-on-error": true
|
||||
},
|
||||
{
|
||||
"id": "request-source",
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "request",
|
||||
"name": "method"
|
||||
},
|
||||
{
|
||||
"source": "request",
|
||||
"name": "remote-addr"
|
||||
}
|
||||
],
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"include-command-output-in-response": true
|
||||
},
|
||||
{
|
||||
"id": "static-params-ok",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"response-message": "success",
|
||||
"include-command-output-in-response": true,
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "passed"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "warn-on-space",
|
||||
"execute-command": "{{ .Hookecho }} foo",
|
||||
"response-message": "success",
|
||||
"include-command-output-in-response": true,
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "passed"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "issue-471",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"response-message": "success",
|
||||
"trigger-rule":
|
||||
{
|
||||
"or":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "foo"
|
||||
},
|
||||
"type": "value",
|
||||
"value": "bar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "exists"
|
||||
},
|
||||
"type": "value",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "issue-471-and",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"response-message": "success",
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "foo"
|
||||
},
|
||||
"type": "value",
|
||||
"value": "bar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "exists"
|
||||
},
|
||||
"type": "value",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "empty-payload-signature",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
316
test/hooks.yaml.tmpl
Normal file
316
test/hooks.yaml.tmpl
Normal file
|
@ -0,0 +1,316 @@
|
|||
- id: github
|
||||
http-methods:
|
||||
- "Post "
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecret
|
||||
type: payload-hmac-sha1
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: ref
|
||||
type: value
|
||||
value: refs/heads/master
|
||||
include-command-output-in-response: true
|
||||
trigger-rule-mismatch-http-response-code: 400
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
pass-arguments-to-command:
|
||||
- source: payload
|
||||
name: head_commit.id
|
||||
- source: payload
|
||||
name: head_commit.author.email
|
||||
pass-environment-to-command:
|
||||
- source: payload
|
||||
name: head_commit.timestamp
|
||||
command-working-directory: /
|
||||
|
||||
- id: github-multi-sig
|
||||
http-methods:
|
||||
- "Post "
|
||||
trigger-rule:
|
||||
and:
|
||||
- or:
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecretFAIL
|
||||
type: payload-hmac-sha1
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecret
|
||||
type: payload-hmac-sha1
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: ref
|
||||
type: value
|
||||
value: refs/heads/master
|
||||
include-command-output-in-response: true
|
||||
trigger-rule-mismatch-http-response-code: 400
|
||||
trigger-signature-soft-failures: true
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
pass-arguments-to-command:
|
||||
- source: payload
|
||||
name: head_commit.id
|
||||
- source: payload
|
||||
name: head_commit.author.email
|
||||
pass-environment-to-command:
|
||||
- source: payload
|
||||
name: head_commit.timestamp
|
||||
command-working-directory: /
|
||||
|
||||
- id: github-multi-sig-fail
|
||||
http-methods:
|
||||
- "Post "
|
||||
trigger-rule:
|
||||
and:
|
||||
- or:
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecretFAIL
|
||||
type: payload-hmac-sha1
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecret
|
||||
type: payload-hmac-sha1
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: ref
|
||||
type: value
|
||||
value: refs/heads/master
|
||||
include-command-output-in-response: true
|
||||
trigger-rule-mismatch-http-response-code: 400
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
pass-arguments-to-command:
|
||||
- source: payload
|
||||
name: head_commit.id
|
||||
- source: payload
|
||||
name: head_commit.author.email
|
||||
pass-environment-to-command:
|
||||
- source: payload
|
||||
name: head_commit.timestamp
|
||||
command-working-directory: /
|
||||
|
||||
- id: bitbucket
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: payload.canon_url
|
||||
type: value
|
||||
value: https://bitbucket.org
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: payload.repository.absolute_url
|
||||
type: value
|
||||
value: /webhook/testing/
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: payload.commits.0.branch
|
||||
type: value
|
||||
value: master
|
||||
parse-parameters-as-json:
|
||||
- source: payload
|
||||
name: payload
|
||||
trigger-rule-mismatch-http-response-code: 999
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
include-command-output-in-response: false
|
||||
command-working-directory: /
|
||||
|
||||
- id: gitlab
|
||||
trigger-rule:
|
||||
match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: ref
|
||||
type: value
|
||||
value: refs/heads/master
|
||||
pass-arguments-to-command:
|
||||
- source: payload
|
||||
name: commits.0.id
|
||||
- source: payload
|
||||
name: user_name
|
||||
- source: payload
|
||||
name: user_email
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
include-command-output-in-response: true
|
||||
command-working-directory: /
|
||||
|
||||
- id: xml
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
command-working-directory: /
|
||||
response-message: success
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: app.users.user.0.-name
|
||||
value: Jeff
|
||||
- match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: "app.messages.message.#text"
|
||||
value: "Hello!!"
|
||||
|
||||
- id: txt-raw
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
command-working-directory: /
|
||||
include-command-output-in-response: true
|
||||
pass-arguments-to-command:
|
||||
- source: raw-request-body
|
||||
|
||||
- id: sendgrid
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
command-working-directory: /
|
||||
response-message: success
|
||||
trigger-rule:
|
||||
match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: root.0.event
|
||||
value: processed
|
||||
|
||||
- id: sendgrid/dir
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
command-working-directory: /
|
||||
response-message: success
|
||||
trigger-rule:
|
||||
match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: root.0.event
|
||||
value: it worked!
|
||||
|
||||
- id: plex
|
||||
trigger-rule:
|
||||
match:
|
||||
type: value
|
||||
parameter:
|
||||
source: payload
|
||||
name: payload.event
|
||||
value: media.play
|
||||
parse-parameters-as-json:
|
||||
- source: payload
|
||||
name: payload
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
command-working-directory: /
|
||||
|
||||
- id: capture-command-output-on-success-not-by-default
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=0
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
|
||||
- id: capture-command-output-on-success-yes-with-flag
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=0
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
|
||||
- id: capture-command-output-on-error-not-by-default
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=1
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
|
||||
- id: capture-command-output-on-error-yes-with-extra-flag
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=1
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
include-command-output-in-response-on-error: true
|
||||
|
||||
- id: request-source
|
||||
pass-arguments-to-command:
|
||||
- source: request
|
||||
name: method
|
||||
- source: request
|
||||
name: remote-addr
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
|
||||
- id: static-params-ok
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: passed
|
||||
|
||||
- id: warn-on-space
|
||||
execute-command: '{{ .Hookecho }} foo'
|
||||
include-command-output-in-response: true
|
||||
|
||||
- id: issue-471
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
trigger-rule:
|
||||
or:
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: foo
|
||||
type: value
|
||||
value: bar
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: exists
|
||||
type: value
|
||||
value: 1
|
||||
|
||||
- id: issue-471-and
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: foo
|
||||
type: value
|
||||
value: bar
|
||||
- match:
|
||||
parameter:
|
||||
source: payload
|
||||
name: exists
|
||||
type: value
|
||||
value: 1
|
||||
|
||||
- id: empty-payload-signature
|
||||
include-command-output-in-response: true
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
command-working-directory: /
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
secret: mysecret
|
||||
type: payload-hmac-sha1
|
30
testutils.go
Normal file
30
testutils.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
func prepareTestSocket(_ string) (socketPath string, transport *http.Transport, cleanup func(), err error) {
|
||||
tmp, err := ioutil.TempDir("", "webhook-socket-")
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
cleanup = func() { os.RemoveAll(tmp) }
|
||||
socketPath = path.Join(tmp, "webhook.sock")
|
||||
socketDialer := &net.Dialer{}
|
||||
transport = &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return socketDialer.DialContext(ctx, "unix", socketPath)
|
||||
},
|
||||
}
|
||||
|
||||
return socketPath, transport, cleanup, nil
|
||||
}
|
22
testutils_windows.go
Normal file
22
testutils_windows.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Microsoft/go-winio"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func prepareTestSocket(hookTmpl string) (socketPath string, transport *http.Transport, cleanup func(), err error) {
|
||||
socketPath = "\\\\.\\pipe\\webhook-" + hookTmpl
|
||||
transport = &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return winio.DialPipeContext(ctx, socketPath)
|
||||
},
|
||||
}
|
||||
|
||||
return socketPath, transport, nil, nil
|
||||
}
|
85
tls.go
Normal file
85
tls.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func writeTLSSupportedCipherStrings(w io.Writer, min uint16) error {
|
||||
for _, c := range tls.CipherSuites() {
|
||||
var found bool
|
||||
|
||||
for _, v := range c.SupportedVersions {
|
||||
if v >= min {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := w.Write([]byte(c.Name + "\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTLSMinVersion converts a version string into a TLS version ID.
|
||||
func getTLSMinVersion(v string) uint16 {
|
||||
switch v {
|
||||
case "1.0":
|
||||
return tls.VersionTLS10
|
||||
case "1.1":
|
||||
return tls.VersionTLS11
|
||||
case "1.2", "":
|
||||
return tls.VersionTLS12
|
||||
case "1.3":
|
||||
return tls.VersionTLS13
|
||||
default:
|
||||
log.Fatalln("error: unknown minimum TLS version:", v)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// getTLSCipherSuites converts a comma separated list of cipher suites into a
|
||||
// slice of TLS cipher suite IDs.
|
||||
func getTLSCipherSuites(v string) []uint16 {
|
||||
supported := tls.CipherSuites()
|
||||
|
||||
if v == "" {
|
||||
suites := make([]uint16, len(supported))
|
||||
|
||||
for _, cs := range supported {
|
||||
suites = append(suites, cs.ID)
|
||||
}
|
||||
|
||||
return suites
|
||||
}
|
||||
|
||||
var found bool
|
||||
txts := strings.Split(v, ",")
|
||||
suites := make([]uint16, len(txts))
|
||||
|
||||
for _, want := range txts {
|
||||
found = false
|
||||
|
||||
for _, cs := range supported {
|
||||
if want == cs.Name {
|
||||
suites = append(suites, cs.ID)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Fatalln("error: unknown TLS cipher suite:", want)
|
||||
}
|
||||
}
|
||||
|
||||
return suites
|
||||
}
|
1
vendor/github.com/Microsoft/go-winio/.gitattributes
generated
vendored
Normal file
1
vendor/github.com/Microsoft/go-winio/.gitattributes
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
10
vendor/github.com/Microsoft/go-winio/.gitignore
generated
vendored
Normal file
10
vendor/github.com/Microsoft/go-winio/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
.vscode/
|
||||
|
||||
*.exe
|
||||
|
||||
# testing
|
||||
testdata
|
||||
|
||||
# go workspaces
|
||||
go.work
|
||||
go.work.sum
|
147
vendor/github.com/Microsoft/go-winio/.golangci.yml
generated
vendored
Normal file
147
vendor/github.com/Microsoft/go-winio/.golangci.yml
generated
vendored
Normal file
|
@ -0,0 +1,147 @@
|
|||
linters:
|
||||
enable:
|
||||
# style
|
||||
- containedctx # struct contains a context
|
||||
- dupl # duplicate code
|
||||
- errname # erorrs are named correctly
|
||||
- nolintlint # "//nolint" directives are properly explained
|
||||
- revive # golint replacement
|
||||
- unconvert # unnecessary conversions
|
||||
- wastedassign
|
||||
|
||||
# bugs, performance, unused, etc ...
|
||||
- contextcheck # function uses a non-inherited context
|
||||
- errorlint # errors not wrapped for 1.13
|
||||
- exhaustive # check exhaustiveness of enum switch statements
|
||||
- gofmt # files are gofmt'ed
|
||||
- gosec # security
|
||||
- nilerr # returns nil even with non-nil error
|
||||
- thelper # test helpers without t.Helper()
|
||||
- unparam # unused function params
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- pkg/etw/sample
|
||||
|
||||
exclude-rules:
|
||||
# err is very often shadowed in nested scopes
|
||||
- linters:
|
||||
- govet
|
||||
text: '^shadow: declaration of "err" shadows declaration'
|
||||
|
||||
# ignore long lines for skip autogen directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^line-length-limit: "
|
||||
source: "^//(go:generate|sys) "
|
||||
|
||||
#TODO: remove after upgrading to go1.18
|
||||
# ignore comment spacing for nolint and sys directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^comment-spacings: no space between comment delimiter and comment text"
|
||||
source: "//(cspell:|nolint:|sys |todo)"
|
||||
|
||||
# not on go 1.18 yet, so no any
|
||||
- linters:
|
||||
- revive
|
||||
text: "^use-any: since GO 1.18 'interface{}' can be replaced by 'any'"
|
||||
|
||||
# allow unjustified ignores of error checks in defer statements
|
||||
- linters:
|
||||
- nolintlint
|
||||
text: "^directive `//nolint:errcheck` should provide explanation"
|
||||
source: '^\s*defer '
|
||||
|
||||
# allow unjustified ignores of error lints for io.EOF
|
||||
- linters:
|
||||
- nolintlint
|
||||
text: "^directive `//nolint:errorlint` should provide explanation"
|
||||
source: '[=|!]= io.EOF'
|
||||
|
||||
|
||||
linters-settings:
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
# struct order is often for Win32 compat
|
||||
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
|
||||
- fieldalignment
|
||||
nolintlint:
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
revive:
|
||||
# revive is more configurable than static check, so likely the preferred alternative to static-check
|
||||
# (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997)
|
||||
enable-all-rules:
|
||||
true
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
|
||||
rules:
|
||||
# rules with required arguments
|
||||
- name: argument-limit
|
||||
disabled: true
|
||||
- name: banned-characters
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
disabled: true
|
||||
- name: file-header
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: function-result-limit
|
||||
disabled: true
|
||||
- name: max-public-structs
|
||||
disabled: true
|
||||
# geneally annoying rules
|
||||
- name: add-constant # complains about any and all strings and integers
|
||||
disabled: true
|
||||
- name: confusing-naming # we frequently use "Foo()" and "foo()" together
|
||||
disabled: true
|
||||
- name: flag-parameter # excessive, and a common idiom we use
|
||||
disabled: true
|
||||
- name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead
|
||||
disabled: true
|
||||
# general config
|
||||
- name: line-length-limit
|
||||
arguments:
|
||||
- 140
|
||||
- name: var-naming
|
||||
arguments:
|
||||
- []
|
||||
- - CID
|
||||
- CRI
|
||||
- CTRD
|
||||
- DACL
|
||||
- DLL
|
||||
- DOS
|
||||
- ETW
|
||||
- FSCTL
|
||||
- GCS
|
||||
- GMSA
|
||||
- HCS
|
||||
- HV
|
||||
- IO
|
||||
- LCOW
|
||||
- LDAP
|
||||
- LPAC
|
||||
- LTSC
|
||||
- MMIO
|
||||
- NT
|
||||
- OCI
|
||||
- PMEM
|
||||
- PWSH
|
||||
- RX
|
||||
- SACl
|
||||
- SID
|
||||
- SMB
|
||||
- TX
|
||||
- VHD
|
||||
- VHDX
|
||||
- VMID
|
||||
- VPCI
|
||||
- WCOW
|
||||
- WIM
|
1
vendor/github.com/Microsoft/go-winio/CODEOWNERS
generated
vendored
Normal file
1
vendor/github.com/Microsoft/go-winio/CODEOWNERS
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* @microsoft/containerplat
|
22
vendor/github.com/Microsoft/go-winio/LICENSE
generated
vendored
Normal file
22
vendor/github.com/Microsoft/go-winio/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Microsoft
|
||||
|
||||
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.
|
||||
|
89
vendor/github.com/Microsoft/go-winio/README.md
generated
vendored
Normal file
89
vendor/github.com/Microsoft/go-winio/README.md
generated
vendored
Normal file
|
@ -0,0 +1,89 @@
|
|||
# go-winio [](https://github.com/microsoft/go-winio/actions/workflows/ci.yml)
|
||||
|
||||
This repository contains utilities for efficiently performing Win32 IO operations in
|
||||
Go. Currently, this is focused on accessing named pipes and other file handles, and
|
||||
for using named pipes as a net transport.
|
||||
|
||||
This code relies on IO completion ports to avoid blocking IO on system threads, allowing Go
|
||||
to reuse the thread to schedule another goroutine. This limits support to Windows Vista and
|
||||
newer operating systems. This is similar to the implementation of network sockets in Go's net
|
||||
package.
|
||||
|
||||
Please see the LICENSE file for licensing information.
|
||||
|
||||
## Contributing
|
||||
|
||||
This project welcomes contributions and suggestions.
|
||||
Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that
|
||||
you have the right to, and actually do, grant us the rights to use your contribution.
|
||||
For details, visit [Microsoft CLA](https://cla.microsoft.com).
|
||||
|
||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to
|
||||
provide a CLA and decorate the PR appropriately (e.g., label, comment).
|
||||
Simply follow the instructions provided by the bot.
|
||||
You will only need to do this once across all repos using our CLA.
|
||||
|
||||
Additionally, the pull request pipeline requires the following steps to be performed before
|
||||
mergining.
|
||||
|
||||
### Code Sign-Off
|
||||
|
||||
We require that contributors sign their commits using [`git commit --signoff`][git-commit-s]
|
||||
to certify they either authored the work themselves or otherwise have permission to use it in this project.
|
||||
|
||||
A range of commits can be signed off using [`git rebase --signoff`][git-rebase-s].
|
||||
|
||||
Please see [the developer certificate](https://developercertificate.org) for more info,
|
||||
as well as to make sure that you can attest to the rules listed.
|
||||
Our CI uses the DCO Github app to ensure that all commits in a given PR are signed-off.
|
||||
|
||||
### Linting
|
||||
|
||||
Code must pass a linting stage, which uses [`golangci-lint`][lint].
|
||||
The linting settings are stored in [`.golangci.yaml`](./.golangci.yaml), and can be run
|
||||
automatically with VSCode by adding the following to your workspace or folder settings:
|
||||
|
||||
```json
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintOnSave": "package",
|
||||
```
|
||||
|
||||
Additional editor [integrations options are also available][lint-ide].
|
||||
|
||||
Alternatively, `golangci-lint` can be [installed locally][lint-install] and run from the repo root:
|
||||
|
||||
```shell
|
||||
# use . or specify a path to only lint a package
|
||||
# to show all lint errors, use flags "--max-issues-per-linter=0 --max-same-issues=0"
|
||||
> golangci-lint run ./...
|
||||
```
|
||||
|
||||
### Go Generate
|
||||
|
||||
The pipeline checks that auto-generated code, via `go generate`, are up to date.
|
||||
|
||||
This can be done for the entire repo:
|
||||
|
||||
```shell
|
||||
> go generate ./...
|
||||
```
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
## Special Thanks
|
||||
|
||||
Thanks to [natefinch][natefinch] for the inspiration for this library.
|
||||
See [npipe](https://github.com/natefinch/npipe) for another named pipe implementation.
|
||||
|
||||
[lint]: https://golangci-lint.run/
|
||||
[lint-ide]: https://golangci-lint.run/usage/integrations/#editor-integration
|
||||
[lint-install]: https://golangci-lint.run/usage/install/#local-installation
|
||||
|
||||
[git-commit-s]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s
|
||||
[git-rebase-s]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---signoff
|
||||
|
||||
[natefinch]: https://github.com/natefinch
|
41
vendor/github.com/Microsoft/go-winio/SECURITY.md
generated
vendored
Normal file
41
vendor/github.com/Microsoft/go-winio/SECURITY.md
generated
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.7 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
287
vendor/github.com/Microsoft/go-winio/backup.go
generated
vendored
Normal file
287
vendor/github.com/Microsoft/go-winio/backup.go
generated
vendored
Normal file
|
@ -0,0 +1,287 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/Microsoft/go-winio/internal/fs"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
|
||||
//sys backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
|
||||
|
||||
const (
|
||||
BackupData = uint32(iota + 1)
|
||||
BackupEaData
|
||||
BackupSecurity
|
||||
BackupAlternateData
|
||||
BackupLink
|
||||
BackupPropertyData
|
||||
BackupObjectId //revive:disable-line:var-naming ID, not Id
|
||||
BackupReparseData
|
||||
BackupSparseBlock
|
||||
BackupTxfsData
|
||||
)
|
||||
|
||||
const (
|
||||
StreamSparseAttributes = uint32(8)
|
||||
)
|
||||
|
||||
//nolint:revive // var-naming: ALL_CAPS
|
||||
const (
|
||||
WRITE_DAC = windows.WRITE_DAC
|
||||
WRITE_OWNER = windows.WRITE_OWNER
|
||||
ACCESS_SYSTEM_SECURITY = windows.ACCESS_SYSTEM_SECURITY
|
||||
)
|
||||
|
||||
// BackupHeader represents a backup stream of a file.
|
||||
type BackupHeader struct {
|
||||
//revive:disable-next-line:var-naming ID, not Id
|
||||
Id uint32 // The backup stream ID
|
||||
Attributes uint32 // Stream attributes
|
||||
Size int64 // The size of the stream in bytes
|
||||
Name string // The name of the stream (for BackupAlternateData only).
|
||||
Offset int64 // The offset of the stream in the file (for BackupSparseBlock only).
|
||||
}
|
||||
|
||||
type win32StreamID struct {
|
||||
StreamID uint32
|
||||
Attributes uint32
|
||||
Size uint64
|
||||
NameSize uint32
|
||||
}
|
||||
|
||||
// BackupStreamReader reads from a stream produced by the BackupRead Win32 API and produces a series
|
||||
// of BackupHeader values.
|
||||
type BackupStreamReader struct {
|
||||
r io.Reader
|
||||
bytesLeft int64
|
||||
}
|
||||
|
||||
// NewBackupStreamReader produces a BackupStreamReader from any io.Reader.
|
||||
func NewBackupStreamReader(r io.Reader) *BackupStreamReader {
|
||||
return &BackupStreamReader{r, 0}
|
||||
}
|
||||
|
||||
// Next returns the next backup stream and prepares for calls to Read(). It skips the remainder of the current stream if
|
||||
// it was not completely read.
|
||||
func (r *BackupStreamReader) Next() (*BackupHeader, error) {
|
||||
if r.bytesLeft > 0 { //nolint:nestif // todo: flatten this
|
||||
if s, ok := r.r.(io.Seeker); ok {
|
||||
// Make sure Seek on io.SeekCurrent sometimes succeeds
|
||||
// before trying the actual seek.
|
||||
if _, err := s.Seek(0, io.SeekCurrent); err == nil {
|
||||
if _, err = s.Seek(r.bytesLeft, io.SeekCurrent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.bytesLeft = 0
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(io.Discard, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var wsi win32StreamID
|
||||
if err := binary.Read(r.r, binary.LittleEndian, &wsi); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr := &BackupHeader{
|
||||
Id: wsi.StreamID,
|
||||
Attributes: wsi.Attributes,
|
||||
Size: int64(wsi.Size),
|
||||
}
|
||||
if wsi.NameSize != 0 {
|
||||
name := make([]uint16, int(wsi.NameSize/2))
|
||||
if err := binary.Read(r.r, binary.LittleEndian, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr.Name = windows.UTF16ToString(name)
|
||||
}
|
||||
if wsi.StreamID == BackupSparseBlock {
|
||||
if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr.Size -= 8
|
||||
}
|
||||
r.bytesLeft = hdr.Size
|
||||
return hdr, nil
|
||||
}
|
||||
|
||||
// Read reads from the current backup stream.
|
||||
func (r *BackupStreamReader) Read(b []byte) (int, error) {
|
||||
if r.bytesLeft == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if int64(len(b)) > r.bytesLeft {
|
||||
b = b[:r.bytesLeft]
|
||||
}
|
||||
n, err := r.r.Read(b)
|
||||
r.bytesLeft -= int64(n)
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
} else if r.bytesLeft == 0 && err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// BackupStreamWriter writes a stream compatible with the BackupWrite Win32 API.
|
||||
type BackupStreamWriter struct {
|
||||
w io.Writer
|
||||
bytesLeft int64
|
||||
}
|
||||
|
||||
// NewBackupStreamWriter produces a BackupStreamWriter on top of an io.Writer.
|
||||
func NewBackupStreamWriter(w io.Writer) *BackupStreamWriter {
|
||||
return &BackupStreamWriter{w, 0}
|
||||
}
|
||||
|
||||
// WriteHeader writes the next backup stream header and prepares for calls to Write().
|
||||
func (w *BackupStreamWriter) WriteHeader(hdr *BackupHeader) error {
|
||||
if w.bytesLeft != 0 {
|
||||
return fmt.Errorf("missing %d bytes", w.bytesLeft)
|
||||
}
|
||||
name := utf16.Encode([]rune(hdr.Name))
|
||||
wsi := win32StreamID{
|
||||
StreamID: hdr.Id,
|
||||
Attributes: hdr.Attributes,
|
||||
Size: uint64(hdr.Size),
|
||||
NameSize: uint32(len(name) * 2),
|
||||
}
|
||||
if hdr.Id == BackupSparseBlock {
|
||||
// Include space for the int64 block offset
|
||||
wsi.Size += 8
|
||||
}
|
||||
if err := binary.Write(w.w, binary.LittleEndian, &wsi); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(name) != 0 {
|
||||
if err := binary.Write(w.w, binary.LittleEndian, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if hdr.Id == BackupSparseBlock {
|
||||
if err := binary.Write(w.w, binary.LittleEndian, hdr.Offset); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.bytesLeft = hdr.Size
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes to the current backup stream.
|
||||
func (w *BackupStreamWriter) Write(b []byte) (int, error) {
|
||||
if w.bytesLeft < int64(len(b)) {
|
||||
return 0, fmt.Errorf("too many bytes by %d", int64(len(b))-w.bytesLeft)
|
||||
}
|
||||
n, err := w.w.Write(b)
|
||||
w.bytesLeft -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// BackupFileReader provides an io.ReadCloser interface on top of the BackupRead Win32 API.
|
||||
type BackupFileReader struct {
|
||||
f *os.File
|
||||
includeSecurity bool
|
||||
ctx uintptr
|
||||
}
|
||||
|
||||
// NewBackupFileReader returns a new BackupFileReader from a file handle. If includeSecurity is true,
|
||||
// Read will attempt to read the security descriptor of the file.
|
||||
func NewBackupFileReader(f *os.File, includeSecurity bool) *BackupFileReader {
|
||||
r := &BackupFileReader{f, includeSecurity, 0}
|
||||
return r
|
||||
}
|
||||
|
||||
// Read reads a backup stream from the file by calling the Win32 API BackupRead().
|
||||
func (r *BackupFileReader) Read(b []byte) (int, error) {
|
||||
var bytesRead uint32
|
||||
err := backupRead(windows.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "BackupRead", Path: r.f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(r.f)
|
||||
if bytesRead == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return int(bytesRead), nil
|
||||
}
|
||||
|
||||
// Close frees Win32 resources associated with the BackupFileReader. It does not close
|
||||
// the underlying file.
|
||||
func (r *BackupFileReader) Close() error {
|
||||
if r.ctx != 0 {
|
||||
_ = backupRead(windows.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
|
||||
runtime.KeepAlive(r.f)
|
||||
r.ctx = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackupFileWriter provides an io.WriteCloser interface on top of the BackupWrite Win32 API.
|
||||
type BackupFileWriter struct {
|
||||
f *os.File
|
||||
includeSecurity bool
|
||||
ctx uintptr
|
||||
}
|
||||
|
||||
// NewBackupFileWriter returns a new BackupFileWriter from a file handle. If includeSecurity is true,
|
||||
// Write() will attempt to restore the security descriptor from the stream.
|
||||
func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter {
|
||||
w := &BackupFileWriter{f, includeSecurity, 0}
|
||||
return w
|
||||
}
|
||||
|
||||
// Write restores a portion of the file using the provided backup stream.
|
||||
func (w *BackupFileWriter) Write(b []byte) (int, error) {
|
||||
var bytesWritten uint32
|
||||
err := backupWrite(windows.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "BackupWrite", Path: w.f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(w.f)
|
||||
if int(bytesWritten) != len(b) {
|
||||
return int(bytesWritten), errors.New("not all bytes could be written")
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// Close frees Win32 resources associated with the BackupFileWriter. It does not
|
||||
// close the underlying file.
|
||||
func (w *BackupFileWriter) Close() error {
|
||||
if w.ctx != 0 {
|
||||
_ = backupWrite(windows.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
|
||||
runtime.KeepAlive(w.f)
|
||||
w.ctx = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OpenForBackup opens a file or directory, potentially skipping access checks if the backup
|
||||
// or restore privileges have been acquired.
|
||||
//
|
||||
// If the file opened was a directory, it cannot be used with Readdir().
|
||||
func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) {
|
||||
h, err := fs.CreateFile(path,
|
||||
fs.AccessMask(access),
|
||||
fs.FileShareMode(share),
|
||||
nil,
|
||||
fs.FileCreationDisposition(createmode),
|
||||
fs.FILE_FLAG_BACKUP_SEMANTICS|fs.FILE_FLAG_OPEN_REPARSE_POINT,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
err = &os.PathError{Op: "open", Path: path, Err: err}
|
||||
return nil, err
|
||||
}
|
||||
return os.NewFile(uintptr(h), path), nil
|
||||
}
|
22
vendor/github.com/Microsoft/go-winio/doc.go
generated
vendored
Normal file
22
vendor/github.com/Microsoft/go-winio/doc.go
generated
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
// This package provides utilities for efficiently performing Win32 IO operations in Go.
|
||||
// Currently, this package is provides support for genreal IO and management of
|
||||
// - named pipes
|
||||
// - files
|
||||
// - [Hyper-V sockets]
|
||||
//
|
||||
// This code is similar to Go's [net] package, and uses IO completion ports to avoid
|
||||
// blocking IO on system threads, allowing Go to reuse the thread to schedule other goroutines.
|
||||
//
|
||||
// This limits support to Windows Vista and newer operating systems.
|
||||
//
|
||||
// Additionally, this package provides support for:
|
||||
// - creating and managing GUIDs
|
||||
// - writing to [ETW]
|
||||
// - opening and manageing VHDs
|
||||
// - parsing [Windows Image files]
|
||||
// - auto-generating Win32 API code
|
||||
//
|
||||
// [Hyper-V sockets]: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service
|
||||
// [ETW]: https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw-
|
||||
// [Windows Image files]: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/work-with-windows-images
|
||||
package winio
|
137
vendor/github.com/Microsoft/go-winio/ea.go
generated
vendored
Normal file
137
vendor/github.com/Microsoft/go-winio/ea.go
generated
vendored
Normal file
|
@ -0,0 +1,137 @@
|
|||
package winio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type fileFullEaInformation struct {
|
||||
NextEntryOffset uint32
|
||||
Flags uint8
|
||||
NameLength uint8
|
||||
ValueLength uint16
|
||||
}
|
||||
|
||||
var (
|
||||
fileFullEaInformationSize = binary.Size(&fileFullEaInformation{})
|
||||
|
||||
errInvalidEaBuffer = errors.New("invalid extended attribute buffer")
|
||||
errEaNameTooLarge = errors.New("extended attribute name too large")
|
||||
errEaValueTooLarge = errors.New("extended attribute value too large")
|
||||
)
|
||||
|
||||
// ExtendedAttribute represents a single Windows EA.
|
||||
type ExtendedAttribute struct {
|
||||
Name string
|
||||
Value []byte
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) {
|
||||
var info fileFullEaInformation
|
||||
err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info)
|
||||
if err != nil {
|
||||
err = errInvalidEaBuffer
|
||||
return ea, nb, err
|
||||
}
|
||||
|
||||
nameOffset := fileFullEaInformationSize
|
||||
nameLen := int(info.NameLength)
|
||||
valueOffset := nameOffset + int(info.NameLength) + 1
|
||||
valueLen := int(info.ValueLength)
|
||||
nextOffset := int(info.NextEntryOffset)
|
||||
if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) {
|
||||
err = errInvalidEaBuffer
|
||||
return ea, nb, err
|
||||
}
|
||||
|
||||
ea.Name = string(b[nameOffset : nameOffset+nameLen])
|
||||
ea.Value = b[valueOffset : valueOffset+valueLen]
|
||||
ea.Flags = info.Flags
|
||||
if info.NextEntryOffset != 0 {
|
||||
nb = b[info.NextEntryOffset:]
|
||||
}
|
||||
return ea, nb, err
|
||||
}
|
||||
|
||||
// DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION
|
||||
// buffer retrieved from BackupRead, ZwQueryEaFile, etc.
|
||||
func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) {
|
||||
for len(b) != 0 {
|
||||
ea, nb, err := parseEa(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eas = append(eas, ea)
|
||||
b = nb
|
||||
}
|
||||
return eas, err
|
||||
}
|
||||
|
||||
func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
|
||||
if int(uint8(len(ea.Name))) != len(ea.Name) {
|
||||
return errEaNameTooLarge
|
||||
}
|
||||
if int(uint16(len(ea.Value))) != len(ea.Value) {
|
||||
return errEaValueTooLarge
|
||||
}
|
||||
entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value))
|
||||
withPadding := (entrySize + 3) &^ 3
|
||||
nextOffset := uint32(0)
|
||||
if !last {
|
||||
nextOffset = withPadding
|
||||
}
|
||||
info := fileFullEaInformation{
|
||||
NextEntryOffset: nextOffset,
|
||||
Flags: ea.Flags,
|
||||
NameLength: uint8(len(ea.Name)),
|
||||
ValueLength: uint16(len(ea.Value)),
|
||||
}
|
||||
|
||||
err := binary.Write(buf, binary.LittleEndian, &info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte(ea.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = buf.WriteByte(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write(ea.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION
|
||||
// buffer for use with BackupWrite, ZwSetEaFile, etc.
|
||||
func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
for i := range eas {
|
||||
last := false
|
||||
if i == len(eas)-1 {
|
||||
last = true
|
||||
}
|
||||
|
||||
err := writeEa(&buf, &eas[i], last)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
320
vendor/github.com/Microsoft/go-winio/file.go
generated
vendored
Normal file
320
vendor/github.com/Microsoft/go-winio/file.go
generated
vendored
Normal file
|
@ -0,0 +1,320 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx
|
||||
//sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort
|
||||
//sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
|
||||
//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
|
||||
//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
|
||||
|
||||
var (
|
||||
ErrFileClosed = errors.New("file has already been closed")
|
||||
ErrTimeout = &timeoutError{}
|
||||
)
|
||||
|
||||
type timeoutError struct{}
|
||||
|
||||
func (*timeoutError) Error() string { return "i/o timeout" }
|
||||
func (*timeoutError) Timeout() bool { return true }
|
||||
func (*timeoutError) Temporary() bool { return true }
|
||||
|
||||
type timeoutChan chan struct{}
|
||||
|
||||
var ioInitOnce sync.Once
|
||||
var ioCompletionPort windows.Handle
|
||||
|
||||
// ioResult contains the result of an asynchronous IO operation.
|
||||
type ioResult struct {
|
||||
bytes uint32
|
||||
err error
|
||||
}
|
||||
|
||||
// ioOperation represents an outstanding asynchronous Win32 IO.
|
||||
type ioOperation struct {
|
||||
o windows.Overlapped
|
||||
ch chan ioResult
|
||||
}
|
||||
|
||||
func initIO() {
|
||||
h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ioCompletionPort = h
|
||||
go ioCompletionProcessor(h)
|
||||
}
|
||||
|
||||
// win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall.
|
||||
// It takes ownership of this handle and will close it if it is garbage collected.
|
||||
type win32File struct {
|
||||
handle windows.Handle
|
||||
wg sync.WaitGroup
|
||||
wgLock sync.RWMutex
|
||||
closing atomic.Bool
|
||||
socket bool
|
||||
readDeadline deadlineHandler
|
||||
writeDeadline deadlineHandler
|
||||
}
|
||||
|
||||
type deadlineHandler struct {
|
||||
setLock sync.Mutex
|
||||
channel timeoutChan
|
||||
channelLock sync.RWMutex
|
||||
timer *time.Timer
|
||||
timedout atomic.Bool
|
||||
}
|
||||
|
||||
// makeWin32File makes a new win32File from an existing file handle.
|
||||
func makeWin32File(h windows.Handle) (*win32File, error) {
|
||||
f := &win32File{handle: h}
|
||||
ioInitOnce.Do(initIO)
|
||||
_, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = setFileCompletionNotificationModes(h, windows.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS|windows.FILE_SKIP_SET_EVENT_ON_HANDLE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.readDeadline.channel = make(timeoutChan)
|
||||
f.writeDeadline.channel = make(timeoutChan)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Deprecated: use NewOpenFile instead.
|
||||
func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
|
||||
return NewOpenFile(windows.Handle(h))
|
||||
}
|
||||
|
||||
func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) {
|
||||
// If we return the result of makeWin32File directly, it can result in an
|
||||
// interface-wrapped nil, rather than a nil interface value.
|
||||
f, err := makeWin32File(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// closeHandle closes the resources associated with a Win32 handle.
|
||||
func (f *win32File) closeHandle() {
|
||||
f.wgLock.Lock()
|
||||
// Atomically set that we are closing, releasing the resources only once.
|
||||
if !f.closing.Swap(true) {
|
||||
f.wgLock.Unlock()
|
||||
// cancel all IO and wait for it to complete
|
||||
_ = cancelIoEx(f.handle, nil)
|
||||
f.wg.Wait()
|
||||
// at this point, no new IO can start
|
||||
windows.Close(f.handle)
|
||||
f.handle = 0
|
||||
} else {
|
||||
f.wgLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes a win32File.
|
||||
func (f *win32File) Close() error {
|
||||
f.closeHandle()
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsClosed checks if the file has been closed.
|
||||
func (f *win32File) IsClosed() bool {
|
||||
return f.closing.Load()
|
||||
}
|
||||
|
||||
// prepareIO prepares for a new IO operation.
|
||||
// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning.
|
||||
func (f *win32File) prepareIO() (*ioOperation, error) {
|
||||
f.wgLock.RLock()
|
||||
if f.closing.Load() {
|
||||
f.wgLock.RUnlock()
|
||||
return nil, ErrFileClosed
|
||||
}
|
||||
f.wg.Add(1)
|
||||
f.wgLock.RUnlock()
|
||||
c := &ioOperation{}
|
||||
c.ch = make(chan ioResult)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ioCompletionProcessor processes completed async IOs forever.
|
||||
func ioCompletionProcessor(h windows.Handle) {
|
||||
for {
|
||||
var bytes uint32
|
||||
var key uintptr
|
||||
var op *ioOperation
|
||||
err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE)
|
||||
if op == nil {
|
||||
panic(err)
|
||||
}
|
||||
op.ch <- ioResult{bytes, err}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: helsaawy - create an asyncIO version that takes a context
|
||||
|
||||
// asyncIO processes the return value from ReadFile or WriteFile, blocking until
|
||||
// the operation has actually completed.
|
||||
func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) {
|
||||
if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
|
||||
return int(bytes), err
|
||||
}
|
||||
|
||||
if f.closing.Load() {
|
||||
_ = cancelIoEx(f.handle, &c.o)
|
||||
}
|
||||
|
||||
var timeout timeoutChan
|
||||
if d != nil {
|
||||
d.channelLock.Lock()
|
||||
timeout = d.channel
|
||||
d.channelLock.Unlock()
|
||||
}
|
||||
|
||||
var r ioResult
|
||||
select {
|
||||
case r = <-c.ch:
|
||||
err = r.err
|
||||
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
if f.closing.Load() {
|
||||
err = ErrFileClosed
|
||||
}
|
||||
} else if err != nil && f.socket {
|
||||
// err is from Win32. Query the overlapped structure to get the winsock error.
|
||||
var bytes, flags uint32
|
||||
err = wsaGetOverlappedResult(f.handle, &c.o, &bytes, false, &flags)
|
||||
}
|
||||
case <-timeout:
|
||||
_ = cancelIoEx(f.handle, &c.o)
|
||||
r = <-c.ch
|
||||
err = r.err
|
||||
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
err = ErrTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// runtime.KeepAlive is needed, as c is passed via native
|
||||
// code to ioCompletionProcessor, c must remain alive
|
||||
// until the channel read is complete.
|
||||
// todo: (de)allocate *ioOperation via win32 heap functions, instead of needing to KeepAlive?
|
||||
runtime.KeepAlive(c)
|
||||
return int(r.bytes), err
|
||||
}
|
||||
|
||||
// Read reads from a file handle.
|
||||
func (f *win32File) Read(b []byte) (int, error) {
|
||||
c, err := f.prepareIO()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.wg.Done()
|
||||
|
||||
if f.readDeadline.timedout.Load() {
|
||||
return 0, ErrTimeout
|
||||
}
|
||||
|
||||
var bytes uint32
|
||||
err = windows.ReadFile(f.handle, b, &bytes, &c.o)
|
||||
n, err := f.asyncIO(c, &f.readDeadline, bytes, err)
|
||||
runtime.KeepAlive(b)
|
||||
|
||||
// Handle EOF conditions.
|
||||
if err == nil && n == 0 && len(b) != 0 {
|
||||
return 0, io.EOF
|
||||
} else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
|
||||
return 0, io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Write writes to a file handle.
|
||||
func (f *win32File) Write(b []byte) (int, error) {
|
||||
c, err := f.prepareIO()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.wg.Done()
|
||||
|
||||
if f.writeDeadline.timedout.Load() {
|
||||
return 0, ErrTimeout
|
||||
}
|
||||
|
||||
var bytes uint32
|
||||
err = windows.WriteFile(f.handle, b, &bytes, &c.o)
|
||||
n, err := f.asyncIO(c, &f.writeDeadline, bytes, err)
|
||||
runtime.KeepAlive(b)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (f *win32File) SetReadDeadline(deadline time.Time) error {
|
||||
return f.readDeadline.set(deadline)
|
||||
}
|
||||
|
||||
func (f *win32File) SetWriteDeadline(deadline time.Time) error {
|
||||
return f.writeDeadline.set(deadline)
|
||||
}
|
||||
|
||||
func (f *win32File) Flush() error {
|
||||
return windows.FlushFileBuffers(f.handle)
|
||||
}
|
||||
|
||||
func (f *win32File) Fd() uintptr {
|
||||
return uintptr(f.handle)
|
||||
}
|
||||
|
||||
func (d *deadlineHandler) set(deadline time.Time) error {
|
||||
d.setLock.Lock()
|
||||
defer d.setLock.Unlock()
|
||||
|
||||
if d.timer != nil {
|
||||
if !d.timer.Stop() {
|
||||
<-d.channel
|
||||
}
|
||||
d.timer = nil
|
||||
}
|
||||
d.timedout.Store(false)
|
||||
|
||||
select {
|
||||
case <-d.channel:
|
||||
d.channelLock.Lock()
|
||||
d.channel = make(chan struct{})
|
||||
d.channelLock.Unlock()
|
||||
default:
|
||||
}
|
||||
|
||||
if deadline.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
timeoutIO := func() {
|
||||
d.timedout.Store(true)
|
||||
close(d.channel)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
duration := deadline.Sub(now)
|
||||
if deadline.After(now) {
|
||||
// Deadline is in the future, set a timer to wait
|
||||
d.timer = time.AfterFunc(duration, timeoutIO)
|
||||
} else {
|
||||
// Deadline is in the past. Cancel all pending IO now.
|
||||
timeoutIO()
|
||||
}
|
||||
return nil
|
||||
}
|
106
vendor/github.com/Microsoft/go-winio/fileinfo.go
generated
vendored
Normal file
106
vendor/github.com/Microsoft/go-winio/fileinfo.go
generated
vendored
Normal file
|
@ -0,0 +1,106 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// FileBasicInfo contains file access time and file attributes information.
|
||||
type FileBasicInfo struct {
|
||||
CreationTime, LastAccessTime, LastWriteTime, ChangeTime windows.Filetime
|
||||
FileAttributes uint32
|
||||
_ uint32 // padding
|
||||
}
|
||||
|
||||
// alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing
|
||||
// uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64
|
||||
// alignment is necessary to pass this as FILE_BASIC_INFO.
|
||||
type alignedFileBasicInfo struct {
|
||||
CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64
|
||||
FileAttributes uint32
|
||||
_ uint32 // padding
|
||||
}
|
||||
|
||||
// GetFileBasicInfo retrieves times and attributes for a file.
|
||||
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
|
||||
bi := &alignedFileBasicInfo{}
|
||||
if err := windows.GetFileInformationByHandleEx(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileBasicInfo,
|
||||
(*byte)(unsafe.Pointer(bi)),
|
||||
uint32(unsafe.Sizeof(*bi)),
|
||||
); err != nil {
|
||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
// Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the
|
||||
// public API of this module. The data may be unnecessarily aligned.
|
||||
return (*FileBasicInfo)(unsafe.Pointer(bi)), nil
|
||||
}
|
||||
|
||||
// SetFileBasicInfo sets times and attributes for a file.
|
||||
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
|
||||
// Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is
|
||||
// suitable to pass to GetFileInformationByHandleEx.
|
||||
biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi))
|
||||
if err := windows.SetFileInformationByHandle(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileBasicInfo,
|
||||
(*byte)(unsafe.Pointer(&biAligned)),
|
||||
uint32(unsafe.Sizeof(biAligned)),
|
||||
); err != nil {
|
||||
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileStandardInfo contains extended information for the file.
|
||||
// FILE_STANDARD_INFO in WinBase.h
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info
|
||||
type FileStandardInfo struct {
|
||||
AllocationSize, EndOfFile int64
|
||||
NumberOfLinks uint32
|
||||
DeletePending, Directory bool
|
||||
}
|
||||
|
||||
// GetFileStandardInfo retrieves ended information for the file.
|
||||
func GetFileStandardInfo(f *os.File) (*FileStandardInfo, error) {
|
||||
si := &FileStandardInfo{}
|
||||
if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()),
|
||||
windows.FileStandardInfo,
|
||||
(*byte)(unsafe.Pointer(si)),
|
||||
uint32(unsafe.Sizeof(*si))); err != nil {
|
||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return si, nil
|
||||
}
|
||||
|
||||
// FileIDInfo contains the volume serial number and file ID for a file. This pair should be
|
||||
// unique on a system.
|
||||
type FileIDInfo struct {
|
||||
VolumeSerialNumber uint64
|
||||
FileID [16]byte
|
||||
}
|
||||
|
||||
// GetFileID retrieves the unique (volume, file ID) pair for a file.
|
||||
func GetFileID(f *os.File) (*FileIDInfo, error) {
|
||||
fileID := &FileIDInfo{}
|
||||
if err := windows.GetFileInformationByHandleEx(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileIdInfo,
|
||||
(*byte)(unsafe.Pointer(fileID)),
|
||||
uint32(unsafe.Sizeof(*fileID)),
|
||||
); err != nil {
|
||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return fileID, nil
|
||||
}
|
582
vendor/github.com/Microsoft/go-winio/hvsock.go
generated
vendored
Normal file
582
vendor/github.com/Microsoft/go-winio/hvsock.go
generated
vendored
Normal file
|
@ -0,0 +1,582 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/Microsoft/go-winio/internal/socket"
|
||||
"github.com/Microsoft/go-winio/pkg/guid"
|
||||
)
|
||||
|
||||
const afHVSock = 34 // AF_HYPERV
|
||||
|
||||
// Well known Service and VM IDs
|
||||
// https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service#vmid-wildcards
|
||||
|
||||
// HvsockGUIDWildcard is the wildcard VmId for accepting connections from all partitions.
|
||||
func HvsockGUIDWildcard() guid.GUID { // 00000000-0000-0000-0000-000000000000
|
||||
return guid.GUID{}
|
||||
}
|
||||
|
||||
// HvsockGUIDBroadcast is the wildcard VmId for broadcasting sends to all partitions.
|
||||
func HvsockGUIDBroadcast() guid.GUID { // ffffffff-ffff-ffff-ffff-ffffffffffff
|
||||
return guid.GUID{
|
||||
Data1: 0xffffffff,
|
||||
Data2: 0xffff,
|
||||
Data3: 0xffff,
|
||||
Data4: [8]uint8{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
|
||||
}
|
||||
}
|
||||
|
||||
// HvsockGUIDLoopback is the Loopback VmId for accepting connections to the same partition as the connector.
|
||||
func HvsockGUIDLoopback() guid.GUID { // e0e16197-dd56-4a10-9195-5ee7a155a838
|
||||
return guid.GUID{
|
||||
Data1: 0xe0e16197,
|
||||
Data2: 0xdd56,
|
||||
Data3: 0x4a10,
|
||||
Data4: [8]uint8{0x91, 0x95, 0x5e, 0xe7, 0xa1, 0x55, 0xa8, 0x38},
|
||||
}
|
||||
}
|
||||
|
||||
// HvsockGUIDSiloHost is the address of a silo's host partition:
|
||||
// - The silo host of a hosted silo is the utility VM.
|
||||
// - The silo host of a silo on a physical host is the physical host.
|
||||
func HvsockGUIDSiloHost() guid.GUID { // 36bd0c5c-7276-4223-88ba-7d03b654c568
|
||||
return guid.GUID{
|
||||
Data1: 0x36bd0c5c,
|
||||
Data2: 0x7276,
|
||||
Data3: 0x4223,
|
||||
Data4: [8]byte{0x88, 0xba, 0x7d, 0x03, 0xb6, 0x54, 0xc5, 0x68},
|
||||
}
|
||||
}
|
||||
|
||||
// HvsockGUIDChildren is the wildcard VmId for accepting connections from the connector's child partitions.
|
||||
func HvsockGUIDChildren() guid.GUID { // 90db8b89-0d35-4f79-8ce9-49ea0ac8b7cd
|
||||
return guid.GUID{
|
||||
Data1: 0x90db8b89,
|
||||
Data2: 0xd35,
|
||||
Data3: 0x4f79,
|
||||
Data4: [8]uint8{0x8c, 0xe9, 0x49, 0xea, 0xa, 0xc8, 0xb7, 0xcd},
|
||||
}
|
||||
}
|
||||
|
||||
// HvsockGUIDParent is the wildcard VmId for accepting connections from the connector's parent partition.
|
||||
// Listening on this VmId accepts connection from:
|
||||
// - Inside silos: silo host partition.
|
||||
// - Inside hosted silo: host of the VM.
|
||||
// - Inside VM: VM host.
|
||||
// - Physical host: Not supported.
|
||||
func HvsockGUIDParent() guid.GUID { // a42e7cda-d03f-480c-9cc2-a4de20abb878
|
||||
return guid.GUID{
|
||||
Data1: 0xa42e7cda,
|
||||
Data2: 0xd03f,
|
||||
Data3: 0x480c,
|
||||
Data4: [8]uint8{0x9c, 0xc2, 0xa4, 0xde, 0x20, 0xab, 0xb8, 0x78},
|
||||
}
|
||||
}
|
||||
|
||||
// hvsockVsockServiceTemplate is the Service GUID used for the VSOCK protocol.
|
||||
func hvsockVsockServiceTemplate() guid.GUID { // 00000000-facb-11e6-bd58-64006a7986d3
|
||||
return guid.GUID{
|
||||
Data2: 0xfacb,
|
||||
Data3: 0x11e6,
|
||||
Data4: [8]uint8{0xbd, 0x58, 0x64, 0x00, 0x6a, 0x79, 0x86, 0xd3},
|
||||
}
|
||||
}
|
||||
|
||||
// An HvsockAddr is an address for a AF_HYPERV socket.
|
||||
type HvsockAddr struct {
|
||||
VMID guid.GUID
|
||||
ServiceID guid.GUID
|
||||
}
|
||||
|
||||
type rawHvsockAddr struct {
|
||||
Family uint16
|
||||
_ uint16
|
||||
VMID guid.GUID
|
||||
ServiceID guid.GUID
|
||||
}
|
||||
|
||||
var _ socket.RawSockaddr = &rawHvsockAddr{}
|
||||
|
||||
// Network returns the address's network name, "hvsock".
|
||||
func (*HvsockAddr) Network() string {
|
||||
return "hvsock"
|
||||
}
|
||||
|
||||
func (addr *HvsockAddr) String() string {
|
||||
return fmt.Sprintf("%s:%s", &addr.VMID, &addr.ServiceID)
|
||||
}
|
||||
|
||||
// VsockServiceID returns an hvsock service ID corresponding to the specified AF_VSOCK port.
|
||||
func VsockServiceID(port uint32) guid.GUID {
|
||||
g := hvsockVsockServiceTemplate() // make a copy
|
||||
g.Data1 = port
|
||||
return g
|
||||
}
|
||||
|
||||
func (addr *HvsockAddr) raw() rawHvsockAddr {
|
||||
return rawHvsockAddr{
|
||||
Family: afHVSock,
|
||||
VMID: addr.VMID,
|
||||
ServiceID: addr.ServiceID,
|
||||
}
|
||||
}
|
||||
|
||||
func (addr *HvsockAddr) fromRaw(raw *rawHvsockAddr) {
|
||||
addr.VMID = raw.VMID
|
||||
addr.ServiceID = raw.ServiceID
|
||||
}
|
||||
|
||||
// Sockaddr returns a pointer to and the size of this struct.
|
||||
//
|
||||
// Implements the [socket.RawSockaddr] interface, and allows use in
|
||||
// [socket.Bind] and [socket.ConnectEx].
|
||||
func (r *rawHvsockAddr) Sockaddr() (unsafe.Pointer, int32, error) {
|
||||
return unsafe.Pointer(r), int32(unsafe.Sizeof(rawHvsockAddr{})), nil
|
||||
}
|
||||
|
||||
// Sockaddr interface allows use with `sockets.Bind()` and `.ConnectEx()`.
|
||||
func (r *rawHvsockAddr) FromBytes(b []byte) error {
|
||||
n := int(unsafe.Sizeof(rawHvsockAddr{}))
|
||||
|
||||
if len(b) < n {
|
||||
return fmt.Errorf("got %d, want %d: %w", len(b), n, socket.ErrBufferSize)
|
||||
}
|
||||
|
||||
copy(unsafe.Slice((*byte)(unsafe.Pointer(r)), n), b[:n])
|
||||
if r.Family != afHVSock {
|
||||
return fmt.Errorf("got %d, want %d: %w", r.Family, afHVSock, socket.ErrAddrFamily)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HvsockListener is a socket listener for the AF_HYPERV address family.
|
||||
type HvsockListener struct {
|
||||
sock *win32File
|
||||
addr HvsockAddr
|
||||
}
|
||||
|
||||
var _ net.Listener = &HvsockListener{}
|
||||
|
||||
// HvsockConn is a connected socket of the AF_HYPERV address family.
|
||||
type HvsockConn struct {
|
||||
sock *win32File
|
||||
local, remote HvsockAddr
|
||||
}
|
||||
|
||||
var _ net.Conn = &HvsockConn{}
|
||||
|
||||
func newHVSocket() (*win32File, error) {
|
||||
fd, err := windows.Socket(afHVSock, windows.SOCK_STREAM, 1)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("socket", err)
|
||||
}
|
||||
f, err := makeWin32File(fd)
|
||||
if err != nil {
|
||||
windows.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
f.socket = true
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ListenHvsock listens for connections on the specified hvsock address.
|
||||
func ListenHvsock(addr *HvsockAddr) (_ *HvsockListener, err error) {
|
||||
l := &HvsockListener{addr: *addr}
|
||||
|
||||
var sock *win32File
|
||||
sock, err = newHVSocket()
|
||||
if err != nil {
|
||||
return nil, l.opErr("listen", err)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = sock.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
sa := addr.raw()
|
||||
err = socket.Bind(sock.handle, &sa)
|
||||
if err != nil {
|
||||
return nil, l.opErr("listen", os.NewSyscallError("socket", err))
|
||||
}
|
||||
err = windows.Listen(sock.handle, 16)
|
||||
if err != nil {
|
||||
return nil, l.opErr("listen", os.NewSyscallError("listen", err))
|
||||
}
|
||||
return &HvsockListener{sock: sock, addr: *addr}, nil
|
||||
}
|
||||
|
||||
func (l *HvsockListener) opErr(op string, err error) error {
|
||||
return &net.OpError{Op: op, Net: "hvsock", Addr: &l.addr, Err: err}
|
||||
}
|
||||
|
||||
// Addr returns the listener's network address.
|
||||
func (l *HvsockListener) Addr() net.Addr {
|
||||
return &l.addr
|
||||
}
|
||||
|
||||
// Accept waits for the next connection and returns it.
|
||||
func (l *HvsockListener) Accept() (_ net.Conn, err error) {
|
||||
sock, err := newHVSocket()
|
||||
if err != nil {
|
||||
return nil, l.opErr("accept", err)
|
||||
}
|
||||
defer func() {
|
||||
if sock != nil {
|
||||
sock.Close()
|
||||
}
|
||||
}()
|
||||
c, err := l.sock.prepareIO()
|
||||
if err != nil {
|
||||
return nil, l.opErr("accept", err)
|
||||
}
|
||||
defer l.sock.wg.Done()
|
||||
|
||||
// AcceptEx, per documentation, requires an extra 16 bytes per address.
|
||||
//
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/mswsock/nf-mswsock-acceptex
|
||||
const addrlen = uint32(16 + unsafe.Sizeof(rawHvsockAddr{}))
|
||||
var addrbuf [addrlen * 2]byte
|
||||
|
||||
var bytes uint32
|
||||
err = windows.AcceptEx(l.sock.handle, sock.handle, &addrbuf[0], 0 /* rxdatalen */, addrlen, addrlen, &bytes, &c.o)
|
||||
if _, err = l.sock.asyncIO(c, nil, bytes, err); err != nil {
|
||||
return nil, l.opErr("accept", os.NewSyscallError("acceptex", err))
|
||||
}
|
||||
|
||||
conn := &HvsockConn{
|
||||
sock: sock,
|
||||
}
|
||||
// The local address returned in the AcceptEx buffer is the same as the Listener socket's
|
||||
// address. However, the service GUID reported by GetSockName is different from the Listeners
|
||||
// socket, and is sometimes the same as the local address of the socket that dialed the
|
||||
// address, with the service GUID.Data1 incremented, but othertimes is different.
|
||||
// todo: does the local address matter? is the listener's address or the actual address appropriate?
|
||||
conn.local.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[0])))
|
||||
conn.remote.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[addrlen])))
|
||||
|
||||
// initialize the accepted socket and update its properties with those of the listening socket
|
||||
if err = windows.Setsockopt(sock.handle,
|
||||
windows.SOL_SOCKET, windows.SO_UPDATE_ACCEPT_CONTEXT,
|
||||
(*byte)(unsafe.Pointer(&l.sock.handle)), int32(unsafe.Sizeof(l.sock.handle))); err != nil {
|
||||
return nil, conn.opErr("accept", os.NewSyscallError("setsockopt", err))
|
||||
}
|
||||
|
||||
sock = nil
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Close closes the listener, causing any pending Accept calls to fail.
|
||||
func (l *HvsockListener) Close() error {
|
||||
return l.sock.Close()
|
||||
}
|
||||
|
||||
// HvsockDialer configures and dials a Hyper-V Socket (ie, [HvsockConn]).
|
||||
type HvsockDialer struct {
|
||||
// Deadline is the time the Dial operation must connect before erroring.
|
||||
Deadline time.Time
|
||||
|
||||
// Retries is the number of additional connects to try if the connection times out, is refused,
|
||||
// or the host is unreachable
|
||||
Retries uint
|
||||
|
||||
// RetryWait is the time to wait after a connection error to retry
|
||||
RetryWait time.Duration
|
||||
|
||||
rt *time.Timer // redial wait timer
|
||||
}
|
||||
|
||||
// Dial the Hyper-V socket at addr.
|
||||
//
|
||||
// See [HvsockDialer.Dial] for more information.
|
||||
func Dial(ctx context.Context, addr *HvsockAddr) (conn *HvsockConn, err error) {
|
||||
return (&HvsockDialer{}).Dial(ctx, addr)
|
||||
}
|
||||
|
||||
// Dial attempts to connect to the Hyper-V socket at addr, and returns a connection if successful.
|
||||
// Will attempt (HvsockDialer).Retries if dialing fails, waiting (HvsockDialer).RetryWait between
|
||||
// retries.
|
||||
//
|
||||
// Dialing can be cancelled either by providing (HvsockDialer).Deadline, or cancelling ctx.
|
||||
func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *HvsockConn, err error) {
|
||||
op := "dial"
|
||||
// create the conn early to use opErr()
|
||||
conn = &HvsockConn{
|
||||
remote: *addr,
|
||||
}
|
||||
|
||||
if !d.Deadline.IsZero() {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, d.Deadline)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
// preemptive timeout/cancellation check
|
||||
if err = ctx.Err(); err != nil {
|
||||
return nil, conn.opErr(op, err)
|
||||
}
|
||||
|
||||
sock, err := newHVSocket()
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, err)
|
||||
}
|
||||
defer func() {
|
||||
if sock != nil {
|
||||
sock.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
sa := addr.raw()
|
||||
err = socket.Bind(sock.handle, &sa)
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, os.NewSyscallError("bind", err))
|
||||
}
|
||||
|
||||
c, err := sock.prepareIO()
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, err)
|
||||
}
|
||||
defer sock.wg.Done()
|
||||
var bytes uint32
|
||||
for i := uint(0); i <= d.Retries; i++ {
|
||||
err = socket.ConnectEx(
|
||||
sock.handle,
|
||||
&sa,
|
||||
nil, // sendBuf
|
||||
0, // sendDataLen
|
||||
&bytes,
|
||||
(*windows.Overlapped)(unsafe.Pointer(&c.o)))
|
||||
_, err = sock.asyncIO(c, nil, bytes, err)
|
||||
if i < d.Retries && canRedial(err) {
|
||||
if err = d.redialWait(ctx); err == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, os.NewSyscallError("connectex", err))
|
||||
}
|
||||
|
||||
// update the connection properties, so shutdown can be used
|
||||
if err = windows.Setsockopt(
|
||||
sock.handle,
|
||||
windows.SOL_SOCKET,
|
||||
windows.SO_UPDATE_CONNECT_CONTEXT,
|
||||
nil, // optvalue
|
||||
0, // optlen
|
||||
); err != nil {
|
||||
return nil, conn.opErr(op, os.NewSyscallError("setsockopt", err))
|
||||
}
|
||||
|
||||
// get the local name
|
||||
var sal rawHvsockAddr
|
||||
err = socket.GetSockName(sock.handle, &sal)
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, os.NewSyscallError("getsockname", err))
|
||||
}
|
||||
conn.local.fromRaw(&sal)
|
||||
|
||||
// one last check for timeout, since asyncIO doesn't check the context
|
||||
if err = ctx.Err(); err != nil {
|
||||
return nil, conn.opErr(op, err)
|
||||
}
|
||||
|
||||
conn.sock = sock
|
||||
sock = nil
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// redialWait waits before attempting to redial, resetting the timer as appropriate.
|
||||
func (d *HvsockDialer) redialWait(ctx context.Context) (err error) {
|
||||
if d.RetryWait == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.rt == nil {
|
||||
d.rt = time.NewTimer(d.RetryWait)
|
||||
} else {
|
||||
// should already be stopped and drained
|
||||
d.rt.Reset(d.RetryWait)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-d.rt.C:
|
||||
return nil
|
||||
}
|
||||
|
||||
// stop and drain the timer
|
||||
if !d.rt.Stop() {
|
||||
<-d.rt.C
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// assumes error is a plain, unwrapped windows.Errno provided by direct syscall.
|
||||
func canRedial(err error) bool {
|
||||
//nolint:errorlint // guaranteed to be an Errno
|
||||
switch err {
|
||||
case windows.WSAECONNREFUSED, windows.WSAENETUNREACH, windows.WSAETIMEDOUT,
|
||||
windows.ERROR_CONNECTION_REFUSED, windows.ERROR_CONNECTION_UNAVAIL:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *HvsockConn) opErr(op string, err error) error {
|
||||
// translate from "file closed" to "socket closed"
|
||||
if errors.Is(err, ErrFileClosed) {
|
||||
err = socket.ErrSocketClosed
|
||||
}
|
||||
return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err}
|
||||
}
|
||||
|
||||
func (conn *HvsockConn) Read(b []byte) (int, error) {
|
||||
c, err := conn.sock.prepareIO()
|
||||
if err != nil {
|
||||
return 0, conn.opErr("read", err)
|
||||
}
|
||||
defer conn.sock.wg.Done()
|
||||
buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
||||
var flags, bytes uint32
|
||||
err = windows.WSARecv(conn.sock.handle, &buf, 1, &bytes, &flags, &c.o, nil)
|
||||
n, err := conn.sock.asyncIO(c, &conn.sock.readDeadline, bytes, err)
|
||||
if err != nil {
|
||||
var eno windows.Errno
|
||||
if errors.As(err, &eno) {
|
||||
err = os.NewSyscallError("wsarecv", eno)
|
||||
}
|
||||
return 0, conn.opErr("read", err)
|
||||
} else if n == 0 {
|
||||
err = io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (conn *HvsockConn) Write(b []byte) (int, error) {
|
||||
t := 0
|
||||
for len(b) != 0 {
|
||||
n, err := conn.write(b)
|
||||
if err != nil {
|
||||
return t + n, err
|
||||
}
|
||||
t += n
|
||||
b = b[n:]
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (conn *HvsockConn) write(b []byte) (int, error) {
|
||||
c, err := conn.sock.prepareIO()
|
||||
if err != nil {
|
||||
return 0, conn.opErr("write", err)
|
||||
}
|
||||
defer conn.sock.wg.Done()
|
||||
buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
||||
var bytes uint32
|
||||
err = windows.WSASend(conn.sock.handle, &buf, 1, &bytes, 0, &c.o, nil)
|
||||
n, err := conn.sock.asyncIO(c, &conn.sock.writeDeadline, bytes, err)
|
||||
if err != nil {
|
||||
var eno windows.Errno
|
||||
if errors.As(err, &eno) {
|
||||
err = os.NewSyscallError("wsasend", eno)
|
||||
}
|
||||
return 0, conn.opErr("write", err)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close closes the socket connection, failing any pending read or write calls.
|
||||
func (conn *HvsockConn) Close() error {
|
||||
return conn.sock.Close()
|
||||
}
|
||||
|
||||
func (conn *HvsockConn) IsClosed() bool {
|
||||
return conn.sock.IsClosed()
|
||||
}
|
||||
|
||||
// shutdown disables sending or receiving on a socket.
|
||||
func (conn *HvsockConn) shutdown(how int) error {
|
||||
if conn.IsClosed() {
|
||||
return socket.ErrSocketClosed
|
||||
}
|
||||
|
||||
err := windows.Shutdown(conn.sock.handle, how)
|
||||
if err != nil {
|
||||
// If the connection was closed, shutdowns fail with "not connected"
|
||||
if errors.Is(err, windows.WSAENOTCONN) ||
|
||||
errors.Is(err, windows.WSAESHUTDOWN) {
|
||||
err = socket.ErrSocketClosed
|
||||
}
|
||||
return os.NewSyscallError("shutdown", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseRead shuts down the read end of the socket, preventing future read operations.
|
||||
func (conn *HvsockConn) CloseRead() error {
|
||||
err := conn.shutdown(windows.SHUT_RD)
|
||||
if err != nil {
|
||||
return conn.opErr("closeread", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseWrite shuts down the write end of the socket, preventing future write operations and
|
||||
// notifying the other endpoint that no more data will be written.
|
||||
func (conn *HvsockConn) CloseWrite() error {
|
||||
err := conn.shutdown(windows.SHUT_WR)
|
||||
if err != nil {
|
||||
return conn.opErr("closewrite", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalAddr returns the local address of the connection.
|
||||
func (conn *HvsockConn) LocalAddr() net.Addr {
|
||||
return &conn.local
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote address of the connection.
|
||||
func (conn *HvsockConn) RemoteAddr() net.Addr {
|
||||
return &conn.remote
|
||||
}
|
||||
|
||||
// SetDeadline implements the net.Conn SetDeadline method.
|
||||
func (conn *HvsockConn) SetDeadline(t time.Time) error {
|
||||
// todo: implement `SetDeadline` for `win32File`
|
||||
if err := conn.SetReadDeadline(t); err != nil {
|
||||
return fmt.Errorf("set read deadline: %w", err)
|
||||
}
|
||||
if err := conn.SetWriteDeadline(t); err != nil {
|
||||
return fmt.Errorf("set write deadline: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetReadDeadline implements the net.Conn SetReadDeadline method.
|
||||
func (conn *HvsockConn) SetReadDeadline(t time.Time) error {
|
||||
return conn.sock.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// SetWriteDeadline implements the net.Conn SetWriteDeadline method.
|
||||
func (conn *HvsockConn) SetWriteDeadline(t time.Time) error {
|
||||
return conn.sock.SetWriteDeadline(t)
|
||||
}
|
2
vendor/github.com/Microsoft/go-winio/internal/fs/doc.go
generated
vendored
Normal file
2
vendor/github.com/Microsoft/go-winio/internal/fs/doc.go
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
// This package contains Win32 filesystem functionality.
|
||||
package fs
|
262
vendor/github.com/Microsoft/go-winio/internal/fs/fs.go
generated
vendored
Normal file
262
vendor/github.com/Microsoft/go-winio/internal/fs/fs.go
generated
vendored
Normal file
|
@ -0,0 +1,262 @@
|
|||
//go:build windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/Microsoft/go-winio/internal/stringbuffer"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go fs.go
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
|
||||
//sys CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateFileW
|
||||
|
||||
const NullHandle windows.Handle = 0
|
||||
|
||||
// AccessMask defines standard, specific, and generic rights.
|
||||
//
|
||||
// Used with CreateFile and NtCreateFile (and co.).
|
||||
//
|
||||
// Bitmask:
|
||||
// 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
|
||||
// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
|
||||
// +---------------+---------------+-------------------------------+
|
||||
// |G|G|G|G|Resvd|A| StandardRights| SpecificRights |
|
||||
// |R|W|E|A| |S| | |
|
||||
// +-+-------------+---------------+-------------------------------+
|
||||
//
|
||||
// GR Generic Read
|
||||
// GW Generic Write
|
||||
// GE Generic Exectue
|
||||
// GA Generic All
|
||||
// Resvd Reserved
|
||||
// AS Access Security System
|
||||
//
|
||||
// https://learn.microsoft.com/en-us/windows/win32/secauthz/access-mask
|
||||
//
|
||||
// https://learn.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights
|
||||
//
|
||||
// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants
|
||||
type AccessMask = windows.ACCESS_MASK
|
||||
|
||||
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||
const (
|
||||
// Not actually any.
|
||||
//
|
||||
// For CreateFile: "query certain metadata such as file, directory, or device attributes without accessing that file or device"
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#parameters
|
||||
FILE_ANY_ACCESS AccessMask = 0
|
||||
|
||||
GENERIC_READ AccessMask = 0x8000_0000
|
||||
GENERIC_WRITE AccessMask = 0x4000_0000
|
||||
GENERIC_EXECUTE AccessMask = 0x2000_0000
|
||||
GENERIC_ALL AccessMask = 0x1000_0000
|
||||
ACCESS_SYSTEM_SECURITY AccessMask = 0x0100_0000
|
||||
|
||||
// Specific Object Access
|
||||
// from ntioapi.h
|
||||
|
||||
FILE_READ_DATA AccessMask = (0x0001) // file & pipe
|
||||
FILE_LIST_DIRECTORY AccessMask = (0x0001) // directory
|
||||
|
||||
FILE_WRITE_DATA AccessMask = (0x0002) // file & pipe
|
||||
FILE_ADD_FILE AccessMask = (0x0002) // directory
|
||||
|
||||
FILE_APPEND_DATA AccessMask = (0x0004) // file
|
||||
FILE_ADD_SUBDIRECTORY AccessMask = (0x0004) // directory
|
||||
FILE_CREATE_PIPE_INSTANCE AccessMask = (0x0004) // named pipe
|
||||
|
||||
FILE_READ_EA AccessMask = (0x0008) // file & directory
|
||||
FILE_READ_PROPERTIES AccessMask = FILE_READ_EA
|
||||
|
||||
FILE_WRITE_EA AccessMask = (0x0010) // file & directory
|
||||
FILE_WRITE_PROPERTIES AccessMask = FILE_WRITE_EA
|
||||
|
||||
FILE_EXECUTE AccessMask = (0x0020) // file
|
||||
FILE_TRAVERSE AccessMask = (0x0020) // directory
|
||||
|
||||
FILE_DELETE_CHILD AccessMask = (0x0040) // directory
|
||||
|
||||
FILE_READ_ATTRIBUTES AccessMask = (0x0080) // all
|
||||
|
||||
FILE_WRITE_ATTRIBUTES AccessMask = (0x0100) // all
|
||||
|
||||
FILE_ALL_ACCESS AccessMask = (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF)
|
||||
FILE_GENERIC_READ AccessMask = (STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE)
|
||||
FILE_GENERIC_WRITE AccessMask = (STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE)
|
||||
FILE_GENERIC_EXECUTE AccessMask = (STANDARD_RIGHTS_EXECUTE | FILE_READ_ATTRIBUTES | FILE_EXECUTE | SYNCHRONIZE)
|
||||
|
||||
SPECIFIC_RIGHTS_ALL AccessMask = 0x0000FFFF
|
||||
|
||||
// Standard Access
|
||||
// from ntseapi.h
|
||||
|
||||
DELETE AccessMask = 0x0001_0000
|
||||
READ_CONTROL AccessMask = 0x0002_0000
|
||||
WRITE_DAC AccessMask = 0x0004_0000
|
||||
WRITE_OWNER AccessMask = 0x0008_0000
|
||||
SYNCHRONIZE AccessMask = 0x0010_0000
|
||||
|
||||
STANDARD_RIGHTS_REQUIRED AccessMask = 0x000F_0000
|
||||
|
||||
STANDARD_RIGHTS_READ AccessMask = READ_CONTROL
|
||||
STANDARD_RIGHTS_WRITE AccessMask = READ_CONTROL
|
||||
STANDARD_RIGHTS_EXECUTE AccessMask = READ_CONTROL
|
||||
|
||||
STANDARD_RIGHTS_ALL AccessMask = 0x001F_0000
|
||||
)
|
||||
|
||||
type FileShareMode uint32
|
||||
|
||||
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||
const (
|
||||
FILE_SHARE_NONE FileShareMode = 0x00
|
||||
FILE_SHARE_READ FileShareMode = 0x01
|
||||
FILE_SHARE_WRITE FileShareMode = 0x02
|
||||
FILE_SHARE_DELETE FileShareMode = 0x04
|
||||
FILE_SHARE_VALID_FLAGS FileShareMode = 0x07
|
||||
)
|
||||
|
||||
type FileCreationDisposition uint32
|
||||
|
||||
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||
const (
|
||||
// from winbase.h
|
||||
|
||||
CREATE_NEW FileCreationDisposition = 0x01
|
||||
CREATE_ALWAYS FileCreationDisposition = 0x02
|
||||
OPEN_EXISTING FileCreationDisposition = 0x03
|
||||
OPEN_ALWAYS FileCreationDisposition = 0x04
|
||||
TRUNCATE_EXISTING FileCreationDisposition = 0x05
|
||||
)
|
||||
|
||||
// Create disposition values for NtCreate*
|
||||
type NTFileCreationDisposition uint32
|
||||
|
||||
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||
const (
|
||||
// From ntioapi.h
|
||||
|
||||
FILE_SUPERSEDE NTFileCreationDisposition = 0x00
|
||||
FILE_OPEN NTFileCreationDisposition = 0x01
|
||||
FILE_CREATE NTFileCreationDisposition = 0x02
|
||||
FILE_OPEN_IF NTFileCreationDisposition = 0x03
|
||||
FILE_OVERWRITE NTFileCreationDisposition = 0x04
|
||||
FILE_OVERWRITE_IF NTFileCreationDisposition = 0x05
|
||||
FILE_MAXIMUM_DISPOSITION NTFileCreationDisposition = 0x05
|
||||
)
|
||||
|
||||
// CreateFile and co. take flags or attributes together as one parameter.
|
||||
// Define alias until we can use generics to allow both
|
||||
//
|
||||
// https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
|
||||
type FileFlagOrAttribute uint32
|
||||
|
||||
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||
const (
|
||||
// from winnt.h
|
||||
|
||||
FILE_FLAG_WRITE_THROUGH FileFlagOrAttribute = 0x8000_0000
|
||||
FILE_FLAG_OVERLAPPED FileFlagOrAttribute = 0x4000_0000
|
||||
FILE_FLAG_NO_BUFFERING FileFlagOrAttribute = 0x2000_0000
|
||||
FILE_FLAG_RANDOM_ACCESS FileFlagOrAttribute = 0x1000_0000
|
||||
FILE_FLAG_SEQUENTIAL_SCAN FileFlagOrAttribute = 0x0800_0000
|
||||
FILE_FLAG_DELETE_ON_CLOSE FileFlagOrAttribute = 0x0400_0000
|
||||
FILE_FLAG_BACKUP_SEMANTICS FileFlagOrAttribute = 0x0200_0000
|
||||
FILE_FLAG_POSIX_SEMANTICS FileFlagOrAttribute = 0x0100_0000
|
||||
FILE_FLAG_OPEN_REPARSE_POINT FileFlagOrAttribute = 0x0020_0000
|
||||
FILE_FLAG_OPEN_NO_RECALL FileFlagOrAttribute = 0x0010_0000
|
||||
FILE_FLAG_FIRST_PIPE_INSTANCE FileFlagOrAttribute = 0x0008_0000
|
||||
)
|
||||
|
||||
// NtCreate* functions take a dedicated CreateOptions parameter.
|
||||
//
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/Winternl/nf-winternl-ntcreatefile
|
||||
//
|
||||
// https://learn.microsoft.com/en-us/windows/win32/devnotes/nt-create-named-pipe-file
|
||||
type NTCreateOptions uint32
|
||||
|
||||
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||
const (
|
||||
// From ntioapi.h
|
||||
|
||||
FILE_DIRECTORY_FILE NTCreateOptions = 0x0000_0001
|
||||
FILE_WRITE_THROUGH NTCreateOptions = 0x0000_0002
|
||||
FILE_SEQUENTIAL_ONLY NTCreateOptions = 0x0000_0004
|
||||
FILE_NO_INTERMEDIATE_BUFFERING NTCreateOptions = 0x0000_0008
|
||||
|
||||
FILE_SYNCHRONOUS_IO_ALERT NTCreateOptions = 0x0000_0010
|
||||
FILE_SYNCHRONOUS_IO_NONALERT NTCreateOptions = 0x0000_0020
|
||||
FILE_NON_DIRECTORY_FILE NTCreateOptions = 0x0000_0040
|
||||
FILE_CREATE_TREE_CONNECTION NTCreateOptions = 0x0000_0080
|
||||
|
||||
FILE_COMPLETE_IF_OPLOCKED NTCreateOptions = 0x0000_0100
|
||||
FILE_NO_EA_KNOWLEDGE NTCreateOptions = 0x0000_0200
|
||||
FILE_DISABLE_TUNNELING NTCreateOptions = 0x0000_0400
|
||||
FILE_RANDOM_ACCESS NTCreateOptions = 0x0000_0800
|
||||
|
||||
FILE_DELETE_ON_CLOSE NTCreateOptions = 0x0000_1000
|
||||
FILE_OPEN_BY_FILE_ID NTCreateOptions = 0x0000_2000
|
||||
FILE_OPEN_FOR_BACKUP_INTENT NTCreateOptions = 0x0000_4000
|
||||
FILE_NO_COMPRESSION NTCreateOptions = 0x0000_8000
|
||||
)
|
||||
|
||||
type FileSQSFlag = FileFlagOrAttribute
|
||||
|
||||
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||
const (
|
||||
// from winbase.h
|
||||
|
||||
SECURITY_ANONYMOUS FileSQSFlag = FileSQSFlag(SecurityAnonymous << 16)
|
||||
SECURITY_IDENTIFICATION FileSQSFlag = FileSQSFlag(SecurityIdentification << 16)
|
||||
SECURITY_IMPERSONATION FileSQSFlag = FileSQSFlag(SecurityImpersonation << 16)
|
||||
SECURITY_DELEGATION FileSQSFlag = FileSQSFlag(SecurityDelegation << 16)
|
||||
|
||||
SECURITY_SQOS_PRESENT FileSQSFlag = 0x0010_0000
|
||||
SECURITY_VALID_SQOS_FLAGS FileSQSFlag = 0x001F_0000
|
||||
)
|
||||
|
||||
// GetFinalPathNameByHandle flags
|
||||
//
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew#parameters
|
||||
type GetFinalPathFlag uint32
|
||||
|
||||
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||
const (
|
||||
GetFinalPathDefaultFlag GetFinalPathFlag = 0x0
|
||||
|
||||
FILE_NAME_NORMALIZED GetFinalPathFlag = 0x0
|
||||
FILE_NAME_OPENED GetFinalPathFlag = 0x8
|
||||
|
||||
VOLUME_NAME_DOS GetFinalPathFlag = 0x0
|
||||
VOLUME_NAME_GUID GetFinalPathFlag = 0x1
|
||||
VOLUME_NAME_NT GetFinalPathFlag = 0x2
|
||||
VOLUME_NAME_NONE GetFinalPathFlag = 0x4
|
||||
)
|
||||
|
||||
// getFinalPathNameByHandle facilitates calling the Windows API GetFinalPathNameByHandle
|
||||
// with the given handle and flags. It transparently takes care of creating a buffer of the
|
||||
// correct size for the call.
|
||||
//
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew
|
||||
func GetFinalPathNameByHandle(h windows.Handle, flags GetFinalPathFlag) (string, error) {
|
||||
b := stringbuffer.NewWString()
|
||||
//TODO: can loop infinitely if Win32 keeps returning the same (or a larger) n?
|
||||
for {
|
||||
n, err := windows.GetFinalPathNameByHandle(h, b.Pointer(), b.Cap(), uint32(flags))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// If the buffer wasn't large enough, n will be the total size needed (including null terminator).
|
||||
// Resize and try again.
|
||||
if n > b.Cap() {
|
||||
b.ResizeTo(n)
|
||||
continue
|
||||
}
|
||||
// If the buffer is large enough, n will be the size not including the null terminator.
|
||||
// Convert to a Go string and return.
|
||||
return b.String(), nil
|
||||
}
|
||||
}
|
12
vendor/github.com/Microsoft/go-winio/internal/fs/security.go
generated
vendored
Normal file
12
vendor/github.com/Microsoft/go-winio/internal/fs/security.go
generated
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
package fs
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level
|
||||
type SecurityImpersonationLevel int32 // C default enums underlying type is `int`, which is Go `int32`
|
||||
|
||||
// Impersonation levels
|
||||
const (
|
||||
SecurityAnonymous SecurityImpersonationLevel = 0
|
||||
SecurityIdentification SecurityImpersonationLevel = 1
|
||||
SecurityImpersonation SecurityImpersonationLevel = 2
|
||||
SecurityDelegation SecurityImpersonationLevel = 3
|
||||
)
|
61
vendor/github.com/Microsoft/go-winio/internal/fs/zsyscall_windows.go
generated
vendored
Normal file
61
vendor/github.com/Microsoft/go-winio/internal/fs/zsyscall_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
//go:build windows
|
||||
|
||||
// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
|
||||
procCreateFileW = modkernel32.NewProc("CreateFileW")
|
||||
)
|
||||
|
||||
func CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) {
|
||||
var _p0 *uint16
|
||||
_p0, err = syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return _CreateFile(_p0, access, mode, sa, createmode, attrs, templatefile)
|
||||
}
|
||||
|
||||
func _CreateFile(name *uint16, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) {
|
||||
r0, _, e1 := syscall.SyscallN(procCreateFileW.Addr(), uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile))
|
||||
handle = windows.Handle(r0)
|
||||
if handle == windows.InvalidHandle {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
20
vendor/github.com/Microsoft/go-winio/internal/socket/rawaddr.go
generated
vendored
Normal file
20
vendor/github.com/Microsoft/go-winio/internal/socket/rawaddr.go
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
package socket
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// RawSockaddr allows structs to be used with [Bind] and [ConnectEx]. The
|
||||
// struct must meet the Win32 sockaddr requirements specified here:
|
||||
// https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2
|
||||
//
|
||||
// Specifically, the struct size must be least larger than an int16 (unsigned short)
|
||||
// for the address family.
|
||||
type RawSockaddr interface {
|
||||
// Sockaddr returns a pointer to the RawSockaddr and its struct size, allowing
|
||||
// for the RawSockaddr's data to be overwritten by syscalls (if necessary).
|
||||
//
|
||||
// It is the callers responsibility to validate that the values are valid; invalid
|
||||
// pointers or size can cause a panic.
|
||||
Sockaddr() (unsafe.Pointer, int32, error)
|
||||
}
|
177
vendor/github.com/Microsoft/go-winio/internal/socket/socket.go
generated
vendored
Normal file
177
vendor/github.com/Microsoft/go-winio/internal/socket/socket.go
generated
vendored
Normal file
|
@ -0,0 +1,177 @@
|
|||
//go:build windows
|
||||
|
||||
package socket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/Microsoft/go-winio/pkg/guid"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go socket.go
|
||||
|
||||
//sys getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getsockname
|
||||
//sys getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getpeername
|
||||
//sys bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) [failretval==socketError] = ws2_32.bind
|
||||
|
||||
const socketError = uintptr(^uint32(0))
|
||||
|
||||
var (
|
||||
// todo(helsaawy): create custom error types to store the desired vs actual size and addr family?
|
||||
|
||||
ErrBufferSize = errors.New("buffer size")
|
||||
ErrAddrFamily = errors.New("address family")
|
||||
ErrInvalidPointer = errors.New("invalid pointer")
|
||||
ErrSocketClosed = fmt.Errorf("socket closed: %w", net.ErrClosed)
|
||||
)
|
||||
|
||||
// todo(helsaawy): replace these with generics, ie: GetSockName[S RawSockaddr](s windows.Handle) (S, error)
|
||||
|
||||
// GetSockName writes the local address of socket s to the [RawSockaddr] rsa.
|
||||
// If rsa is not large enough, the [windows.WSAEFAULT] is returned.
|
||||
func GetSockName(s windows.Handle, rsa RawSockaddr) error {
|
||||
ptr, l, err := rsa.Sockaddr()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not retrieve socket pointer and size: %w", err)
|
||||
}
|
||||
|
||||
// although getsockname returns WSAEFAULT if the buffer is too small, it does not set
|
||||
// &l to the correct size, so--apart from doubling the buffer repeatedly--there is no remedy
|
||||
return getsockname(s, ptr, &l)
|
||||
}
|
||||
|
||||
// GetPeerName returns the remote address the socket is connected to.
|
||||
//
|
||||
// See [GetSockName] for more information.
|
||||
func GetPeerName(s windows.Handle, rsa RawSockaddr) error {
|
||||
ptr, l, err := rsa.Sockaddr()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not retrieve socket pointer and size: %w", err)
|
||||
}
|
||||
|
||||
return getpeername(s, ptr, &l)
|
||||
}
|
||||
|
||||
func Bind(s windows.Handle, rsa RawSockaddr) (err error) {
|
||||
ptr, l, err := rsa.Sockaddr()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not retrieve socket pointer and size: %w", err)
|
||||
}
|
||||
|
||||
return bind(s, ptr, l)
|
||||
}
|
||||
|
||||
// "golang.org/x/sys/windows".ConnectEx and .Bind only accept internal implementations of the
|
||||
// their sockaddr interface, so they cannot be used with HvsockAddr
|
||||
// Replicate functionality here from
|
||||
// https://cs.opensource.google/go/x/sys/+/master:windows/syscall_windows.go
|
||||
|
||||
// The function pointers to `AcceptEx`, `ConnectEx` and `GetAcceptExSockaddrs` must be loaded at
|
||||
// runtime via a WSAIoctl call:
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/Mswsock/nc-mswsock-lpfn_connectex#remarks
|
||||
|
||||
type runtimeFunc struct {
|
||||
id guid.GUID
|
||||
once sync.Once
|
||||
addr uintptr
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *runtimeFunc) Load() error {
|
||||
f.once.Do(func() {
|
||||
var s windows.Handle
|
||||
s, f.err = windows.Socket(windows.AF_INET, windows.SOCK_STREAM, windows.IPPROTO_TCP)
|
||||
if f.err != nil {
|
||||
return
|
||||
}
|
||||
defer windows.CloseHandle(s) //nolint:errcheck
|
||||
|
||||
var n uint32
|
||||
f.err = windows.WSAIoctl(s,
|
||||
windows.SIO_GET_EXTENSION_FUNCTION_POINTER,
|
||||
(*byte)(unsafe.Pointer(&f.id)),
|
||||
uint32(unsafe.Sizeof(f.id)),
|
||||
(*byte)(unsafe.Pointer(&f.addr)),
|
||||
uint32(unsafe.Sizeof(f.addr)),
|
||||
&n,
|
||||
nil, // overlapped
|
||||
0, // completionRoutine
|
||||
)
|
||||
})
|
||||
return f.err
|
||||
}
|
||||
|
||||
var (
|
||||
// todo: add `AcceptEx` and `GetAcceptExSockaddrs`
|
||||
WSAID_CONNECTEX = guid.GUID{ //revive:disable-line:var-naming ALL_CAPS
|
||||
Data1: 0x25a207b9,
|
||||
Data2: 0xddf3,
|
||||
Data3: 0x4660,
|
||||
Data4: [8]byte{0x8e, 0xe9, 0x76, 0xe5, 0x8c, 0x74, 0x06, 0x3e},
|
||||
}
|
||||
|
||||
connectExFunc = runtimeFunc{id: WSAID_CONNECTEX}
|
||||
)
|
||||
|
||||
func ConnectEx(
|
||||
fd windows.Handle,
|
||||
rsa RawSockaddr,
|
||||
sendBuf *byte,
|
||||
sendDataLen uint32,
|
||||
bytesSent *uint32,
|
||||
overlapped *windows.Overlapped,
|
||||
) error {
|
||||
if err := connectExFunc.Load(); err != nil {
|
||||
return fmt.Errorf("failed to load ConnectEx function pointer: %w", err)
|
||||
}
|
||||
ptr, n, err := rsa.Sockaddr()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return connectEx(fd, ptr, n, sendBuf, sendDataLen, bytesSent, overlapped)
|
||||
}
|
||||
|
||||
// BOOL LpfnConnectex(
|
||||
// [in] SOCKET s,
|
||||
// [in] const sockaddr *name,
|
||||
// [in] int namelen,
|
||||
// [in, optional] PVOID lpSendBuffer,
|
||||
// [in] DWORD dwSendDataLength,
|
||||
// [out] LPDWORD lpdwBytesSent,
|
||||
// [in] LPOVERLAPPED lpOverlapped
|
||||
// )
|
||||
|
||||
func connectEx(
|
||||
s windows.Handle,
|
||||
name unsafe.Pointer,
|
||||
namelen int32,
|
||||
sendBuf *byte,
|
||||
sendDataLen uint32,
|
||||
bytesSent *uint32,
|
||||
overlapped *windows.Overlapped,
|
||||
) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(connectExFunc.addr,
|
||||
uintptr(s),
|
||||
uintptr(name),
|
||||
uintptr(namelen),
|
||||
uintptr(unsafe.Pointer(sendBuf)),
|
||||
uintptr(sendDataLen),
|
||||
uintptr(unsafe.Pointer(bytesSent)),
|
||||
uintptr(unsafe.Pointer(overlapped)),
|
||||
)
|
||||
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = error(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
69
vendor/github.com/Microsoft/go-winio/internal/socket/zsyscall_windows.go
generated
vendored
Normal file
69
vendor/github.com/Microsoft/go-winio/internal/socket/zsyscall_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,69 @@
|
|||
//go:build windows
|
||||
|
||||
// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT.
|
||||
|
||||
package socket
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modws2_32 = windows.NewLazySystemDLL("ws2_32.dll")
|
||||
|
||||
procbind = modws2_32.NewProc("bind")
|
||||
procgetpeername = modws2_32.NewProc("getpeername")
|
||||
procgetsockname = modws2_32.NewProc("getsockname")
|
||||
)
|
||||
|
||||
func bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procbind.Addr(), uintptr(s), uintptr(name), uintptr(namelen))
|
||||
if r1 == socketError {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procgetpeername.Addr(), uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen)))
|
||||
if r1 == socketError {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procgetsockname.Addr(), uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen)))
|
||||
if r1 == socketError {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
132
vendor/github.com/Microsoft/go-winio/internal/stringbuffer/wstring.go
generated
vendored
Normal file
132
vendor/github.com/Microsoft/go-winio/internal/stringbuffer/wstring.go
generated
vendored
Normal file
|
@ -0,0 +1,132 @@
|
|||
package stringbuffer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// TODO: worth exporting and using in mkwinsyscall?
|
||||
|
||||
// Uint16BufferSize is the buffer size in the pool, chosen somewhat arbitrarily to accommodate
|
||||
// large path strings:
|
||||
// MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310.
|
||||
const MinWStringCap = 310
|
||||
|
||||
// use *[]uint16 since []uint16 creates an extra allocation where the slice header
|
||||
// is copied to heap and then referenced via pointer in the interface header that sync.Pool
|
||||
// stores.
|
||||
var pathPool = sync.Pool{ // if go1.18+ adds Pool[T], use that to store []uint16 directly
|
||||
New: func() interface{} {
|
||||
b := make([]uint16, MinWStringCap)
|
||||
return &b
|
||||
},
|
||||
}
|
||||
|
||||
func newBuffer() []uint16 { return *(pathPool.Get().(*[]uint16)) }
|
||||
|
||||
// freeBuffer copies the slice header data, and puts a pointer to that in the pool.
|
||||
// This avoids taking a pointer to the slice header in WString, which can be set to nil.
|
||||
func freeBuffer(b []uint16) { pathPool.Put(&b) }
|
||||
|
||||
// WString is a wide string buffer ([]uint16) meant for storing UTF-16 encoded strings
|
||||
// for interacting with Win32 APIs.
|
||||
// Sizes are specified as uint32 and not int.
|
||||
//
|
||||
// It is not thread safe.
|
||||
type WString struct {
|
||||
// type-def allows casting to []uint16 directly, use struct to prevent that and allow adding fields in the future.
|
||||
|
||||
// raw buffer
|
||||
b []uint16
|
||||
}
|
||||
|
||||
// NewWString returns a [WString] allocated from a shared pool with an
|
||||
// initial capacity of at least [MinWStringCap].
|
||||
// Since the buffer may have been previously used, its contents are not guaranteed to be empty.
|
||||
//
|
||||
// The buffer should be freed via [WString.Free]
|
||||
func NewWString() *WString {
|
||||
return &WString{
|
||||
b: newBuffer(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WString) Free() {
|
||||
if b.empty() {
|
||||
return
|
||||
}
|
||||
freeBuffer(b.b)
|
||||
b.b = nil
|
||||
}
|
||||
|
||||
// ResizeTo grows the buffer to at least c and returns the new capacity, freeing the
|
||||
// previous buffer back into pool.
|
||||
func (b *WString) ResizeTo(c uint32) uint32 {
|
||||
// already sufficient (or n is 0)
|
||||
if c <= b.Cap() {
|
||||
return b.Cap()
|
||||
}
|
||||
|
||||
if c <= MinWStringCap {
|
||||
c = MinWStringCap
|
||||
}
|
||||
// allocate at-least double buffer size, as is done in [bytes.Buffer] and other places
|
||||
if c <= 2*b.Cap() {
|
||||
c = 2 * b.Cap()
|
||||
}
|
||||
|
||||
b2 := make([]uint16, c)
|
||||
if !b.empty() {
|
||||
copy(b2, b.b)
|
||||
freeBuffer(b.b)
|
||||
}
|
||||
b.b = b2
|
||||
return c
|
||||
}
|
||||
|
||||
// Buffer returns the underlying []uint16 buffer.
|
||||
func (b *WString) Buffer() []uint16 {
|
||||
if b.empty() {
|
||||
return nil
|
||||
}
|
||||
return b.b
|
||||
}
|
||||
|
||||
// Pointer returns a pointer to the first uint16 in the buffer.
|
||||
// If the [WString.Free] has already been called, the pointer will be nil.
|
||||
func (b *WString) Pointer() *uint16 {
|
||||
if b.empty() {
|
||||
return nil
|
||||
}
|
||||
return &b.b[0]
|
||||
}
|
||||
|
||||
// String returns the returns the UTF-8 encoding of the UTF-16 string in the buffer.
|
||||
//
|
||||
// It assumes that the data is null-terminated.
|
||||
func (b *WString) String() string {
|
||||
// Using [windows.UTF16ToString] would require importing "golang.org/x/sys/windows"
|
||||
// and would make this code Windows-only, which makes no sense.
|
||||
// So copy UTF16ToString code into here.
|
||||
// If other windows-specific code is added, switch to [windows.UTF16ToString]
|
||||
|
||||
s := b.b
|
||||
for i, v := range s {
|
||||
if v == 0 {
|
||||
s = s[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
return string(utf16.Decode(s))
|
||||
}
|
||||
|
||||
// Cap returns the underlying buffer capacity.
|
||||
func (b *WString) Cap() uint32 {
|
||||
if b.empty() {
|
||||
return 0
|
||||
}
|
||||
return b.cap()
|
||||
}
|
||||
|
||||
func (b *WString) cap() uint32 { return uint32(cap(b.b)) }
|
||||
func (b *WString) empty() bool { return b == nil || b.cap() == 0 }
|
586
vendor/github.com/Microsoft/go-winio/pipe.go
generated
vendored
Normal file
586
vendor/github.com/Microsoft/go-winio/pipe.go
generated
vendored
Normal file
|
@ -0,0 +1,586 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/Microsoft/go-winio/internal/fs"
|
||||
)
|
||||
|
||||
//sys connectNamedPipe(pipe windows.Handle, o *windows.Overlapped) (err error) = ConnectNamedPipe
|
||||
//sys createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateNamedPipeW
|
||||
//sys disconnectNamedPipe(pipe windows.Handle) (err error) = DisconnectNamedPipe
|
||||
//sys getNamedPipeInfo(pipe windows.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) = GetNamedPipeInfo
|
||||
//sys getNamedPipeHandleState(pipe windows.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
|
||||
//sys ntCreateNamedPipeFile(pipe *windows.Handle, access ntAccessMask, oa *objectAttributes, iosb *ioStatusBlock, share ntFileShareMode, disposition ntFileCreationDisposition, options ntFileOptions, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) = ntdll.NtCreateNamedPipeFile
|
||||
//sys rtlNtStatusToDosError(status ntStatus) (winerr error) = ntdll.RtlNtStatusToDosErrorNoTeb
|
||||
//sys rtlDosPathNameToNtPathName(name *uint16, ntName *unicodeString, filePart uintptr, reserved uintptr) (status ntStatus) = ntdll.RtlDosPathNameToNtPathName_U
|
||||
//sys rtlDefaultNpAcl(dacl *uintptr) (status ntStatus) = ntdll.RtlDefaultNpAcl
|
||||
|
||||
type PipeConn interface {
|
||||
net.Conn
|
||||
Disconnect() error
|
||||
Flush() error
|
||||
}
|
||||
|
||||
// type aliases for mkwinsyscall code
|
||||
type (
|
||||
ntAccessMask = fs.AccessMask
|
||||
ntFileShareMode = fs.FileShareMode
|
||||
ntFileCreationDisposition = fs.NTFileCreationDisposition
|
||||
ntFileOptions = fs.NTCreateOptions
|
||||
)
|
||||
|
||||
type ioStatusBlock struct {
|
||||
Status, Information uintptr
|
||||
}
|
||||
|
||||
// typedef struct _OBJECT_ATTRIBUTES {
|
||||
// ULONG Length;
|
||||
// HANDLE RootDirectory;
|
||||
// PUNICODE_STRING ObjectName;
|
||||
// ULONG Attributes;
|
||||
// PVOID SecurityDescriptor;
|
||||
// PVOID SecurityQualityOfService;
|
||||
// } OBJECT_ATTRIBUTES;
|
||||
//
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/ntdef/ns-ntdef-_object_attributes
|
||||
type objectAttributes struct {
|
||||
Length uintptr
|
||||
RootDirectory uintptr
|
||||
ObjectName *unicodeString
|
||||
Attributes uintptr
|
||||
SecurityDescriptor *securityDescriptor
|
||||
SecurityQoS uintptr
|
||||
}
|
||||
|
||||
type unicodeString struct {
|
||||
Length uint16
|
||||
MaximumLength uint16
|
||||
Buffer uintptr
|
||||
}
|
||||
|
||||
// typedef struct _SECURITY_DESCRIPTOR {
|
||||
// BYTE Revision;
|
||||
// BYTE Sbz1;
|
||||
// SECURITY_DESCRIPTOR_CONTROL Control;
|
||||
// PSID Owner;
|
||||
// PSID Group;
|
||||
// PACL Sacl;
|
||||
// PACL Dacl;
|
||||
// } SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;
|
||||
//
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_descriptor
|
||||
type securityDescriptor struct {
|
||||
Revision byte
|
||||
Sbz1 byte
|
||||
Control uint16
|
||||
Owner uintptr
|
||||
Group uintptr
|
||||
Sacl uintptr //revive:disable-line:var-naming SACL, not Sacl
|
||||
Dacl uintptr //revive:disable-line:var-naming DACL, not Dacl
|
||||
}
|
||||
|
||||
type ntStatus int32
|
||||
|
||||
func (status ntStatus) Err() error {
|
||||
if status >= 0 {
|
||||
return nil
|
||||
}
|
||||
return rtlNtStatusToDosError(status)
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
|
||||
ErrPipeListenerClosed = net.ErrClosed
|
||||
|
||||
errPipeWriteClosed = errors.New("pipe has been closed for write")
|
||||
)
|
||||
|
||||
type win32Pipe struct {
|
||||
*win32File
|
||||
path string
|
||||
}
|
||||
|
||||
var _ PipeConn = (*win32Pipe)(nil)
|
||||
|
||||
type win32MessageBytePipe struct {
|
||||
win32Pipe
|
||||
writeClosed bool
|
||||
readEOF bool
|
||||
}
|
||||
|
||||
type pipeAddress string
|
||||
|
||||
func (f *win32Pipe) LocalAddr() net.Addr {
|
||||
return pipeAddress(f.path)
|
||||
}
|
||||
|
||||
func (f *win32Pipe) RemoteAddr() net.Addr {
|
||||
return pipeAddress(f.path)
|
||||
}
|
||||
|
||||
func (f *win32Pipe) SetDeadline(t time.Time) error {
|
||||
if err := f.SetReadDeadline(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (f *win32Pipe) Disconnect() error {
|
||||
return disconnectNamedPipe(f.win32File.handle)
|
||||
}
|
||||
|
||||
// CloseWrite closes the write side of a message pipe in byte mode.
|
||||
func (f *win32MessageBytePipe) CloseWrite() error {
|
||||
if f.writeClosed {
|
||||
return errPipeWriteClosed
|
||||
}
|
||||
err := f.win32File.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = f.win32File.Write(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.writeClosed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since
|
||||
// they are used to implement CloseWrite().
|
||||
func (f *win32MessageBytePipe) Write(b []byte) (int, error) {
|
||||
if f.writeClosed {
|
||||
return 0, errPipeWriteClosed
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return f.win32File.Write(b)
|
||||
}
|
||||
|
||||
// Read reads bytes from a message pipe in byte mode. A read of a zero-byte message on a message
|
||||
// mode pipe will return io.EOF, as will all subsequent reads.
|
||||
func (f *win32MessageBytePipe) Read(b []byte) (int, error) {
|
||||
if f.readEOF {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n, err := f.win32File.Read(b)
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
// If this was the result of a zero-byte read, then
|
||||
// it is possible that the read was due to a zero-size
|
||||
// message. Since we are simulating CloseWrite with a
|
||||
// zero-byte message, ensure that all future Read() calls
|
||||
// also return EOF.
|
||||
f.readEOF = true
|
||||
} else if err == windows.ERROR_MORE_DATA { //nolint:errorlint // err is Errno
|
||||
// ERROR_MORE_DATA indicates that the pipe's read mode is message mode
|
||||
// and the message still has more bytes. Treat this as a success, since
|
||||
// this package presents all named pipes as byte streams.
|
||||
err = nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (pipeAddress) Network() string {
|
||||
return "pipe"
|
||||
}
|
||||
|
||||
func (s pipeAddress) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// tryDialPipe attempts to dial the pipe at `path` until `ctx` cancellation or timeout.
|
||||
func tryDialPipe(ctx context.Context, path *string, access fs.AccessMask, impLevel PipeImpLevel) (windows.Handle, error) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return windows.Handle(0), ctx.Err()
|
||||
default:
|
||||
h, err := fs.CreateFile(*path,
|
||||
access,
|
||||
0, // mode
|
||||
nil, // security attributes
|
||||
fs.OPEN_EXISTING,
|
||||
fs.FILE_FLAG_OVERLAPPED|fs.SECURITY_SQOS_PRESENT|fs.FileSQSFlag(impLevel),
|
||||
0, // template file handle
|
||||
)
|
||||
if err == nil {
|
||||
return h, nil
|
||||
}
|
||||
if err != windows.ERROR_PIPE_BUSY { //nolint:errorlint // err is Errno
|
||||
return h, &os.PathError{Err: err, Op: "open", Path: *path}
|
||||
}
|
||||
// Wait 10 msec and try again. This is a rather simplistic
|
||||
// view, as we always try each 10 milliseconds.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DialPipe connects to a named pipe by path, timing out if the connection
|
||||
// takes longer than the specified duration. If timeout is nil, then we use
|
||||
// a default timeout of 2 seconds. (We do not use WaitNamedPipe.)
|
||||
func DialPipe(path string, timeout *time.Duration) (net.Conn, error) {
|
||||
var absTimeout time.Time
|
||||
if timeout != nil {
|
||||
absTimeout = time.Now().Add(*timeout)
|
||||
} else {
|
||||
absTimeout = time.Now().Add(2 * time.Second)
|
||||
}
|
||||
ctx, cancel := context.WithDeadline(context.Background(), absTimeout)
|
||||
defer cancel()
|
||||
conn, err := DialPipeContext(ctx, path)
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// DialPipeContext attempts to connect to a named pipe by `path` until `ctx`
|
||||
// cancellation or timeout.
|
||||
func DialPipeContext(ctx context.Context, path string) (net.Conn, error) {
|
||||
return DialPipeAccess(ctx, path, uint32(fs.GENERIC_READ|fs.GENERIC_WRITE))
|
||||
}
|
||||
|
||||
// PipeImpLevel is an enumeration of impersonation levels that may be set
|
||||
// when calling DialPipeAccessImpersonation.
|
||||
type PipeImpLevel uint32
|
||||
|
||||
const (
|
||||
PipeImpLevelAnonymous = PipeImpLevel(fs.SECURITY_ANONYMOUS)
|
||||
PipeImpLevelIdentification = PipeImpLevel(fs.SECURITY_IDENTIFICATION)
|
||||
PipeImpLevelImpersonation = PipeImpLevel(fs.SECURITY_IMPERSONATION)
|
||||
PipeImpLevelDelegation = PipeImpLevel(fs.SECURITY_DELEGATION)
|
||||
)
|
||||
|
||||
// DialPipeAccess attempts to connect to a named pipe by `path` with `access` until `ctx`
|
||||
// cancellation or timeout.
|
||||
func DialPipeAccess(ctx context.Context, path string, access uint32) (net.Conn, error) {
|
||||
return DialPipeAccessImpLevel(ctx, path, access, PipeImpLevelAnonymous)
|
||||
}
|
||||
|
||||
// DialPipeAccessImpLevel attempts to connect to a named pipe by `path` with
|
||||
// `access` at `impLevel` until `ctx` cancellation or timeout. The other
|
||||
// DialPipe* implementations use PipeImpLevelAnonymous.
|
||||
func DialPipeAccessImpLevel(ctx context.Context, path string, access uint32, impLevel PipeImpLevel) (net.Conn, error) {
|
||||
var err error
|
||||
var h windows.Handle
|
||||
h, err = tryDialPipe(ctx, &path, fs.AccessMask(access), impLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var flags uint32
|
||||
err = getNamedPipeInfo(h, &flags, nil, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := makeWin32File(h)
|
||||
if err != nil {
|
||||
windows.Close(h)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the pipe is in message mode, return a message byte pipe, which
|
||||
// supports CloseWrite().
|
||||
if flags&windows.PIPE_TYPE_MESSAGE != 0 {
|
||||
return &win32MessageBytePipe{
|
||||
win32Pipe: win32Pipe{win32File: f, path: path},
|
||||
}, nil
|
||||
}
|
||||
return &win32Pipe{win32File: f, path: path}, nil
|
||||
}
|
||||
|
||||
type acceptResponse struct {
|
||||
f *win32File
|
||||
err error
|
||||
}
|
||||
|
||||
type win32PipeListener struct {
|
||||
firstHandle windows.Handle
|
||||
path string
|
||||
config PipeConfig
|
||||
acceptCh chan (chan acceptResponse)
|
||||
closeCh chan int
|
||||
doneCh chan int
|
||||
}
|
||||
|
||||
func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (windows.Handle, error) {
|
||||
path16, err := windows.UTF16FromString(path)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "open", Path: path, Err: err}
|
||||
}
|
||||
|
||||
var oa objectAttributes
|
||||
oa.Length = unsafe.Sizeof(oa)
|
||||
|
||||
var ntPath unicodeString
|
||||
if err := rtlDosPathNameToNtPathName(&path16[0],
|
||||
&ntPath,
|
||||
0,
|
||||
0,
|
||||
).Err(); err != nil {
|
||||
return 0, &os.PathError{Op: "open", Path: path, Err: err}
|
||||
}
|
||||
defer windows.LocalFree(windows.Handle(ntPath.Buffer)) //nolint:errcheck
|
||||
oa.ObjectName = &ntPath
|
||||
oa.Attributes = windows.OBJ_CASE_INSENSITIVE
|
||||
|
||||
// The security descriptor is only needed for the first pipe.
|
||||
if first {
|
||||
if sd != nil {
|
||||
//todo: does `sdb` need to be allocated on the heap, or can go allocate it?
|
||||
l := uint32(len(sd))
|
||||
sdb, err := windows.LocalAlloc(0, l)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("LocalAlloc for security descriptor with of length %d: %w", l, err)
|
||||
}
|
||||
defer windows.LocalFree(windows.Handle(sdb)) //nolint:errcheck
|
||||
copy((*[0xffff]byte)(unsafe.Pointer(sdb))[:], sd)
|
||||
oa.SecurityDescriptor = (*securityDescriptor)(unsafe.Pointer(sdb))
|
||||
} else {
|
||||
// Construct the default named pipe security descriptor.
|
||||
var dacl uintptr
|
||||
if err := rtlDefaultNpAcl(&dacl).Err(); err != nil {
|
||||
return 0, fmt.Errorf("getting default named pipe ACL: %w", err)
|
||||
}
|
||||
defer windows.LocalFree(windows.Handle(dacl)) //nolint:errcheck
|
||||
|
||||
sdb := &securityDescriptor{
|
||||
Revision: 1,
|
||||
Control: windows.SE_DACL_PRESENT,
|
||||
Dacl: dacl,
|
||||
}
|
||||
oa.SecurityDescriptor = sdb
|
||||
}
|
||||
}
|
||||
|
||||
typ := uint32(windows.FILE_PIPE_REJECT_REMOTE_CLIENTS)
|
||||
if c.MessageMode {
|
||||
typ |= windows.FILE_PIPE_MESSAGE_TYPE
|
||||
}
|
||||
|
||||
disposition := fs.FILE_OPEN
|
||||
access := fs.GENERIC_READ | fs.GENERIC_WRITE | fs.SYNCHRONIZE
|
||||
if first {
|
||||
disposition = fs.FILE_CREATE
|
||||
// By not asking for read or write access, the named pipe file system
|
||||
// will put this pipe into an initially disconnected state, blocking
|
||||
// client connections until the next call with first == false.
|
||||
access = fs.SYNCHRONIZE
|
||||
}
|
||||
|
||||
timeout := int64(-50 * 10000) // 50ms
|
||||
|
||||
var (
|
||||
h windows.Handle
|
||||
iosb ioStatusBlock
|
||||
)
|
||||
err = ntCreateNamedPipeFile(&h,
|
||||
access,
|
||||
&oa,
|
||||
&iosb,
|
||||
fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE,
|
||||
disposition,
|
||||
0,
|
||||
typ,
|
||||
0,
|
||||
0,
|
||||
0xffffffff,
|
||||
uint32(c.InputBufferSize),
|
||||
uint32(c.OutputBufferSize),
|
||||
&timeout).Err()
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "open", Path: path, Err: err}
|
||||
}
|
||||
|
||||
runtime.KeepAlive(ntPath)
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) makeServerPipe() (*win32File, error) {
|
||||
h, err := makeServerPipeHandle(l.path, nil, &l.config, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := makeWin32File(h)
|
||||
if err != nil {
|
||||
windows.Close(h)
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) {
|
||||
p, err := l.makeServerPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wait for the client to connect.
|
||||
ch := make(chan error)
|
||||
go func(p *win32File) {
|
||||
ch <- connectPipe(p)
|
||||
}(p)
|
||||
|
||||
select {
|
||||
case err = <-ch:
|
||||
if err != nil {
|
||||
p.Close()
|
||||
p = nil
|
||||
}
|
||||
case <-l.closeCh:
|
||||
// Abort the connect request by closing the handle.
|
||||
p.Close()
|
||||
p = nil
|
||||
err = <-ch
|
||||
if err == nil || err == ErrFileClosed { //nolint:errorlint // err is Errno
|
||||
err = ErrPipeListenerClosed
|
||||
}
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) listenerRoutine() {
|
||||
closed := false
|
||||
for !closed {
|
||||
select {
|
||||
case <-l.closeCh:
|
||||
closed = true
|
||||
case responseCh := <-l.acceptCh:
|
||||
var (
|
||||
p *win32File
|
||||
err error
|
||||
)
|
||||
for {
|
||||
p, err = l.makeConnectedServerPipe()
|
||||
// If the connection was immediately closed by the client, try
|
||||
// again.
|
||||
if err != windows.ERROR_NO_DATA { //nolint:errorlint // err is Errno
|
||||
break
|
||||
}
|
||||
}
|
||||
responseCh <- acceptResponse{p, err}
|
||||
closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno
|
||||
}
|
||||
}
|
||||
windows.Close(l.firstHandle)
|
||||
l.firstHandle = 0
|
||||
// Notify Close() and Accept() callers that the handle has been closed.
|
||||
close(l.doneCh)
|
||||
}
|
||||
|
||||
// PipeConfig contain configuration for the pipe listener.
|
||||
type PipeConfig struct {
|
||||
// SecurityDescriptor contains a Windows security descriptor in SDDL format.
|
||||
SecurityDescriptor string
|
||||
|
||||
// MessageMode determines whether the pipe is in byte or message mode. In either
|
||||
// case the pipe is read in byte mode by default. The only practical difference in
|
||||
// this implementation is that CloseWrite() is only supported for message mode pipes;
|
||||
// CloseWrite() is implemented as a zero-byte write, but zero-byte writes are only
|
||||
// transferred to the reader (and returned as io.EOF in this implementation)
|
||||
// when the pipe is in message mode.
|
||||
MessageMode bool
|
||||
|
||||
// InputBufferSize specifies the size of the input buffer, in bytes.
|
||||
InputBufferSize int32
|
||||
|
||||
// OutputBufferSize specifies the size of the output buffer, in bytes.
|
||||
OutputBufferSize int32
|
||||
}
|
||||
|
||||
// ListenPipe creates a listener on a Windows named pipe path, e.g. \\.\pipe\mypipe.
|
||||
// The pipe must not already exist.
|
||||
func ListenPipe(path string, c *PipeConfig) (net.Listener, error) {
|
||||
var (
|
||||
sd []byte
|
||||
err error
|
||||
)
|
||||
if c == nil {
|
||||
c = &PipeConfig{}
|
||||
}
|
||||
if c.SecurityDescriptor != "" {
|
||||
sd, err = SddlToSecurityDescriptor(c.SecurityDescriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
h, err := makeServerPipeHandle(path, sd, c, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l := &win32PipeListener{
|
||||
firstHandle: h,
|
||||
path: path,
|
||||
config: *c,
|
||||
acceptCh: make(chan (chan acceptResponse)),
|
||||
closeCh: make(chan int),
|
||||
doneCh: make(chan int),
|
||||
}
|
||||
go l.listenerRoutine()
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func connectPipe(p *win32File) error {
|
||||
c, err := p.prepareIO()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer p.wg.Done()
|
||||
|
||||
err = connectNamedPipe(p.handle, &c.o)
|
||||
_, err = p.asyncIO(c, nil, 0, err)
|
||||
if err != nil && err != windows.ERROR_PIPE_CONNECTED { //nolint:errorlint // err is Errno
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) Accept() (net.Conn, error) {
|
||||
ch := make(chan acceptResponse)
|
||||
select {
|
||||
case l.acceptCh <- ch:
|
||||
response := <-ch
|
||||
err := response.err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if l.config.MessageMode {
|
||||
return &win32MessageBytePipe{
|
||||
win32Pipe: win32Pipe{win32File: response.f, path: l.path},
|
||||
}, nil
|
||||
}
|
||||
return &win32Pipe{win32File: response.f, path: l.path}, nil
|
||||
case <-l.doneCh:
|
||||
return nil, ErrPipeListenerClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) Close() error {
|
||||
select {
|
||||
case l.closeCh <- 1:
|
||||
<-l.doneCh
|
||||
case <-l.doneCh:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) Addr() net.Addr {
|
||||
return pipeAddress(l.path)
|
||||
}
|
232
vendor/github.com/Microsoft/go-winio/pkg/guid/guid.go
generated
vendored
Normal file
232
vendor/github.com/Microsoft/go-winio/pkg/guid/guid.go
generated
vendored
Normal file
|
@ -0,0 +1,232 @@
|
|||
// Package guid provides a GUID type. The backing structure for a GUID is
|
||||
// identical to that used by the golang.org/x/sys/windows GUID type.
|
||||
// There are two main binary encodings used for a GUID, the big-endian encoding,
|
||||
// and the Windows (mixed-endian) encoding. See here for details:
|
||||
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding
|
||||
package guid
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha1" //nolint:gosec // not used for secure application
|
||||
"encoding"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer -type=Variant -trimprefix=Variant -linecomment
|
||||
|
||||
// Variant specifies which GUID variant (or "type") of the GUID. It determines
|
||||
// how the entirety of the rest of the GUID is interpreted.
|
||||
type Variant uint8
|
||||
|
||||
// The variants specified by RFC 4122 section 4.1.1.
|
||||
const (
|
||||
// VariantUnknown specifies a GUID variant which does not conform to one of
|
||||
// the variant encodings specified in RFC 4122.
|
||||
VariantUnknown Variant = iota
|
||||
VariantNCS
|
||||
VariantRFC4122 // RFC 4122
|
||||
VariantMicrosoft
|
||||
VariantFuture
|
||||
)
|
||||
|
||||
// Version specifies how the bits in the GUID were generated. For instance, a
|
||||
// version 4 GUID is randomly generated, and a version 5 is generated from the
|
||||
// hash of an input string.
|
||||
type Version uint8
|
||||
|
||||
func (v Version) String() string {
|
||||
return strconv.FormatUint(uint64(v), 10)
|
||||
}
|
||||
|
||||
var _ = (encoding.TextMarshaler)(GUID{})
|
||||
var _ = (encoding.TextUnmarshaler)(&GUID{})
|
||||
|
||||
// NewV4 returns a new version 4 (pseudorandom) GUID, as defined by RFC 4122.
|
||||
func NewV4() (GUID, error) {
|
||||
var b [16]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return GUID{}, err
|
||||
}
|
||||
|
||||
g := FromArray(b)
|
||||
g.setVersion(4) // Version 4 means randomly generated.
|
||||
g.setVariant(VariantRFC4122)
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// NewV5 returns a new version 5 (generated from a string via SHA-1 hashing)
|
||||
// GUID, as defined by RFC 4122. The RFC is unclear on the encoding of the name,
|
||||
// and the sample code treats it as a series of bytes, so we do the same here.
|
||||
//
|
||||
// Some implementations, such as those found on Windows, treat the name as a
|
||||
// big-endian UTF16 stream of bytes. If that is desired, the string can be
|
||||
// encoded as such before being passed to this function.
|
||||
func NewV5(namespace GUID, name []byte) (GUID, error) {
|
||||
b := sha1.New() //nolint:gosec // not used for secure application
|
||||
namespaceBytes := namespace.ToArray()
|
||||
b.Write(namespaceBytes[:])
|
||||
b.Write(name)
|
||||
|
||||
a := [16]byte{}
|
||||
copy(a[:], b.Sum(nil))
|
||||
|
||||
g := FromArray(a)
|
||||
g.setVersion(5) // Version 5 means generated from a string.
|
||||
g.setVariant(VariantRFC4122)
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func fromArray(b [16]byte, order binary.ByteOrder) GUID {
|
||||
var g GUID
|
||||
g.Data1 = order.Uint32(b[0:4])
|
||||
g.Data2 = order.Uint16(b[4:6])
|
||||
g.Data3 = order.Uint16(b[6:8])
|
||||
copy(g.Data4[:], b[8:16])
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GUID) toArray(order binary.ByteOrder) [16]byte {
|
||||
b := [16]byte{}
|
||||
order.PutUint32(b[0:4], g.Data1)
|
||||
order.PutUint16(b[4:6], g.Data2)
|
||||
order.PutUint16(b[6:8], g.Data3)
|
||||
copy(b[8:16], g.Data4[:])
|
||||
return b
|
||||
}
|
||||
|
||||
// FromArray constructs a GUID from a big-endian encoding array of 16 bytes.
|
||||
func FromArray(b [16]byte) GUID {
|
||||
return fromArray(b, binary.BigEndian)
|
||||
}
|
||||
|
||||
// ToArray returns an array of 16 bytes representing the GUID in big-endian
|
||||
// encoding.
|
||||
func (g GUID) ToArray() [16]byte {
|
||||
return g.toArray(binary.BigEndian)
|
||||
}
|
||||
|
||||
// FromWindowsArray constructs a GUID from a Windows encoding array of bytes.
|
||||
func FromWindowsArray(b [16]byte) GUID {
|
||||
return fromArray(b, binary.LittleEndian)
|
||||
}
|
||||
|
||||
// ToWindowsArray returns an array of 16 bytes representing the GUID in Windows
|
||||
// encoding.
|
||||
func (g GUID) ToWindowsArray() [16]byte {
|
||||
return g.toArray(binary.LittleEndian)
|
||||
}
|
||||
|
||||
func (g GUID) String() string {
|
||||
return fmt.Sprintf(
|
||||
"%08x-%04x-%04x-%04x-%012x",
|
||||
g.Data1,
|
||||
g.Data2,
|
||||
g.Data3,
|
||||
g.Data4[:2],
|
||||
g.Data4[2:])
|
||||
}
|
||||
|
||||
// FromString parses a string containing a GUID and returns the GUID. The only
|
||||
// format currently supported is the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
|
||||
// format.
|
||||
func FromString(s string) (GUID, error) {
|
||||
if len(s) != 36 {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
|
||||
var g GUID
|
||||
|
||||
data1, err := strconv.ParseUint(s[0:8], 16, 32)
|
||||
if err != nil {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
g.Data1 = uint32(data1)
|
||||
|
||||
data2, err := strconv.ParseUint(s[9:13], 16, 16)
|
||||
if err != nil {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
g.Data2 = uint16(data2)
|
||||
|
||||
data3, err := strconv.ParseUint(s[14:18], 16, 16)
|
||||
if err != nil {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
g.Data3 = uint16(data3)
|
||||
|
||||
for i, x := range []int{19, 21, 24, 26, 28, 30, 32, 34} {
|
||||
v, err := strconv.ParseUint(s[x:x+2], 16, 8)
|
||||
if err != nil {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
g.Data4[i] = uint8(v)
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (g *GUID) setVariant(v Variant) {
|
||||
d := g.Data4[0]
|
||||
switch v {
|
||||
case VariantNCS:
|
||||
d = (d & 0x7f)
|
||||
case VariantRFC4122:
|
||||
d = (d & 0x3f) | 0x80
|
||||
case VariantMicrosoft:
|
||||
d = (d & 0x1f) | 0xc0
|
||||
case VariantFuture:
|
||||
d = (d & 0x0f) | 0xe0
|
||||
case VariantUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid variant: %d", v))
|
||||
}
|
||||
g.Data4[0] = d
|
||||
}
|
||||
|
||||
// Variant returns the GUID variant, as defined in RFC 4122.
|
||||
func (g GUID) Variant() Variant {
|
||||
b := g.Data4[0]
|
||||
if b&0x80 == 0 {
|
||||
return VariantNCS
|
||||
} else if b&0xc0 == 0x80 {
|
||||
return VariantRFC4122
|
||||
} else if b&0xe0 == 0xc0 {
|
||||
return VariantMicrosoft
|
||||
} else if b&0xe0 == 0xe0 {
|
||||
return VariantFuture
|
||||
}
|
||||
return VariantUnknown
|
||||
}
|
||||
|
||||
func (g *GUID) setVersion(v Version) {
|
||||
g.Data3 = (g.Data3 & 0x0fff) | (uint16(v) << 12)
|
||||
}
|
||||
|
||||
// Version returns the GUID version, as defined in RFC 4122.
|
||||
func (g GUID) Version() Version {
|
||||
return Version((g.Data3 & 0xF000) >> 12)
|
||||
}
|
||||
|
||||
// MarshalText returns the textual representation of the GUID.
|
||||
func (g GUID) MarshalText() ([]byte, error) {
|
||||
return []byte(g.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText takes the textual representation of a GUID, and unmarhals it
|
||||
// into this GUID.
|
||||
func (g *GUID) UnmarshalText(text []byte) error {
|
||||
g2, err := FromString(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*g = g2
|
||||
return nil
|
||||
}
|
16
vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go
generated
vendored
Normal file
16
vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package guid
|
||||
|
||||
// GUID represents a GUID/UUID. It has the same structure as
|
||||
// golang.org/x/sys/windows.GUID so that it can be used with functions expecting
|
||||
// that type. It is defined as its own type as that is only available to builds
|
||||
// targeted at `windows`. The representation matches that used by native Windows
|
||||
// code.
|
||||
type GUID struct {
|
||||
Data1 uint32
|
||||
Data2 uint16
|
||||
Data3 uint16
|
||||
Data4 [8]byte
|
||||
}
|
13
vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.go
generated
vendored
Normal file
13
vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package guid
|
||||
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
// GUID represents a GUID/UUID. It has the same structure as
|
||||
// golang.org/x/sys/windows.GUID so that it can be used with functions expecting
|
||||
// that type. It is defined as its own type so that stringification and
|
||||
// marshaling can be supported. The representation matches that used by native
|
||||
// Windows code.
|
||||
type GUID windows.GUID
|
27
vendor/github.com/Microsoft/go-winio/pkg/guid/variant_string.go
generated
vendored
Normal file
27
vendor/github.com/Microsoft/go-winio/pkg/guid/variant_string.go
generated
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Code generated by "stringer -type=Variant -trimprefix=Variant -linecomment"; DO NOT EDIT.
|
||||
|
||||
package guid
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[VariantUnknown-0]
|
||||
_ = x[VariantNCS-1]
|
||||
_ = x[VariantRFC4122-2]
|
||||
_ = x[VariantMicrosoft-3]
|
||||
_ = x[VariantFuture-4]
|
||||
}
|
||||
|
||||
const _Variant_name = "UnknownNCSRFC 4122MicrosoftFuture"
|
||||
|
||||
var _Variant_index = [...]uint8{0, 7, 10, 18, 27, 33}
|
||||
|
||||
func (i Variant) String() string {
|
||||
if i >= Variant(len(_Variant_index)-1) {
|
||||
return "Variant(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _Variant_name[_Variant_index[i]:_Variant_index[i+1]]
|
||||
}
|
196
vendor/github.com/Microsoft/go-winio/privilege.go
generated
vendored
Normal file
196
vendor/github.com/Microsoft/go-winio/privilege.go
generated
vendored
Normal file
|
@ -0,0 +1,196 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unicode/utf16"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) [true] = advapi32.AdjustTokenPrivileges
|
||||
//sys impersonateSelf(level uint32) (err error) = advapi32.ImpersonateSelf
|
||||
//sys revertToSelf() (err error) = advapi32.RevertToSelf
|
||||
//sys openThreadToken(thread windows.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) = advapi32.OpenThreadToken
|
||||
//sys getCurrentThread() (h windows.Handle) = GetCurrentThread
|
||||
//sys lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) = advapi32.LookupPrivilegeValueW
|
||||
//sys lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) = advapi32.LookupPrivilegeNameW
|
||||
//sys lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) = advapi32.LookupPrivilegeDisplayNameW
|
||||
|
||||
const (
|
||||
//revive:disable-next-line:var-naming ALL_CAPS
|
||||
SE_PRIVILEGE_ENABLED = windows.SE_PRIVILEGE_ENABLED
|
||||
|
||||
//revive:disable-next-line:var-naming ALL_CAPS
|
||||
ERROR_NOT_ALL_ASSIGNED windows.Errno = windows.ERROR_NOT_ALL_ASSIGNED
|
||||
|
||||
SeBackupPrivilege = "SeBackupPrivilege"
|
||||
SeRestorePrivilege = "SeRestorePrivilege"
|
||||
SeSecurityPrivilege = "SeSecurityPrivilege"
|
||||
)
|
||||
|
||||
var (
|
||||
privNames = make(map[string]uint64)
|
||||
privNameMutex sync.Mutex
|
||||
)
|
||||
|
||||
// PrivilegeError represents an error enabling privileges.
|
||||
type PrivilegeError struct {
|
||||
privileges []uint64
|
||||
}
|
||||
|
||||
func (e *PrivilegeError) Error() string {
|
||||
s := "Could not enable privilege "
|
||||
if len(e.privileges) > 1 {
|
||||
s = "Could not enable privileges "
|
||||
}
|
||||
for i, p := range e.privileges {
|
||||
if i != 0 {
|
||||
s += ", "
|
||||
}
|
||||
s += `"`
|
||||
s += getPrivilegeName(p)
|
||||
s += `"`
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// RunWithPrivilege enables a single privilege for a function call.
|
||||
func RunWithPrivilege(name string, fn func() error) error {
|
||||
return RunWithPrivileges([]string{name}, fn)
|
||||
}
|
||||
|
||||
// RunWithPrivileges enables privileges for a function call.
|
||||
func RunWithPrivileges(names []string, fn func() error) error {
|
||||
privileges, err := mapPrivileges(names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
token, err := newThreadToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer releaseThreadToken(token)
|
||||
err = adjustPrivileges(token, privileges, SE_PRIVILEGE_ENABLED)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn()
|
||||
}
|
||||
|
||||
func mapPrivileges(names []string) ([]uint64, error) {
|
||||
privileges := make([]uint64, 0, len(names))
|
||||
privNameMutex.Lock()
|
||||
defer privNameMutex.Unlock()
|
||||
for _, name := range names {
|
||||
p, ok := privNames[name]
|
||||
if !ok {
|
||||
err := lookupPrivilegeValue("", name, &p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privNames[name] = p
|
||||
}
|
||||
privileges = append(privileges, p)
|
||||
}
|
||||
return privileges, nil
|
||||
}
|
||||
|
||||
// EnableProcessPrivileges enables privileges globally for the process.
|
||||
func EnableProcessPrivileges(names []string) error {
|
||||
return enableDisableProcessPrivilege(names, SE_PRIVILEGE_ENABLED)
|
||||
}
|
||||
|
||||
// DisableProcessPrivileges disables privileges globally for the process.
|
||||
func DisableProcessPrivileges(names []string) error {
|
||||
return enableDisableProcessPrivilege(names, 0)
|
||||
}
|
||||
|
||||
func enableDisableProcessPrivilege(names []string, action uint32) error {
|
||||
privileges, err := mapPrivileges(names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := windows.CurrentProcess()
|
||||
var token windows.Token
|
||||
err = windows.OpenProcessToken(p, windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer token.Close()
|
||||
return adjustPrivileges(token, privileges, action)
|
||||
}
|
||||
|
||||
func adjustPrivileges(token windows.Token, privileges []uint64, action uint32) error {
|
||||
var b bytes.Buffer
|
||||
_ = binary.Write(&b, binary.LittleEndian, uint32(len(privileges)))
|
||||
for _, p := range privileges {
|
||||
_ = binary.Write(&b, binary.LittleEndian, p)
|
||||
_ = binary.Write(&b, binary.LittleEndian, action)
|
||||
}
|
||||
prevState := make([]byte, b.Len())
|
||||
reqSize := uint32(0)
|
||||
success, err := adjustTokenPrivileges(token, false, &b.Bytes()[0], uint32(len(prevState)), &prevState[0], &reqSize)
|
||||
if !success {
|
||||
return err
|
||||
}
|
||||
if err == ERROR_NOT_ALL_ASSIGNED { //nolint:errorlint // err is Errno
|
||||
return &PrivilegeError{privileges}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPrivilegeName(luid uint64) string {
|
||||
var nameBuffer [256]uint16
|
||||
bufSize := uint32(len(nameBuffer))
|
||||
err := lookupPrivilegeName("", &luid, &nameBuffer[0], &bufSize)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("<unknown privilege %d>", luid)
|
||||
}
|
||||
|
||||
var displayNameBuffer [256]uint16
|
||||
displayBufSize := uint32(len(displayNameBuffer))
|
||||
var langID uint32
|
||||
err = lookupPrivilegeDisplayName("", &nameBuffer[0], &displayNameBuffer[0], &displayBufSize, &langID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("<unknown privilege %s>", string(utf16.Decode(nameBuffer[:bufSize])))
|
||||
}
|
||||
|
||||
return string(utf16.Decode(displayNameBuffer[:displayBufSize]))
|
||||
}
|
||||
|
||||
func newThreadToken() (windows.Token, error) {
|
||||
err := impersonateSelf(windows.SecurityImpersonation)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var token windows.Token
|
||||
err = openThreadToken(getCurrentThread(), windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, false, &token)
|
||||
if err != nil {
|
||||
rerr := revertToSelf()
|
||||
if rerr != nil {
|
||||
panic(rerr)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func releaseThreadToken(h windows.Token) {
|
||||
err := revertToSelf()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
h.Close()
|
||||
}
|
131
vendor/github.com/Microsoft/go-winio/reparse.go
generated
vendored
Normal file
131
vendor/github.com/Microsoft/go-winio/reparse.go
generated
vendored
Normal file
|
@ -0,0 +1,131 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
reparseTagMountPoint = 0xA0000003
|
||||
reparseTagSymlink = 0xA000000C
|
||||
)
|
||||
|
||||
type reparseDataBuffer struct {
|
||||
ReparseTag uint32
|
||||
ReparseDataLength uint16
|
||||
Reserved uint16
|
||||
SubstituteNameOffset uint16
|
||||
SubstituteNameLength uint16
|
||||
PrintNameOffset uint16
|
||||
PrintNameLength uint16
|
||||
}
|
||||
|
||||
// ReparsePoint describes a Win32 symlink or mount point.
|
||||
type ReparsePoint struct {
|
||||
Target string
|
||||
IsMountPoint bool
|
||||
}
|
||||
|
||||
// UnsupportedReparsePointError is returned when trying to decode a non-symlink or
|
||||
// mount point reparse point.
|
||||
type UnsupportedReparsePointError struct {
|
||||
Tag uint32
|
||||
}
|
||||
|
||||
func (e *UnsupportedReparsePointError) Error() string {
|
||||
return fmt.Sprintf("unsupported reparse point %x", e.Tag)
|
||||
}
|
||||
|
||||
// DecodeReparsePoint decodes a Win32 REPARSE_DATA_BUFFER structure containing either a symlink
|
||||
// or a mount point.
|
||||
func DecodeReparsePoint(b []byte) (*ReparsePoint, error) {
|
||||
tag := binary.LittleEndian.Uint32(b[0:4])
|
||||
return DecodeReparsePointData(tag, b[8:])
|
||||
}
|
||||
|
||||
func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
|
||||
isMountPoint := false
|
||||
switch tag {
|
||||
case reparseTagMountPoint:
|
||||
isMountPoint = true
|
||||
case reparseTagSymlink:
|
||||
default:
|
||||
return nil, &UnsupportedReparsePointError{tag}
|
||||
}
|
||||
nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6])
|
||||
if !isMountPoint {
|
||||
nameOffset += 4
|
||||
}
|
||||
nameLength := binary.LittleEndian.Uint16(b[6:8])
|
||||
name := make([]uint16, nameLength/2)
|
||||
err := binary.Read(bytes.NewReader(b[nameOffset:nameOffset+nameLength]), binary.LittleEndian, &name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil
|
||||
}
|
||||
|
||||
func isDriveLetter(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
|
||||
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or
|
||||
// mount point.
|
||||
func EncodeReparsePoint(rp *ReparsePoint) []byte {
|
||||
// Generate an NT path and determine if this is a relative path.
|
||||
var ntTarget string
|
||||
relative := false
|
||||
if strings.HasPrefix(rp.Target, `\\?\`) {
|
||||
ntTarget = `\??\` + rp.Target[4:]
|
||||
} else if strings.HasPrefix(rp.Target, `\\`) {
|
||||
ntTarget = `\??\UNC\` + rp.Target[2:]
|
||||
} else if len(rp.Target) >= 2 && isDriveLetter(rp.Target[0]) && rp.Target[1] == ':' {
|
||||
ntTarget = `\??\` + rp.Target
|
||||
} else {
|
||||
ntTarget = rp.Target
|
||||
relative = true
|
||||
}
|
||||
|
||||
// The paths must be NUL-terminated even though they are counted strings.
|
||||
target16 := utf16.Encode([]rune(rp.Target + "\x00"))
|
||||
ntTarget16 := utf16.Encode([]rune(ntTarget + "\x00"))
|
||||
|
||||
size := int(unsafe.Sizeof(reparseDataBuffer{})) - 8
|
||||
size += len(ntTarget16)*2 + len(target16)*2
|
||||
|
||||
tag := uint32(reparseTagMountPoint)
|
||||
if !rp.IsMountPoint {
|
||||
tag = reparseTagSymlink
|
||||
size += 4 // Add room for symlink flags
|
||||
}
|
||||
|
||||
data := reparseDataBuffer{
|
||||
ReparseTag: tag,
|
||||
ReparseDataLength: uint16(size),
|
||||
SubstituteNameOffset: 0,
|
||||
SubstituteNameLength: uint16((len(ntTarget16) - 1) * 2),
|
||||
PrintNameOffset: uint16(len(ntTarget16) * 2),
|
||||
PrintNameLength: uint16((len(target16) - 1) * 2),
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
_ = binary.Write(&b, binary.LittleEndian, &data)
|
||||
if !rp.IsMountPoint {
|
||||
flags := uint32(0)
|
||||
if relative {
|
||||
flags |= 1
|
||||
}
|
||||
_ = binary.Write(&b, binary.LittleEndian, flags)
|
||||
}
|
||||
|
||||
_ = binary.Write(&b, binary.LittleEndian, ntTarget16)
|
||||
_ = binary.Write(&b, binary.LittleEndian, target16)
|
||||
return b.Bytes()
|
||||
}
|
133
vendor/github.com/Microsoft/go-winio/sd.go
generated
vendored
Normal file
133
vendor/github.com/Microsoft/go-winio/sd.go
generated
vendored
Normal file
|
@ -0,0 +1,133 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountNameW
|
||||
//sys lookupAccountSid(systemName *uint16, sid *byte, name *uint16, nameSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountSidW
|
||||
//sys convertSidToStringSid(sid *byte, str **uint16) (err error) = advapi32.ConvertSidToStringSidW
|
||||
//sys convertStringSidToSid(str *uint16, sid **byte) (err error) = advapi32.ConvertStringSidToSidW
|
||||
|
||||
type AccountLookupError struct {
|
||||
Name string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *AccountLookupError) Error() string {
|
||||
if e.Name == "" {
|
||||
return "lookup account: empty account name specified"
|
||||
}
|
||||
var s string
|
||||
switch {
|
||||
case errors.Is(e.Err, windows.ERROR_INVALID_SID):
|
||||
s = "the security ID structure is invalid"
|
||||
case errors.Is(e.Err, windows.ERROR_NONE_MAPPED):
|
||||
s = "not found"
|
||||
default:
|
||||
s = e.Err.Error()
|
||||
}
|
||||
return "lookup account " + e.Name + ": " + s
|
||||
}
|
||||
|
||||
func (e *AccountLookupError) Unwrap() error { return e.Err }
|
||||
|
||||
type SddlConversionError struct {
|
||||
Sddl string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *SddlConversionError) Error() string {
|
||||
return "convert " + e.Sddl + ": " + e.Err.Error()
|
||||
}
|
||||
|
||||
func (e *SddlConversionError) Unwrap() error { return e.Err }
|
||||
|
||||
// LookupSidByName looks up the SID of an account by name
|
||||
//
|
||||
//revive:disable-next-line:var-naming SID, not Sid
|
||||
func LookupSidByName(name string) (sid string, err error) {
|
||||
if name == "" {
|
||||
return "", &AccountLookupError{name, windows.ERROR_NONE_MAPPED}
|
||||
}
|
||||
|
||||
var sidSize, sidNameUse, refDomainSize uint32
|
||||
err = lookupAccountName(nil, name, nil, &sidSize, nil, &refDomainSize, &sidNameUse)
|
||||
if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno
|
||||
return "", &AccountLookupError{name, err}
|
||||
}
|
||||
sidBuffer := make([]byte, sidSize)
|
||||
refDomainBuffer := make([]uint16, refDomainSize)
|
||||
err = lookupAccountName(nil, name, &sidBuffer[0], &sidSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse)
|
||||
if err != nil {
|
||||
return "", &AccountLookupError{name, err}
|
||||
}
|
||||
var strBuffer *uint16
|
||||
err = convertSidToStringSid(&sidBuffer[0], &strBuffer)
|
||||
if err != nil {
|
||||
return "", &AccountLookupError{name, err}
|
||||
}
|
||||
sid = windows.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(strBuffer))[:])
|
||||
_, _ = windows.LocalFree(windows.Handle(unsafe.Pointer(strBuffer)))
|
||||
return sid, nil
|
||||
}
|
||||
|
||||
// LookupNameBySid looks up the name of an account by SID
|
||||
//
|
||||
//revive:disable-next-line:var-naming SID, not Sid
|
||||
func LookupNameBySid(sid string) (name string, err error) {
|
||||
if sid == "" {
|
||||
return "", &AccountLookupError{sid, windows.ERROR_NONE_MAPPED}
|
||||
}
|
||||
|
||||
sidBuffer, err := windows.UTF16PtrFromString(sid)
|
||||
if err != nil {
|
||||
return "", &AccountLookupError{sid, err}
|
||||
}
|
||||
|
||||
var sidPtr *byte
|
||||
if err = convertStringSidToSid(sidBuffer, &sidPtr); err != nil {
|
||||
return "", &AccountLookupError{sid, err}
|
||||
}
|
||||
defer windows.LocalFree(windows.Handle(unsafe.Pointer(sidPtr))) //nolint:errcheck
|
||||
|
||||
var nameSize, refDomainSize, sidNameUse uint32
|
||||
err = lookupAccountSid(nil, sidPtr, nil, &nameSize, nil, &refDomainSize, &sidNameUse)
|
||||
if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno
|
||||
return "", &AccountLookupError{sid, err}
|
||||
}
|
||||
|
||||
nameBuffer := make([]uint16, nameSize)
|
||||
refDomainBuffer := make([]uint16, refDomainSize)
|
||||
err = lookupAccountSid(nil, sidPtr, &nameBuffer[0], &nameSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse)
|
||||
if err != nil {
|
||||
return "", &AccountLookupError{sid, err}
|
||||
}
|
||||
|
||||
name = windows.UTF16ToString(nameBuffer)
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func SddlToSecurityDescriptor(sddl string) ([]byte, error) {
|
||||
sd, err := windows.SecurityDescriptorFromString(sddl)
|
||||
if err != nil {
|
||||
return nil, &SddlConversionError{Sddl: sddl, Err: err}
|
||||
}
|
||||
b := unsafe.Slice((*byte)(unsafe.Pointer(sd)), sd.Length())
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func SecurityDescriptorToSddl(sd []byte) (string, error) {
|
||||
if l := int(unsafe.Sizeof(windows.SECURITY_DESCRIPTOR{})); len(sd) < l {
|
||||
return "", fmt.Errorf("SecurityDescriptor (%d) smaller than expected (%d): %w", len(sd), l, windows.ERROR_INCORRECT_SIZE)
|
||||
}
|
||||
s := (*windows.SECURITY_DESCRIPTOR)(unsafe.Pointer(&sd[0]))
|
||||
return s.String(), nil
|
||||
}
|
5
vendor/github.com/Microsoft/go-winio/syscall.go
generated
vendored
Normal file
5
vendor/github.com/Microsoft/go-winio/syscall.go
generated
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
//go:build windows
|
||||
|
||||
package winio
|
||||
|
||||
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go
|
378
vendor/github.com/Microsoft/go-winio/zsyscall_windows.go
generated
vendored
Normal file
378
vendor/github.com/Microsoft/go-winio/zsyscall_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,378 @@
|
|||
//go:build windows
|
||||
|
||||
// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT.
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
modntdll = windows.NewLazySystemDLL("ntdll.dll")
|
||||
modws2_32 = windows.NewLazySystemDLL("ws2_32.dll")
|
||||
|
||||
procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges")
|
||||
procConvertSidToStringSidW = modadvapi32.NewProc("ConvertSidToStringSidW")
|
||||
procConvertStringSidToSidW = modadvapi32.NewProc("ConvertStringSidToSidW")
|
||||
procImpersonateSelf = modadvapi32.NewProc("ImpersonateSelf")
|
||||
procLookupAccountNameW = modadvapi32.NewProc("LookupAccountNameW")
|
||||
procLookupAccountSidW = modadvapi32.NewProc("LookupAccountSidW")
|
||||
procLookupPrivilegeDisplayNameW = modadvapi32.NewProc("LookupPrivilegeDisplayNameW")
|
||||
procLookupPrivilegeNameW = modadvapi32.NewProc("LookupPrivilegeNameW")
|
||||
procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW")
|
||||
procOpenThreadToken = modadvapi32.NewProc("OpenThreadToken")
|
||||
procRevertToSelf = modadvapi32.NewProc("RevertToSelf")
|
||||
procBackupRead = modkernel32.NewProc("BackupRead")
|
||||
procBackupWrite = modkernel32.NewProc("BackupWrite")
|
||||
procCancelIoEx = modkernel32.NewProc("CancelIoEx")
|
||||
procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe")
|
||||
procCreateIoCompletionPort = modkernel32.NewProc("CreateIoCompletionPort")
|
||||
procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW")
|
||||
procDisconnectNamedPipe = modkernel32.NewProc("DisconnectNamedPipe")
|
||||
procGetCurrentThread = modkernel32.NewProc("GetCurrentThread")
|
||||
procGetNamedPipeHandleStateW = modkernel32.NewProc("GetNamedPipeHandleStateW")
|
||||
procGetNamedPipeInfo = modkernel32.NewProc("GetNamedPipeInfo")
|
||||
procGetQueuedCompletionStatus = modkernel32.NewProc("GetQueuedCompletionStatus")
|
||||
procSetFileCompletionNotificationModes = modkernel32.NewProc("SetFileCompletionNotificationModes")
|
||||
procNtCreateNamedPipeFile = modntdll.NewProc("NtCreateNamedPipeFile")
|
||||
procRtlDefaultNpAcl = modntdll.NewProc("RtlDefaultNpAcl")
|
||||
procRtlDosPathNameToNtPathName_U = modntdll.NewProc("RtlDosPathNameToNtPathName_U")
|
||||
procRtlNtStatusToDosErrorNoTeb = modntdll.NewProc("RtlNtStatusToDosErrorNoTeb")
|
||||
procWSAGetOverlappedResult = modws2_32.NewProc("WSAGetOverlappedResult")
|
||||
)
|
||||
|
||||
func adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) {
|
||||
var _p0 uint32
|
||||
if releaseAll {
|
||||
_p0 = 1
|
||||
}
|
||||
r0, _, e1 := syscall.SyscallN(procAdjustTokenPrivileges.Addr(), uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize)))
|
||||
success = r0 != 0
|
||||
if true {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func convertSidToStringSid(sid *byte, str **uint16) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procConvertSidToStringSidW.Addr(), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(str)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func convertStringSidToSid(str *uint16, sid **byte) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procConvertStringSidToSidW.Addr(), uintptr(unsafe.Pointer(str)), uintptr(unsafe.Pointer(sid)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func impersonateSelf(level uint32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procImpersonateSelf.Addr(), uintptr(level))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) {
|
||||
var _p0 *uint16
|
||||
_p0, err = syscall.UTF16PtrFromString(accountName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return _lookupAccountName(systemName, _p0, sid, sidSize, refDomain, refDomainSize, sidNameUse)
|
||||
}
|
||||
|
||||
func _lookupAccountName(systemName *uint16, accountName *uint16, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procLookupAccountNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(accountName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(sidSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lookupAccountSid(systemName *uint16, sid *byte, name *uint16, nameSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procLookupAccountSidW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) {
|
||||
var _p0 *uint16
|
||||
_p0, err = syscall.UTF16PtrFromString(systemName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return _lookupPrivilegeDisplayName(_p0, name, buffer, size, languageId)
|
||||
}
|
||||
|
||||
func _lookupPrivilegeDisplayName(systemName *uint16, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procLookupPrivilegeDisplayNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)), uintptr(unsafe.Pointer(languageId)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) {
|
||||
var _p0 *uint16
|
||||
_p0, err = syscall.UTF16PtrFromString(systemName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return _lookupPrivilegeName(_p0, luid, buffer, size)
|
||||
}
|
||||
|
||||
func _lookupPrivilegeName(systemName *uint16, luid *uint64, buffer *uint16, size *uint32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procLookupPrivilegeNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(luid)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) {
|
||||
var _p0 *uint16
|
||||
_p0, err = syscall.UTF16PtrFromString(systemName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var _p1 *uint16
|
||||
_p1, err = syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return _lookupPrivilegeValue(_p0, _p1, luid)
|
||||
}
|
||||
|
||||
func _lookupPrivilegeValue(systemName *uint16, name *uint16, luid *uint64) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procLookupPrivilegeValueW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func openThreadToken(thread windows.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) {
|
||||
var _p0 uint32
|
||||
if openAsSelf {
|
||||
_p0 = 1
|
||||
}
|
||||
r1, _, e1 := syscall.SyscallN(procOpenThreadToken.Addr(), uintptr(thread), uintptr(accessMask), uintptr(_p0), uintptr(unsafe.Pointer(token)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func revertToSelf() (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procRevertToSelf.Addr())
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) {
|
||||
var _p0 *byte
|
||||
if len(b) > 0 {
|
||||
_p0 = &b[0]
|
||||
}
|
||||
var _p1 uint32
|
||||
if abort {
|
||||
_p1 = 1
|
||||
}
|
||||
var _p2 uint32
|
||||
if processSecurity {
|
||||
_p2 = 1
|
||||
}
|
||||
r1, _, e1 := syscall.SyscallN(procBackupRead.Addr(), uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesRead)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) {
|
||||
var _p0 *byte
|
||||
if len(b) > 0 {
|
||||
_p0 = &b[0]
|
||||
}
|
||||
var _p1 uint32
|
||||
if abort {
|
||||
_p1 = 1
|
||||
}
|
||||
var _p2 uint32
|
||||
if processSecurity {
|
||||
_p2 = 1
|
||||
}
|
||||
r1, _, e1 := syscall.SyscallN(procBackupWrite.Addr(), uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesWritten)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procCancelIoEx.Addr(), uintptr(file), uintptr(unsafe.Pointer(o)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func connectNamedPipe(pipe windows.Handle, o *windows.Overlapped) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procConnectNamedPipe.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(o)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) {
|
||||
r0, _, e1 := syscall.SyscallN(procCreateIoCompletionPort.Addr(), uintptr(file), uintptr(port), uintptr(key), uintptr(threadCount))
|
||||
newport = windows.Handle(r0)
|
||||
if newport == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) {
|
||||
var _p0 *uint16
|
||||
_p0, err = syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return _createNamedPipe(_p0, flags, pipeMode, maxInstances, outSize, inSize, defaultTimeout, sa)
|
||||
}
|
||||
|
||||
func _createNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) {
|
||||
r0, _, e1 := syscall.SyscallN(procCreateNamedPipeW.Addr(), uintptr(unsafe.Pointer(name)), uintptr(flags), uintptr(pipeMode), uintptr(maxInstances), uintptr(outSize), uintptr(inSize), uintptr(defaultTimeout), uintptr(unsafe.Pointer(sa)))
|
||||
handle = windows.Handle(r0)
|
||||
if handle == windows.InvalidHandle {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func disconnectNamedPipe(pipe windows.Handle) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procDisconnectNamedPipe.Addr(), uintptr(pipe))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getCurrentThread() (h windows.Handle) {
|
||||
r0, _, _ := syscall.SyscallN(procGetCurrentThread.Addr())
|
||||
h = windows.Handle(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func getNamedPipeHandleState(pipe windows.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procGetNamedPipeHandleStateW.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(state)), uintptr(unsafe.Pointer(curInstances)), uintptr(unsafe.Pointer(maxCollectionCount)), uintptr(unsafe.Pointer(collectDataTimeout)), uintptr(unsafe.Pointer(userName)), uintptr(maxUserNameSize))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getNamedPipeInfo(pipe windows.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procGetNamedPipeInfo.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(flags)), uintptr(unsafe.Pointer(outSize)), uintptr(unsafe.Pointer(inSize)), uintptr(unsafe.Pointer(maxInstances)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procGetQueuedCompletionStatus.Addr(), uintptr(port), uintptr(unsafe.Pointer(bytes)), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(o)), uintptr(timeout))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procSetFileCompletionNotificationModes.Addr(), uintptr(h), uintptr(flags))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ntCreateNamedPipeFile(pipe *windows.Handle, access ntAccessMask, oa *objectAttributes, iosb *ioStatusBlock, share ntFileShareMode, disposition ntFileCreationDisposition, options ntFileOptions, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) {
|
||||
r0, _, _ := syscall.SyscallN(procNtCreateNamedPipeFile.Addr(), uintptr(unsafe.Pointer(pipe)), uintptr(access), uintptr(unsafe.Pointer(oa)), uintptr(unsafe.Pointer(iosb)), uintptr(share), uintptr(disposition), uintptr(options), uintptr(typ), uintptr(readMode), uintptr(completionMode), uintptr(maxInstances), uintptr(inboundQuota), uintptr(outputQuota), uintptr(unsafe.Pointer(timeout)))
|
||||
status = ntStatus(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func rtlDefaultNpAcl(dacl *uintptr) (status ntStatus) {
|
||||
r0, _, _ := syscall.SyscallN(procRtlDefaultNpAcl.Addr(), uintptr(unsafe.Pointer(dacl)))
|
||||
status = ntStatus(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func rtlDosPathNameToNtPathName(name *uint16, ntName *unicodeString, filePart uintptr, reserved uintptr) (status ntStatus) {
|
||||
r0, _, _ := syscall.SyscallN(procRtlDosPathNameToNtPathName_U.Addr(), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(ntName)), uintptr(filePart), uintptr(reserved))
|
||||
status = ntStatus(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func rtlNtStatusToDosError(status ntStatus) (winerr error) {
|
||||
r0, _, _ := syscall.SyscallN(procRtlNtStatusToDosErrorNoTeb.Addr(), uintptr(status))
|
||||
if r0 != 0 {
|
||||
winerr = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) {
|
||||
var _p0 uint32
|
||||
if wait {
|
||||
_p0 = 1
|
||||
}
|
||||
r1, _, e1 := syscall.SyscallN(procWSAGetOverlappedResult.Addr(), uintptr(h), uintptr(unsafe.Pointer(o)), uintptr(unsafe.Pointer(bytes)), uintptr(_p0), uintptr(unsafe.Pointer(flags)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
4
vendor/github.com/clbanning/mxj/v2/.travis.yml
generated
vendored
Normal file
4
vendor/github.com/clbanning/mxj/v2/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.x
|
22
vendor/github.com/clbanning/mxj/v2/LICENSE
generated
vendored
Normal file
22
vendor/github.com/clbanning/mxj/v2/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
Copyright (c) 2012-2021 Charles Banning <clbanning@gmail.com>. All rights reserved.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
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.
|
||||
|
201
vendor/github.com/clbanning/mxj/v2/anyxml.go
generated
vendored
Normal file
201
vendor/github.com/clbanning/mxj/v2/anyxml.go
generated
vendored
Normal file
|
@ -0,0 +1,201 @@
|
|||
package mxj
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultElementTag = "element"
|
||||
)
|
||||
|
||||
// Encode arbitrary value as XML.
|
||||
//
|
||||
// Note: unmarshaling the resultant
|
||||
// XML may not return the original value, since tag labels may have been injected
|
||||
// to create the XML representation of the value.
|
||||
/*
|
||||
Encode an arbitrary JSON object.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/clbanning/mxj"
|
||||
)
|
||||
|
||||
func main() {
|
||||
jsondata := []byte(`[
|
||||
{ "somekey":"somevalue" },
|
||||
"string",
|
||||
3.14159265,
|
||||
true
|
||||
]`)
|
||||
var i interface{}
|
||||
err := json.Unmarshal(jsondata, &i)
|
||||
if err != nil {
|
||||
// do something
|
||||
}
|
||||
x, err := mxj.AnyXmlIndent(i, "", " ", "mydoc")
|
||||
if err != nil {
|
||||
// do something else
|
||||
}
|
||||
fmt.Println(string(x))
|
||||
}
|
||||
|
||||
output:
|
||||
<mydoc>
|
||||
<somekey>somevalue</somekey>
|
||||
<element>string</element>
|
||||
<element>3.14159265</element>
|
||||
<element>true</element>
|
||||
</mydoc>
|
||||
|
||||
An extreme example is available in examples/goofy_map.go.
|
||||
*/
|
||||
// Alternative values for DefaultRootTag and DefaultElementTag can be set as:
|
||||
// AnyXml( v, myRootTag, myElementTag).
|
||||
func AnyXml(v interface{}, tags ...string) ([]byte, error) {
|
||||
var rt, et string
|
||||
if len(tags) == 1 || len(tags) == 2 {
|
||||
rt = tags[0]
|
||||
} else {
|
||||
rt = DefaultRootTag
|
||||
}
|
||||
if len(tags) == 2 {
|
||||
et = tags[1]
|
||||
} else {
|
||||
et = DefaultElementTag
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
if useGoXmlEmptyElemSyntax {
|
||||
return []byte("<" + rt + "></" + rt + ">"), nil
|
||||
}
|
||||
return []byte("<" + rt + "/>"), nil
|
||||
}
|
||||
if reflect.TypeOf(v).Kind() == reflect.Struct {
|
||||
return xml.Marshal(v)
|
||||
}
|
||||
|
||||
var err error
|
||||
s := new(bytes.Buffer)
|
||||
p := new(pretty)
|
||||
|
||||
var b []byte
|
||||
switch v.(type) {
|
||||
case []interface{}:
|
||||
if _, err = s.WriteString("<" + rt + ">"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, vv := range v.([]interface{}) {
|
||||
switch vv.(type) {
|
||||
case map[string]interface{}:
|
||||
m := vv.(map[string]interface{})
|
||||
if len(m) == 1 {
|
||||
for tag, val := range m {
|
||||
err = marshalMapToXmlIndent(false, s, tag, val, p)
|
||||
}
|
||||
} else {
|
||||
err = marshalMapToXmlIndent(false, s, et, vv, p)
|
||||
}
|
||||
default:
|
||||
err = marshalMapToXmlIndent(false, s, et, vv, p)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if _, err = s.WriteString("</" + rt + ">"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b = s.Bytes()
|
||||
case map[string]interface{}:
|
||||
m := Map(v.(map[string]interface{}))
|
||||
b, err = m.Xml(rt)
|
||||
default:
|
||||
err = marshalMapToXmlIndent(false, s, rt, v, p)
|
||||
b = s.Bytes()
|
||||
}
|
||||
|
||||
return b, err
|
||||
}
|
||||
|
||||
// Encode an arbitrary value as a pretty XML string.
|
||||
// Alternative values for DefaultRootTag and DefaultElementTag can be set as:
|
||||
// AnyXmlIndent( v, "", " ", myRootTag, myElementTag).
|
||||
func AnyXmlIndent(v interface{}, prefix, indent string, tags ...string) ([]byte, error) {
|
||||
var rt, et string
|
||||
if len(tags) == 1 || len(tags) == 2 {
|
||||
rt = tags[0]
|
||||
} else {
|
||||
rt = DefaultRootTag
|
||||
}
|
||||
if len(tags) == 2 {
|
||||
et = tags[1]
|
||||
} else {
|
||||
et = DefaultElementTag
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
if useGoXmlEmptyElemSyntax {
|
||||
return []byte(prefix + "<" + rt + "></" + rt + ">"), nil
|
||||
}
|
||||
return []byte(prefix + "<" + rt + "/>"), nil
|
||||
}
|
||||
if reflect.TypeOf(v).Kind() == reflect.Struct {
|
||||
return xml.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
var err error
|
||||
s := new(bytes.Buffer)
|
||||
p := new(pretty)
|
||||
p.indent = indent
|
||||
p.padding = prefix
|
||||
|
||||
var b []byte
|
||||
switch v.(type) {
|
||||
case []interface{}:
|
||||
if _, err = s.WriteString("<" + rt + ">\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Indent()
|
||||
for _, vv := range v.([]interface{}) {
|
||||
switch vv.(type) {
|
||||
case map[string]interface{}:
|
||||
m := vv.(map[string]interface{})
|
||||
if len(m) == 1 {
|
||||
for tag, val := range m {
|
||||
err = marshalMapToXmlIndent(true, s, tag, val, p)
|
||||
}
|
||||
} else {
|
||||
p.start = 1 // we 1 tag in
|
||||
err = marshalMapToXmlIndent(true, s, et, vv, p)
|
||||
// *s += "\n"
|
||||
if _, err = s.WriteString("\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
default:
|
||||
p.start = 0 // in case trailing p.start = 1
|
||||
err = marshalMapToXmlIndent(true, s, et, vv, p)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if _, err = s.WriteString(`</` + rt + `>`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b = s.Bytes()
|
||||
case map[string]interface{}:
|
||||
m := Map(v.(map[string]interface{}))
|
||||
b, err = m.XmlIndent(prefix, indent, rt)
|
||||
default:
|
||||
err = marshalMapToXmlIndent(true, s, rt, v, p)
|
||||
b = s.Bytes()
|
||||
}
|
||||
|
||||
return b, err
|
||||
}
|
54
vendor/github.com/clbanning/mxj/v2/atomFeedString.xml
generated
vendored
Normal file
54
vendor/github.com/clbanning/mxj/v2/atomFeedString.xml
generated
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us" updated="2009-10-04T01:35:58+00:00"><title>Code Review - My issues</title><link href="http://codereview.appspot.com/" rel="alternate"></link><link href="http://codereview.appspot.com/rss/mine/rsc" rel="self"></link><id>http://codereview.appspot.com/</id><author><name>rietveld<></name></author><entry><title>rietveld: an attempt at pubsubhubbub
|
||||
</title><link href="http://codereview.appspot.com/126085" rel="alternate"></link><updated>2009-10-04T01:35:58+00:00</updated><author><name>email-address-removed</name></author><id>urn:md5:134d9179c41f806be79b3a5f7877d19a</id><summary type="html">
|
||||
An attempt at adding pubsubhubbub support to Rietveld.
|
||||
http://code.google.com/p/pubsubhubbub
|
||||
http://code.google.com/p/rietveld/issues/detail?id=155
|
||||
|
||||
The server side of the protocol is trivial:
|
||||
1. add a &lt;link rel=&quot;hub&quot; href=&quot;hub-server&quot;&gt; tag to all
|
||||
feeds that will be pubsubhubbubbed.
|
||||
2. every time one of those feeds changes, tell the hub
|
||||
with a simple POST request.
|
||||
|
||||
I have tested this by adding debug prints to a local hub
|
||||
server and checking that the server got the right publish
|
||||
requests.
|
||||
|
||||
I can&#39;t quite get the server to work, but I think the bug
|
||||
is not in my code. I think that the server expects to be
|
||||
able to grab the feed and see the feed&#39;s actual URL in
|
||||
the link rel=&quot;self&quot;, but the default value for that drops
|
||||
the :port from the URL, and I cannot for the life of me
|
||||
figure out how to get the Atom generator deep inside
|
||||
django not to do that, or even where it is doing that,
|
||||
or even what code is running to generate the Atom feed.
|
||||
(I thought I knew but I added some assert False statements
|
||||
and it kept running!)
|
||||
|
||||
Ignoring that particular problem, I would appreciate
|
||||
feedback on the right way to get the two values at
|
||||
the top of feeds.py marked NOTE(rsc).
|
||||
|
||||
|
||||
</summary></entry><entry><title>rietveld: correct tab handling
|
||||
</title><link href="http://codereview.appspot.com/124106" rel="alternate"></link><updated>2009-10-03T23:02:17+00:00</updated><author><name>email-address-removed</name></author><id>urn:md5:0a2a4f19bb815101f0ba2904aed7c35a</id><summary type="html">
|
||||
This fixes the buggy tab rendering that can be seen at
|
||||
http://codereview.appspot.com/116075/diff/1/2
|
||||
|
||||
The fundamental problem was that the tab code was
|
||||
not being told what column the text began in, so it
|
||||
didn&#39;t know where to put the tab stops. Another problem
|
||||
was that some of the code assumed that string byte
|
||||
offsets were the same as column offsets, which is only
|
||||
true if there are no tabs.
|
||||
|
||||
In the process of fixing this, I cleaned up the arguments
|
||||
to Fold and ExpandTabs and renamed them Break and
|
||||
_ExpandTabs so that I could be sure that I found all the
|
||||
call sites. I also wanted to verify that ExpandTabs was
|
||||
not being used from outside intra_region_diff.py.
|
||||
|
||||
|
||||
</summary></entry></feed> `
|
||||
|
143
vendor/github.com/clbanning/mxj/v2/doc.go
generated
vendored
Normal file
143
vendor/github.com/clbanning/mxj/v2/doc.go
generated
vendored
Normal file
|
@ -0,0 +1,143 @@
|
|||
// mxj - A collection of map[string]interface{} and associated XML and JSON utilities.
|
||||
// Copyright 2012-2019, Charles Banning. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file
|
||||
|
||||
/*
|
||||
Marshal/Unmarshal XML to/from map[string]interface{} values (and JSON); extract/modify values from maps by key or key-path, including wildcards.
|
||||
|
||||
mxj supplants the legacy x2j and j2x packages. The subpackage x2j-wrapper is provided to facilitate migrating from the x2j package. The x2j and j2x subpackages provide similar functionality of the old packages but are not function-name compatible with them.
|
||||
|
||||
Note: this library was designed for processing ad hoc anonymous messages. Bulk processing large data sets may be much more efficiently performed using the encoding/xml or encoding/json packages from Go's standard library directly.
|
||||
|
||||
Related Packages:
|
||||
checkxml: github.com/clbanning/checkxml provides functions for validating XML data.
|
||||
|
||||
Notes:
|
||||
2022.11.28: v2.7 - add SetGlobalKeyMapPrefix to change default prefix, '#', for default keys
|
||||
2022.11.20: v2.6 - add NewMapForattedXmlSeq for XML docs formatted with whitespace character
|
||||
2021.02.02: v2.5 - add XmlCheckIsValid toggle to force checking that the encoded XML is valid
|
||||
2020.12.14: v2.4 - add XMLEscapeCharsDecoder to preserve XML escaped characters in Map values
|
||||
2020.10.28: v2.3 - add TrimWhiteSpace option
|
||||
2020.05.01: v2.2 - optimize map to XML encoding for large XML docs.
|
||||
2019.07.04: v2.0 - remove unnecessary methods - mv.XmlWriterRaw, mv.XmlIndentWriterRaw - for Map and MapSeq.
|
||||
2019.07.04: Add MapSeq type and move associated functions and methods from Map to MapSeq.
|
||||
2019.01.21: DecodeSimpleValuesAsMap - decode to map[<tag>:map["#text":<value>]] rather than map[<tag>:<value>].
|
||||
2018.04.18: mv.Xml/mv.XmlIndent encodes non-map[string]interface{} map values - map[string]string, map[int]uint, etc.
|
||||
2018.03.29: mv.Gob/NewMapGob support gob encoding/decoding of Maps.
|
||||
2018.03.26: Added mxj/x2j-wrapper sub-package for migrating from legacy x2j package.
|
||||
2017.02.22: LeafNode paths can use ".N" syntax rather than "[N]" for list member indexing.
|
||||
2017.02.21: github.com/clbanning/checkxml provides functions for validating XML data.
|
||||
2017.02.10: SetFieldSeparator changes field separator for args in UpdateValuesForPath, ValuesFor... methods.
|
||||
2017.02.06: Support XMPP stream processing - HandleXMPPStreamTag().
|
||||
2016.11.07: Preserve name space prefix syntax in XmlSeq parser - NewMapXmlSeq(), etc.
|
||||
2016.06.25: Support overriding default XML attribute prefix, "-", in Map keys - SetAttrPrefix().
|
||||
2016.05.26: Support customization of xml.Decoder by exposing CustomDecoder variable.
|
||||
2016.03.19: Escape invalid chars when encoding XML attribute and element values - XMLEscapeChars().
|
||||
2016.03.02: By default decoding XML with float64 and bool value casting will not cast "NaN", "Inf", and "-Inf".
|
||||
To cast them to float64, first set flag with CastNanInf(true).
|
||||
2016.02.22: New mv.Root(), mv.Elements(), mv.Attributes methods let you examine XML document structure.
|
||||
2016.02.16: Add CoerceKeysToLower() option to handle tags with mixed capitalization.
|
||||
2016.02.12: Seek for first xml.StartElement token; only return error if io.EOF is reached first (handles BOM).
|
||||
2015-12-02: NewMapXmlSeq() with mv.XmlSeq() & co. will try to preserve structure of XML doc when re-encoding.
|
||||
2014-08-02: AnyXml() and AnyXmlIndent() will try to marshal arbitrary values to XML.
|
||||
|
||||
SUMMARY
|
||||
|
||||
type Map map[string]interface{}
|
||||
|
||||
Create a Map value, 'mv', from any map[string]interface{} value, 'v':
|
||||
mv := Map(v)
|
||||
|
||||
Unmarshal / marshal XML as a Map value, 'mv':
|
||||
mv, err := NewMapXml(xmlValue) // unmarshal
|
||||
xmlValue, err := mv.Xml() // marshal
|
||||
|
||||
Unmarshal XML from an io.Reader as a Map value, 'mv':
|
||||
mv, err := NewMapXmlReader(xmlReader) // repeated calls, as with an os.File Reader, will process stream
|
||||
mv, raw, err := NewMapXmlReaderRaw(xmlReader) // 'raw' is the raw XML that was decoded
|
||||
|
||||
Marshal Map value, 'mv', to an XML Writer (io.Writer):
|
||||
err := mv.XmlWriter(xmlWriter)
|
||||
raw, err := mv.XmlWriterRaw(xmlWriter) // 'raw' is the raw XML that was written on xmlWriter
|
||||
|
||||
Also, for prettified output:
|
||||
xmlValue, err := mv.XmlIndent(prefix, indent, ...)
|
||||
err := mv.XmlIndentWriter(xmlWriter, prefix, indent, ...)
|
||||
raw, err := mv.XmlIndentWriterRaw(xmlWriter, prefix, indent, ...)
|
||||
|
||||
Bulk process XML with error handling (note: handlers must return a boolean value):
|
||||
err := HandleXmlReader(xmlReader, mapHandler(Map), errHandler(error))
|
||||
err := HandleXmlReaderRaw(xmlReader, mapHandler(Map, []byte), errHandler(error, []byte))
|
||||
|
||||
Converting XML to JSON: see Examples for NewMapXml and HandleXmlReader.
|
||||
|
||||
There are comparable functions and methods for JSON processing.
|
||||
|
||||
Arbitrary structure values can be decoded to / encoded from Map values:
|
||||
mv, err := NewMapStruct(structVal)
|
||||
err := mv.Struct(structPointer)
|
||||
|
||||
To work with XML tag values, JSON or Map key values or structure field values, decode the XML, JSON
|
||||
or structure to a Map value, 'mv', or cast a map[string]interface{} value to a Map value, 'mv', then:
|
||||
paths := mv.PathsForKey(key)
|
||||
path := mv.PathForKeyShortest(key)
|
||||
values, err := mv.ValuesForKey(key, subkeys)
|
||||
values, err := mv.ValuesForPath(path, subkeys) // 'path' can be dot-notation with wildcards and indexed arrays.
|
||||
count, err := mv.UpdateValuesForPath(newVal, path, subkeys)
|
||||
|
||||
Get everything at once, irrespective of path depth:
|
||||
leafnodes := mv.LeafNodes()
|
||||
leafvalues := mv.LeafValues()
|
||||
|
||||
A new Map with whatever keys are desired can be created from the current Map and then encoded in XML
|
||||
or JSON. (Note: keys can use dot-notation. 'oldKey' can also use wildcards and indexed arrays.)
|
||||
newMap, err := mv.NewMap("oldKey_1:newKey_1", "oldKey_2:newKey_2", ..., "oldKey_N:newKey_N")
|
||||
newMap, err := mv.NewMap("oldKey1", "oldKey3", "oldKey5") // a subset of 'mv'; see "examples/partial.go"
|
||||
newXml, err := newMap.Xml() // for example
|
||||
newJson, err := newMap.Json() // ditto
|
||||
|
||||
XML PARSING CONVENTIONS
|
||||
|
||||
Using NewMapXml()
|
||||
|
||||
- Attributes are parsed to `map[string]interface{}` values by prefixing a hyphen, `-`,
|
||||
to the attribute label. (Unless overridden by `PrependAttrWithHyphen(false)` or
|
||||
`SetAttrPrefix()`.)
|
||||
- If the element is a simple element and has attributes, the element value
|
||||
is given the key `#text` for its `map[string]interface{}` representation. (See
|
||||
the 'atomFeedString.xml' test data, below.)
|
||||
- XML comments, directives, and process instructions are ignored.
|
||||
- If CoerceKeysToLower() has been called, then the resultant keys will be lower case.
|
||||
|
||||
Using NewMapXmlSeq()
|
||||
|
||||
- Attributes are parsed to `map["#attr"]map[<attr_label>]map[string]interface{}`values
|
||||
where the `<attr_label>` value has "#text" and "#seq" keys - the "#text" key holds the
|
||||
value for `<attr_label>`.
|
||||
- All elements, except for the root, have a "#seq" key.
|
||||
- Comments, directives, and process instructions are unmarshalled into the Map using the
|
||||
keys "#comment", "#directive", and "#procinst", respectively. (See documentation for more
|
||||
specifics.)
|
||||
- Name space syntax is preserved:
|
||||
- <ns:key>something</ns.key> parses to map["ns:key"]interface{}{"something"}
|
||||
- xmlns:ns="http://myns.com/ns" parses to map["xmlns:ns"]interface{}{"http://myns.com/ns"}
|
||||
|
||||
Both
|
||||
|
||||
- By default, "Nan", "Inf", and "-Inf" values are not cast to float64. If you want them
|
||||
to be cast, set a flag to cast them using CastNanInf(true).
|
||||
|
||||
XML ENCODING CONVENTIONS
|
||||
|
||||
- 'nil' Map values, which may represent 'null' JSON values, are encoded as "<tag/>".
|
||||
NOTE: the operation is not symmetric as "<tag/>" elements are decoded as 'tag:""' Map values,
|
||||
which, then, encode in JSON as '"tag":""' values..
|
||||
- ALSO: there is no guarantee that the encoded XML doc will be the same as the decoded one. (Go
|
||||
randomizes the walk through map[string]interface{} values.) If you plan to re-encode the
|
||||
Map value to XML and want the same sequencing of elements look at NewMapXmlSeq() and
|
||||
mv.XmlSeq() - these try to preserve the element sequencing but with added complexity when
|
||||
working with the Map representation.
|
||||
|
||||
*/
|
||||
package mxj
|
93
vendor/github.com/clbanning/mxj/v2/escapechars.go
generated
vendored
Normal file
93
vendor/github.com/clbanning/mxj/v2/escapechars.go
generated
vendored
Normal file
|
@ -0,0 +1,93 @@
|
|||
// Copyright 2016 Charles Banning. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file
|
||||
|
||||
package mxj
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
var xmlEscapeChars bool
|
||||
|
||||
// XMLEscapeChars(true) forces escaping invalid characters in attribute and element values.
|
||||
// NOTE: this is brute force with NO interrogation of '&' being escaped already; if it is
|
||||
// then '&' will be re-escaped as '&amp;'.
|
||||
//
|
||||
/*
|
||||
The values are:
|
||||
" "
|
||||
' '
|
||||
< <
|
||||
> >
|
||||
& &
|
||||
*/
|
||||
//
|
||||
// Note: if XMLEscapeCharsDecoder(true) has been called - or the default, 'false,' value
|
||||
// has been toggled to 'true' - then XMLEscapeChars(true) is ignored. If XMLEscapeChars(true)
|
||||
// has already been called before XMLEscapeCharsDecoder(true), XMLEscapeChars(false) is called
|
||||
// to turn escape encoding on mv.Xml, etc., to prevent double escaping ampersands, '&'.
|
||||
func XMLEscapeChars(b ...bool) {
|
||||
var bb bool
|
||||
if len(b) == 0 {
|
||||
bb = !xmlEscapeChars
|
||||
} else {
|
||||
bb = b[0]
|
||||
}
|
||||
if bb == true && xmlEscapeCharsDecoder == false {
|
||||
xmlEscapeChars = true
|
||||
} else {
|
||||
xmlEscapeChars = false
|
||||
}
|
||||
}
|
||||
|
||||
// Scan for '&' first, since 's' may contain "&" that is parsed to "&amp;"
|
||||
// - or "<" that is parsed to "&lt;".
|
||||
var escapechars = [][2][]byte{
|
||||
{[]byte(`&`), []byte(`&`)},
|
||||
{[]byte(`<`), []byte(`<`)},
|
||||
{[]byte(`>`), []byte(`>`)},
|
||||
{[]byte(`"`), []byte(`"`)},
|
||||
{[]byte(`'`), []byte(`'`)},
|
||||
}
|
||||
|
||||
func escapeChars(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
b := []byte(s)
|
||||
for _, v := range escapechars {
|
||||
n := bytes.Count(b, v[0])
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
b = bytes.Replace(b, v[0], v[1], n)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// per issue #84, escape CharData values from xml.Decoder
|
||||
|
||||
var xmlEscapeCharsDecoder bool
|
||||
|
||||
// XMLEscapeCharsDecoder(b ...bool) escapes XML characters in xml.CharData values
|
||||
// returned by Decoder.Token. Thus, the internal Map values will contain escaped
|
||||
// values, and you do not need to set XMLEscapeChars for proper encoding.
|
||||
//
|
||||
// By default, the Map values have the non-escaped values returned by Decoder.Token.
|
||||
// XMLEscapeCharsDecoder(true) - or, XMLEscapeCharsDecoder() - will toggle escape
|
||||
// encoding 'on.'
|
||||
//
|
||||
// Note: if XMLEscapeCharDecoder(true) is call then XMLEscapeChars(false) is
|
||||
// called to prevent re-escaping the values on encoding using mv.Xml, etc.
|
||||
func XMLEscapeCharsDecoder(b ...bool) {
|
||||
if len(b) == 0 {
|
||||
xmlEscapeCharsDecoder = !xmlEscapeCharsDecoder
|
||||
} else {
|
||||
xmlEscapeCharsDecoder = b[0]
|
||||
}
|
||||
if xmlEscapeCharsDecoder == true && xmlEscapeChars == true {
|
||||
xmlEscapeChars = false
|
||||
}
|
||||
}
|
9
vendor/github.com/clbanning/mxj/v2/exists.go
generated
vendored
Normal file
9
vendor/github.com/clbanning/mxj/v2/exists.go
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
package mxj
|
||||
|
||||
// Checks whether the path exists. If err != nil then 'false' is returned
|
||||
// along with the error encountered parsing either the "path" or "subkeys"
|
||||
// argument.
|
||||
func (mv Map) Exists(path string, subkeys ...string) (bool, error) {
|
||||
v, err := mv.ValuesForPath(path, subkeys...)
|
||||
return (err == nil && len(v) > 0), err
|
||||
}
|
287
vendor/github.com/clbanning/mxj/v2/files.go
generated
vendored
Normal file
287
vendor/github.com/clbanning/mxj/v2/files.go
generated
vendored
Normal file
|
@ -0,0 +1,287 @@
|
|||
package mxj
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Maps []Map
|
||||
|
||||
func NewMaps() Maps {
|
||||
return make(Maps, 0)
|
||||
}
|
||||
|
||||
type MapRaw struct {
|
||||
M Map
|
||||
R []byte
|
||||
}
|
||||
|
||||
// NewMapsFromXmlFile - creates an array from a file of JSON values.
|
||||
func NewMapsFromJsonFile(name string) (Maps, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||
}
|
||||
|
||||
fh, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
am := make([]Map, 0)
|
||||
for {
|
||||
m, raw, err := NewMapJsonReaderRaw(fh)
|
||||
if err != nil && err != io.EOF {
|
||||
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(raw))
|
||||
}
|
||||
if len(m) > 0 {
|
||||
am = append(am, m)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return am, nil
|
||||
}
|
||||
|
||||
// ReadMapsFromJsonFileRaw - creates an array of MapRaw from a file of JSON values.
|
||||
func NewMapsFromJsonFileRaw(name string) ([]MapRaw, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||
}
|
||||
|
||||
fh, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
am := make([]MapRaw, 0)
|
||||
for {
|
||||
mr := new(MapRaw)
|
||||
mr.M, mr.R, err = NewMapJsonReaderRaw(fh)
|
||||
if err != nil && err != io.EOF {
|
||||
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(mr.R))
|
||||
}
|
||||
if len(mr.M) > 0 {
|
||||
am = append(am, *mr)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return am, nil
|
||||
}
|
||||
|
||||
// NewMapsFromXmlFile - creates an array from a file of XML values.
|
||||
func NewMapsFromXmlFile(name string) (Maps, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||
}
|
||||
|
||||
fh, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
am := make([]Map, 0)
|
||||
for {
|
||||
m, raw, err := NewMapXmlReaderRaw(fh)
|
||||
if err != nil && err != io.EOF {
|
||||
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(raw))
|
||||
}
|
||||
if len(m) > 0 {
|
||||
am = append(am, m)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return am, nil
|
||||
}
|
||||
|
||||
// NewMapsFromXmlFileRaw - creates an array of MapRaw from a file of XML values.
|
||||
// NOTE: the slice with the raw XML is clean with no extra capacity - unlike NewMapXmlReaderRaw().
|
||||
// It is slow at parsing a file from disk and is intended for relatively small utility files.
|
||||
func NewMapsFromXmlFileRaw(name string) ([]MapRaw, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("file %s is not a regular file", name)
|
||||
}
|
||||
|
||||
fh, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
am := make([]MapRaw, 0)
|
||||
for {
|
||||
mr := new(MapRaw)
|
||||
mr.M, mr.R, err = NewMapXmlReaderRaw(fh)
|
||||
if err != nil && err != io.EOF {
|
||||
return am, fmt.Errorf("error: %s - reading: %s", err.Error(), string(mr.R))
|
||||
}
|
||||
if len(mr.M) > 0 {
|
||||
am = append(am, *mr)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return am, nil
|
||||
}
|
||||
|
||||
// ------------------------ Maps writing -------------------------
|
||||
// These are handy-dandy methods for dumping configuration data, etc.
|
||||
|
||||
// JsonString - analogous to mv.Json()
|
||||
func (mvs Maps) JsonString(safeEncoding ...bool) (string, error) {
|
||||
var s string
|
||||
for _, v := range mvs {
|
||||
j, err := v.Json()
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
s += string(j)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// JsonStringIndent - analogous to mv.JsonIndent()
|
||||
func (mvs Maps) JsonStringIndent(prefix, indent string, safeEncoding ...bool) (string, error) {
|
||||
var s string
|
||||
var haveFirst bool
|
||||
for _, v := range mvs {
|
||||
j, err := v.JsonIndent(prefix, indent)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
if haveFirst {
|
||||
s += "\n"
|
||||
} else {
|
||||
haveFirst = true
|
||||
}
|
||||
s += string(j)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// XmlString - analogous to mv.Xml()
|
||||
func (mvs Maps) XmlString() (string, error) {
|
||||
var s string
|
||||
for _, v := range mvs {
|
||||
x, err := v.Xml()
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
s += string(x)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// XmlStringIndent - analogous to mv.XmlIndent()
|
||||
func (mvs Maps) XmlStringIndent(prefix, indent string) (string, error) {
|
||||
var s string
|
||||
for _, v := range mvs {
|
||||
x, err := v.XmlIndent(prefix, indent)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
s += string(x)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// JsonFile - write Maps to named file as JSON
|
||||
// Note: the file will be created, if necessary; if it exists it will be truncated.
|
||||
// If you need to append to a file, open it and use JsonWriter method.
|
||||
func (mvs Maps) JsonFile(file string, safeEncoding ...bool) error {
|
||||
var encoding bool
|
||||
if len(safeEncoding) == 1 {
|
||||
encoding = safeEncoding[0]
|
||||
}
|
||||
s, err := mvs.JsonString(encoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fh, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
fh.WriteString(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// JsonFileIndent - write Maps to named file as pretty JSON
|
||||
// Note: the file will be created, if necessary; if it exists it will be truncated.
|
||||
// If you need to append to a file, open it and use JsonIndentWriter method.
|
||||
func (mvs Maps) JsonFileIndent(file, prefix, indent string, safeEncoding ...bool) error {
|
||||
var encoding bool
|
||||
if len(safeEncoding) == 1 {
|
||||
encoding = safeEncoding[0]
|
||||
}
|
||||
s, err := mvs.JsonStringIndent(prefix, indent, encoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fh, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
fh.WriteString(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// XmlFile - write Maps to named file as XML
|
||||
// Note: the file will be created, if necessary; if it exists it will be truncated.
|
||||
// If you need to append to a file, open it and use XmlWriter method.
|
||||
func (mvs Maps) XmlFile(file string) error {
|
||||
s, err := mvs.XmlString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fh, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
fh.WriteString(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// XmlFileIndent - write Maps to named file as pretty XML
|
||||
// Note: the file will be created,if necessary; if it exists it will be truncated.
|
||||
// If you need to append to a file, open it and use XmlIndentWriter method.
|
||||
func (mvs Maps) XmlFileIndent(file, prefix, indent string) error {
|
||||
s, err := mvs.XmlStringIndent(prefix, indent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fh, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
fh.WriteString(s)
|
||||
return nil
|
||||
}
|
2
vendor/github.com/clbanning/mxj/v2/files_test.badjson
generated
vendored
Normal file
2
vendor/github.com/clbanning/mxj/v2/files_test.badjson
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
{ "this":"is", "a":"test", "file":"for", "files_test.go":"case" }
|
||||
{ "with":"some", "bad":JSON, "in":"it" }
|
9
vendor/github.com/clbanning/mxj/v2/files_test.badxml
generated
vendored
Normal file
9
vendor/github.com/clbanning/mxj/v2/files_test.badxml
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
<doc>
|
||||
<some>test</some>
|
||||
<data>for files.go</data>
|
||||
</doc>
|
||||
<msg>
|
||||
<just>some</just>
|
||||
<another>doc</other>
|
||||
<for>test case</for>
|
||||
</msg>
|
2
vendor/github.com/clbanning/mxj/v2/files_test.json
generated
vendored
Normal file
2
vendor/github.com/clbanning/mxj/v2/files_test.json
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
{ "this":"is", "a":"test", "file":"for", "files_test.go":"case" }
|
||||
{ "with":"just", "two":2, "JSON":"values", "true":true }
|
9
vendor/github.com/clbanning/mxj/v2/files_test.xml
generated
vendored
Normal file
9
vendor/github.com/clbanning/mxj/v2/files_test.xml
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
<doc>
|
||||
<some>test</some>
|
||||
<data>for files.go</data>
|
||||
</doc>
|
||||
<msg>
|
||||
<just>some</just>
|
||||
<another>doc</another>
|
||||
<for>test case</for>
|
||||
</msg>
|
1
vendor/github.com/clbanning/mxj/v2/files_test_dup.json
generated
vendored
Normal file
1
vendor/github.com/clbanning/mxj/v2/files_test_dup.json
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"a":"test","file":"for","files_test.go":"case","this":"is"}{"JSON":"values","true":true,"two":2,"with":"just"}
|
1
vendor/github.com/clbanning/mxj/v2/files_test_dup.xml
generated
vendored
Normal file
1
vendor/github.com/clbanning/mxj/v2/files_test_dup.xml
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<doc><data>for files.go</data><some>test</some></doc><msg><another>doc</another><for>test case</for><just>some</just></msg>
|
12
vendor/github.com/clbanning/mxj/v2/files_test_indent.json
generated
vendored
Normal file
12
vendor/github.com/clbanning/mxj/v2/files_test_indent.json
generated
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"a": "test",
|
||||
"file": "for",
|
||||
"files_test.go": "case",
|
||||
"this": "is"
|
||||
}
|
||||
{
|
||||
"JSON": "values",
|
||||
"true": true,
|
||||
"two": 2,
|
||||
"with": "just"
|
||||
}
|
8
vendor/github.com/clbanning/mxj/v2/files_test_indent.xml
generated
vendored
Normal file
8
vendor/github.com/clbanning/mxj/v2/files_test_indent.xml
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<doc>
|
||||
<data>for files.go</data>
|
||||
<some>test</some>
|
||||
</doc><msg>
|
||||
<another>doc</another>
|
||||
<for>test case</for>
|
||||
<just>some</just>
|
||||
</msg>
|
35
vendor/github.com/clbanning/mxj/v2/gob.go
generated
vendored
Normal file
35
vendor/github.com/clbanning/mxj/v2/gob.go
generated
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
// gob.go - Encode/Decode a Map into a gob object.
|
||||
|
||||
package mxj
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
// NewMapGob returns a Map value for a gob object that has been
|
||||
// encoded from a map[string]interface{} (or compatible type) value.
|
||||
// It is intended to provide symmetric handling of Maps that have
|
||||
// been encoded using mv.Gob.
|
||||
func NewMapGob(gobj []byte) (Map, error) {
|
||||
m := make(map[string]interface{}, 0)
|
||||
if len(gobj) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
r := bytes.NewReader(gobj)
|
||||
dec := gob.NewDecoder(r)
|
||||
if err := dec.Decode(&m); err != nil {
|
||||
return m, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Gob returns a gob-encoded value for the Map 'mv'.
|
||||
func (mv Map) Gob() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
if err := enc.Encode(map[string]interface{}(mv)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue