mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-03-18 10:32:36 +00:00
Compare commits
218 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e0ff74944 | ||
|
|
366c1d0c6c | ||
|
|
17fa163ee3 | ||
|
|
3644fdb533 | ||
|
|
ab7c4e72c6 | ||
|
|
e25e7925b6 | ||
|
|
80237c8090 | ||
|
|
a310953f05 | ||
|
|
a9e92b60f5 | ||
|
|
35e40cd230 | ||
|
|
2575ad722a | ||
|
|
afd5757315 | ||
|
|
dba8b1f215 | ||
|
|
6dd0ef1268 | ||
|
|
83cfaed1a3 | ||
|
|
41cb9ee12e | ||
|
|
667f0dc87d | ||
|
|
a34c2fc0dc | ||
|
|
7a31263e4a | ||
|
|
7f9fd82c0e | ||
|
|
a37d1f4aeb | ||
|
|
acdbdedd5d | ||
|
|
a9b5eba9d4 | ||
|
|
80201224c6 | ||
|
|
e6e7d8d58b | ||
|
|
bf27e94003 | ||
|
|
2ae0a2400d | ||
|
|
db1f4458c5 | ||
|
|
5d5c11c37c | ||
|
|
b4f3b2c540 | ||
|
|
a427534605 | ||
|
|
1d6ca9d392 | ||
|
|
f74a52d4dc | ||
|
|
6d2e9af5d7 | ||
|
|
e4ff4a0745 | ||
|
|
f9677dbaa1 | ||
|
|
0afab6c068 | ||
|
|
1d1b62ec4f | ||
|
|
e2db5087b8 | ||
|
|
241477fb5c | ||
|
|
c8e5886a96 | ||
|
|
8a8cf4aa77 | ||
|
|
7b73004e85 | ||
|
|
56dc6843e0 | ||
|
|
0409eb239d | ||
|
|
cbe04af801 | ||
|
|
59dec1a547 | ||
|
|
c4afeee5b3 | ||
|
|
8c9b8d3217 | ||
|
|
d705ae3eb6 | ||
|
|
c53b2148d1 | ||
|
|
ca897dd3c7 | ||
|
|
4406919565 | ||
|
|
413fb5b3f5 | ||
|
|
e36c146979 | ||
|
|
1cf9c29ef0 | ||
|
|
02e02718d2 | ||
|
|
1a0517f46b | ||
|
|
efbb432df9 | ||
|
|
dfea8884d4 | ||
|
|
d34dacbbe2 | ||
|
|
0595df8b87 | ||
|
|
ebbe6458a8 | ||
|
|
7f2021c312 | ||
|
|
824945141a | ||
|
|
0244f12167 | ||
|
|
60533a9591 | ||
|
|
90f0f603c7 | ||
|
|
683d199774 | ||
|
|
fa632b49a7 | ||
|
|
04579eb03c | ||
|
|
dea223bfe1 | ||
|
|
06c8056443 | ||
|
|
d18f1f8316 | ||
|
|
f9202900ee | ||
|
|
9e34662511 | ||
|
|
1e726e381b | ||
|
|
69a9deab4b | ||
|
|
f9396e01ca | ||
|
|
2d5b170406 | ||
|
|
dc59fb6931 | ||
|
|
793bb97e51 | ||
|
|
ceb8d714e3 | ||
|
|
8d8310ee02 | ||
|
|
0824524d62 | ||
|
|
71eff5ea04 | ||
|
|
50e404f51e | ||
|
|
ffa34039b1 | ||
|
|
d888706e1e | ||
|
|
1ef17542dd | ||
|
|
0566f63d72 | ||
|
|
6d49339e29 | ||
|
|
58f0de4d4e | ||
|
|
f175480f65 | ||
|
|
6dd2bf705b | ||
|
|
f64ee23c74 | ||
|
|
803681a239 | ||
|
|
a2150b4a78 | ||
|
|
ac358be877 | ||
|
|
2996c1a4bc | ||
|
|
e42c4f8648 | ||
|
|
7d5ed601df | ||
|
|
30651c0f75 | ||
|
|
594f1b973a | ||
|
|
77ced2a46d | ||
|
|
b195ed9905 | ||
|
|
59ef3a4244 | ||
|
|
360a4ea562 | ||
|
|
e883358cd6 | ||
|
|
efb1a0b58b | ||
|
|
eb67f76e2b | ||
|
|
5b9c134ab2 | ||
|
|
8db12a4b1a | ||
|
|
7fb85df3ac | ||
|
|
edc3d04d59 | ||
|
|
679bf35ce3 | ||
|
|
17e1ccf9ef | ||
|
|
80e97e7f7e | ||
|
|
5c86e20c92 | ||
|
|
4a030c02f7 | ||
|
|
ea5054866d | ||
|
|
a11e8f730e | ||
|
|
1e66ebd8b3 | ||
|
|
3be0c9ecd9 | ||
|
|
ef8314b554 | ||
|
|
b57cb0e615 | ||
|
|
1d24188a02 | ||
|
|
9d9f64098e | ||
|
|
fbf3d1729e | ||
|
|
eb02ecda20 | ||
|
|
dc62195a8f | ||
|
|
38b58dba69 | ||
|
|
765feafbcc | ||
|
|
0a622b5017 | ||
|
|
a0a9d74662 | ||
|
|
5e7ef0fbb9 | ||
|
|
1d6c176c7f | ||
|
|
3b9f5ee32f | ||
|
|
1619df2d5e | ||
|
|
21b91ea6e4 | ||
|
|
79a8ee37f9 | ||
|
|
6e14fa95a1 | ||
|
|
cd34892943 | ||
|
|
62b17c1822 | ||
|
|
cef0e01cf6 | ||
|
|
6e279bfca5 | ||
|
|
4e7bc05ecf | ||
|
|
8886590ea2 | ||
|
|
8400e9e903 | ||
|
|
eaa120cad8 | ||
|
|
35f9e16e7c | ||
|
|
b29b15cf6c | ||
|
|
8fab07494c | ||
|
|
79816ae337 | ||
|
|
728b5b5d1c | ||
|
|
c4048e5c8e | ||
|
|
bd71e9a122 | ||
|
|
4a053734d9 | ||
|
|
5b439d8316 | ||
|
|
400774555a | ||
|
|
7cd6d123d1 | ||
|
|
90c9d8b0d0 | ||
|
|
96f47116f0 | ||
|
|
78456d7987 | ||
|
|
115692dbfc | ||
|
|
f809ed5eeb | ||
|
|
603206f2cb | ||
|
|
6aa38f071f | ||
|
|
d684dee7a4 | ||
|
|
e3049fb5a5 | ||
|
|
ec18d96b45 | ||
|
|
f03df50def | ||
|
|
b2f091746a | ||
|
|
60431b2836 | ||
|
|
77e01da6e8 | ||
|
|
18c1473bd9 | ||
|
|
1638e1be3b | ||
|
|
01a1cd8434 | ||
|
|
466214c4b5 | ||
|
|
de1295e29d | ||
|
|
7de3338752 | ||
|
|
3db6d5a5ea | ||
|
|
65ba0952b4 | ||
|
|
97798cb5b7 | ||
|
|
b1df4b69ae | ||
|
|
b3dcff2cd5 | ||
|
|
09702697ad | ||
|
|
7abf8b83e3 | ||
|
|
12d873d344 | ||
|
|
672accba0c | ||
|
|
566eab3527 | ||
|
|
0f52533cd8 | ||
|
|
eef58496b5 | ||
|
|
1137f9386b | ||
|
|
93714ab902 | ||
|
|
fc03ba2eda | ||
|
|
3662fbcdf6 | ||
|
|
b762e3c194 | ||
|
|
35ef211477 | ||
|
|
feb386ba1f | ||
|
|
ed4a818a53 | ||
|
|
fa733025dc | ||
|
|
5f603e3291 | ||
|
|
b84c698c1a | ||
|
|
c59aadb221 | ||
|
|
6aaee4b519 | ||
|
|
6f47ad862e | ||
|
|
f18f3da99c | ||
|
|
3e32c889d9 | ||
|
|
f604a3a35d | ||
|
|
5d205b5082 | ||
|
|
756f5f5720 | ||
|
|
9a1c17cc61 | ||
|
|
64253cd919 | ||
|
|
accad7c058 | ||
|
|
485bc7fd2b | ||
|
|
bc3efc6d4c | ||
|
|
135b1a5e1e |
@@ -2,7 +2,7 @@
|
|||||||
.dockerignore
|
.dockerignore
|
||||||
.editorconfig
|
.editorconfig
|
||||||
.gitignore
|
.gitignore
|
||||||
.env.*
|
**/.env*
|
||||||
Dockerfile
|
Dockerfile
|
||||||
Makefile
|
Makefile
|
||||||
LICENSE
|
LICENSE
|
||||||
|
|||||||
4
.github/CODEOWNERS
vendored
Normal file
4
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# This is a comment.
|
||||||
|
# Each line is a file pattern followed by one or more owners.
|
||||||
|
|
||||||
|
/ui/ @frontend
|
||||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Run mizu <command> '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Logs**
|
||||||
|
Upload logs:
|
||||||
|
1. Run the mizu command with `--set dump-logs=true` (e.g `mizu tap --set dump-logs=true`)
|
||||||
|
2. Try to reproduce the issue
|
||||||
|
3. CNTRL+C on terminal tab which runs mizu
|
||||||
|
4. Upload the logs zip file from ~/.mizu/mizu_logs_**.zip
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
32
.github/workflows/acceptance_tests.yml
vendored
Normal file
32
.github/workflows/acceptance_tests.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: acceptance tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'develop'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mizu-acceptance-tests-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-acceptance-tests:
|
||||||
|
name: Run acceptance tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.16
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '^1.16'
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup acceptance test
|
||||||
|
run: source ./acceptanceTests/setup.sh
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: make acceptance-test
|
||||||
46
.github/workflows/pr_validation.yml
vendored
Normal file
46
.github/workflows/pr_validation.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: PR validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'develop'
|
||||||
|
- 'main'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mizu-pr-validation-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-cli:
|
||||||
|
name: Build CLI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.16
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '^1.16'
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Build CLI
|
||||||
|
run: make cli
|
||||||
|
|
||||||
|
build-agent:
|
||||||
|
name: Build Agent
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.16
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '^1.16'
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- shell: bash
|
||||||
|
run: |
|
||||||
|
sudo apt-get install libpcap-dev
|
||||||
|
|
||||||
|
- name: Build Agent
|
||||||
|
run: make agent
|
||||||
39
.github/workflows/publish-docker.yml
vendored
39
.github/workflows/publish-docker.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
name: publish-docker
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'develop'
|
|
||||||
- 'main'
|
|
||||||
jobs:
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Get base image name
|
|
||||||
shell: bash
|
|
||||||
run: echo "##[set-output name=image;]$(echo gcr.io/up9-docker-hub/mizu/${GITHUB_REF#refs/heads/})"
|
|
||||||
id: base_image_step
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: crazy-max/ghaction-docker-meta@v2
|
|
||||||
with:
|
|
||||||
images: ${{ steps.base_image_step.outputs.image }}
|
|
||||||
tags: |
|
|
||||||
type=sha
|
|
||||||
type=raw,${{ github.sha }}
|
|
||||||
type=raw,latest
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: gcr.io
|
|
||||||
username: _json_key
|
|
||||||
password: ${{ secrets.GCR_JSON_KEY }}
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
name: public-cli
|
name: publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- 'develop'
|
||||||
- main
|
- 'main'
|
||||||
- my-temp-release-check
|
|
||||||
|
concurrency:
|
||||||
|
group: mizu-publish-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -28,14 +33,48 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
releaseType: ${{ steps.condval.outputs.value }}
|
releaseType: ${{ steps.condval.outputs.value }}
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Get base image name
|
- name: Get version parameters
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "##[set-output name=build_timestamp;]$(echo $(date +%s))"
|
echo "##[set-output name=build_timestamp;]$(echo $(date +%s))"
|
||||||
echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
|
echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
|
||||||
id: version_parameters
|
id: version_parameters
|
||||||
|
- name: Get base image name
|
||||||
|
shell: bash
|
||||||
|
run: echo "##[set-output name=image;]$(echo gcr.io/up9-docker-hub/mizu/${GITHUB_REF#refs/heads/})"
|
||||||
|
id: base_image_step
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: crazy-max/ghaction-docker-meta@v2
|
||||||
|
with:
|
||||||
|
images: ${{ steps.base_image_step.outputs.image }}
|
||||||
|
tags: |
|
||||||
|
type=sha
|
||||||
|
type=raw,${{ github.sha }}
|
||||||
|
type=raw,${{ steps.versioning.outputs.version }}
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: gcr.io
|
||||||
|
username: _json_key
|
||||||
|
password: ${{ secrets.GCR_JSON_KEY }}
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
SEM_VER=${{ steps.versioning.outputs.version }}
|
||||||
|
BUILD_TIMESTAMP=${{ steps.version_parameters.outputs.build_timestamp }}
|
||||||
|
GIT_BRANCH=${{ steps.version_parameters.outputs.branch }}
|
||||||
|
COMMIT_HASH=${{ github.sha }}
|
||||||
- name: Build and Push CLI
|
- name: Build and Push CLI
|
||||||
run: make push-cli SEM_VER='${{ steps.versioning.outputs.version }}' BUILD_TIMESTAMP='${{ steps.version_parameters.outputs.build_timestamp }}'
|
run: make push-cli SEM_VER='${{ steps.versioning.outputs.version }}' BUILD_TIMESTAMP='${{ steps.version_parameters.outputs.build_timestamp }}'
|
||||||
|
- shell: bash
|
||||||
|
run: |
|
||||||
|
echo '${{ steps.versioning.outputs.version }}' >> cli/bin/version.txt
|
||||||
- name: publish
|
- name: publish
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
56
.github/workflows/tests_validation.yml
vendored
Normal file
56
.github/workflows/tests_validation.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: tests validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'develop'
|
||||||
|
- 'main'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'develop'
|
||||||
|
- 'main'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mizu-tests-validation-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests-cli:
|
||||||
|
name: Run CLI tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.16
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '^1.16'
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: make test-cli
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
|
|
||||||
|
run-tests-agent:
|
||||||
|
name: Run Agent tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.16
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '^1.16'
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- shell: bash
|
||||||
|
run: |
|
||||||
|
sudo apt-get install libpcap-dev
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: make test-agent
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -19,3 +19,10 @@ build
|
|||||||
|
|
||||||
# Mac OS
|
# Mac OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Ignore the scripts that are created for development
|
||||||
|
*dev.*
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|||||||
18
CONTRIBUTE.md
Normal file
18
CONTRIBUTE.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|

|
||||||
|
# CONTRIBUTE
|
||||||
|
We welcome code contributions from the community.
|
||||||
|
Please read and follow the guidelines below.
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
* Before starting work on a major feature, please reach out to us via [GitHub](https://github.com/up9inc/mizu), [Slack](https://join.slack.com/share/zt-u6bbs3pg-X1zhQOXOH0yEoqILgH~csw), [email](mailto:mizu@up9.com), etc. We will make sure no one else is already working on it. A _major feature_ is defined as any change that is > 100 LOC altered (not including tests), or changes any user-facing behavior
|
||||||
|
* Small patches and bug fixes don't need prior communication.
|
||||||
|
|
||||||
|
## Contribution requirements
|
||||||
|
* Code style - most of the code is written in Go, please follow [these guidelines](https://golang.org/doc/effective_go)
|
||||||
|
* Go-tools compatible (`go get`, `go test`, etc)
|
||||||
|
* Unit-test coverage can’t go down ..
|
||||||
|
* Code must be usefully commented. Not only for developers on the project, but also for external users of these packages
|
||||||
|
* When reviewing PRs, you are encouraged to use Golang's [code review comments page](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
36
Dockerfile
36
Dockerfile
@@ -11,22 +11,36 @@ FROM golang:1.16-alpine AS builder
|
|||||||
# Set necessary environment variables needed for our image.
|
# Set necessary environment variables needed for our image.
|
||||||
ENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64
|
ENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64
|
||||||
|
|
||||||
RUN apk add libpcap-dev gcc g++ make
|
RUN apk add libpcap-dev gcc g++ make bash
|
||||||
|
|
||||||
# Move to api working directory (/api-build).
|
# Move to agent working directory (/agent-build).
|
||||||
WORKDIR /app/api-build
|
WORKDIR /app/agent-build
|
||||||
|
|
||||||
COPY api/go.mod api/go.sum ./
|
COPY agent/go.mod agent/go.sum ./
|
||||||
COPY shared/go.mod shared/go.mod ../shared/
|
COPY shared/go.mod shared/go.mod ../shared/
|
||||||
|
COPY tap/go.mod tap/go.mod ../tap/
|
||||||
|
COPY tap/api/go.* ../tap/api/
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
# cheap trick to make the build faster (As long as go.mod wasn't changes)
|
# cheap trick to make the build faster (As long as go.mod wasn't changes)
|
||||||
RUN go list -f '{{.Path}}@{{.Version}}' -m all | sed 1d | grep -e 'go-cache' -e 'sqlite' | xargs go get
|
RUN go list -f '{{.Path}}@{{.Version}}' -m all | sed 1d | grep -e 'go-cache' -e 'sqlite' | xargs go get
|
||||||
|
|
||||||
# Copy and build api code
|
ARG COMMIT_HASH
|
||||||
COPY shared ../shared
|
ARG GIT_BRANCH
|
||||||
COPY api .
|
ARG BUILD_TIMESTAMP
|
||||||
RUN go build -ldflags="-s -w" -o mizuagent .
|
ARG SEM_VER
|
||||||
|
|
||||||
|
# Copy and build agent code
|
||||||
|
COPY shared ../shared
|
||||||
|
COPY tap ../tap
|
||||||
|
COPY agent .
|
||||||
|
RUN go build -ldflags="-s -w \
|
||||||
|
-X 'mizuserver/pkg/version.GitCommitHash=${COMMIT_HASH}' \
|
||||||
|
-X 'mizuserver/pkg/version.Branch=${GIT_BRANCH}' \
|
||||||
|
-X 'mizuserver/pkg/version.BuildTimestamp=${BUILD_TIMESTAMP}' \
|
||||||
|
-X 'mizuserver/pkg/version.SemVer=${SEM_VER}'" -o mizuagent .
|
||||||
|
|
||||||
|
COPY build_extensions.sh ..
|
||||||
|
RUN cd .. && /bin/bash build_extensions.sh
|
||||||
|
|
||||||
FROM alpine:3.13.5
|
FROM alpine:3.13.5
|
||||||
|
|
||||||
@@ -34,10 +48,12 @@ RUN apk add bash libpcap-dev tcpdump
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy binary and config files from /build to root folder of scratch container.
|
# Copy binary and config files from /build to root folder of scratch container.
|
||||||
COPY --from=builder ["/app/api-build/mizuagent", "."]
|
COPY --from=builder ["/app/agent-build/mizuagent", "."]
|
||||||
|
COPY --from=builder ["/app/agent/build/extensions", "extensions"]
|
||||||
COPY --from=site-build ["/app/ui-build/build", "site"]
|
COPY --from=site-build ["/app/ui-build/build", "site"]
|
||||||
|
|
||||||
COPY api/start.sh .
|
# gin-gonic runs in debug mode without this
|
||||||
|
ENV GIN_MODE=release
|
||||||
|
|
||||||
# this script runs both apiserver and passivetapper and exits either if one of them exits, preventing a scenario where the container runs without one process
|
# this script runs both apiserver and passivetapper and exits either if one of them exits, preventing a scenario where the container runs without one process
|
||||||
ENTRYPOINT "/app/mizuagent"
|
ENTRYPOINT "/app/mizuagent"
|
||||||
|
|||||||
62
Makefile
62
Makefile
@@ -8,7 +8,7 @@ SHELL=/bin/bash
|
|||||||
# HELP
|
# HELP
|
||||||
# This will output the help for each task
|
# This will output the help for each task
|
||||||
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||||
.PHONY: help ui api cli tap docker
|
.PHONY: help ui agent cli tap docker
|
||||||
|
|
||||||
help: ## This help.
|
help: ## This help.
|
||||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||||
@@ -19,34 +19,38 @@ help: ## This help.
|
|||||||
TS_SUFFIX="$(shell date '+%s')"
|
TS_SUFFIX="$(shell date '+%s')"
|
||||||
GIT_BRANCH="$(shell git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]' | tr '/' '_')"
|
GIT_BRANCH="$(shell git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]' | tr '/' '_')"
|
||||||
BUCKET_PATH=static.up9.io/mizu/$(GIT_BRANCH)
|
BUCKET_PATH=static.up9.io/mizu/$(GIT_BRANCH)
|
||||||
|
export SEM_VER?=0.0.0
|
||||||
|
|
||||||
ui: ## build UI
|
ui: ## Build UI.
|
||||||
@(cd ui; npm i ; npm run build; )
|
@(cd ui; npm i ; npm run build; )
|
||||||
@ls -l ui/build
|
@ls -l ui/build
|
||||||
|
|
||||||
cli: # build CLI
|
cli: ## Build CLI.
|
||||||
@echo "building cli"; cd cli && $(MAKE) build
|
@echo "building cli"; cd cli && $(MAKE) build
|
||||||
|
|
||||||
api: ## build API server
|
build-cli-ci: ## Build CLI for CI.
|
||||||
@(echo "building API server .." )
|
@echo "building cli for ci"; cd cli && $(MAKE) build GIT_BRANCH=ci SUFFIX=ci
|
||||||
@(cd api; go build -o build/apiserver main.go)
|
|
||||||
@ls -l api/build
|
|
||||||
|
|
||||||
#tap: ## build tap binary
|
agent: ## Build agent.
|
||||||
# @(cd tap; go build -o build/tap ./src)
|
@(echo "building mizu agent .." )
|
||||||
# @ls -l tap/build
|
@(cd agent; go build -o build/mizuagent main.go)
|
||||||
|
${MAKE} extensions
|
||||||
|
@ls -l agent/build
|
||||||
|
|
||||||
docker: ## build Docker image
|
docker: ## Build and publish agent docker image.
|
||||||
@(echo "building docker image" )
|
$(MAKE) push-docker
|
||||||
./build-push-featurebranch.sh
|
|
||||||
|
|
||||||
push: push-docker push-cli ## build and publish Mizu docker image & CLI
|
push: push-docker push-cli ## Build and publish agent docker image & CLI.
|
||||||
|
|
||||||
push-docker:
|
push-docker: ## Build and publish agent docker image.
|
||||||
@echo "publishing Docker image .. "
|
@echo "publishing Docker image .. "
|
||||||
./build-push-featurebranch.sh
|
./build-push-featurebranch.sh
|
||||||
|
|
||||||
push-cli:
|
build-docker-ci: ## Build agent docker image for CI.
|
||||||
|
@echo "building docker image for ci"
|
||||||
|
./build-agent-ci.sh
|
||||||
|
|
||||||
|
push-cli: ## Build and publish CLI.
|
||||||
@echo "publishing CLI .. "
|
@echo "publishing CLI .. "
|
||||||
@cd cli; $(MAKE) build-all
|
@cd cli; $(MAKE) build-all
|
||||||
@echo "publishing file ${OUTPUT_FILE} .."
|
@echo "publishing file ${OUTPUT_FILE} .."
|
||||||
@@ -54,18 +58,28 @@ push-cli:
|
|||||||
gsutil cp -r ./cli/bin/* gs://${BUCKET_PATH}/
|
gsutil cp -r ./cli/bin/* gs://${BUCKET_PATH}/
|
||||||
gsutil setmeta -r -h "Cache-Control:public, max-age=30" gs://${BUCKET_PATH}/\*
|
gsutil setmeta -r -h "Cache-Control:public, max-age=30" gs://${BUCKET_PATH}/\*
|
||||||
|
|
||||||
|
clean: clean-ui clean-agent clean-cli clean-docker ## Clean all build artifacts.
|
||||||
|
|
||||||
clean: clean-ui clean-api clean-cli clean-docker ## Clean all build artifacts
|
clean-ui: ## Clean UI.
|
||||||
|
|
||||||
clean-ui:
|
|
||||||
@(rm -rf ui/build ; echo "UI cleanup done" )
|
@(rm -rf ui/build ; echo "UI cleanup done" )
|
||||||
|
|
||||||
clean-api:
|
clean-agent: ## Clean agent.
|
||||||
@(rm -rf api/build ; echo "api cleanup done" )
|
@(rm -rf agent/build ; echo "agent cleanup done" )
|
||||||
|
|
||||||
clean-cli:
|
clean-cli: ## Clean CLI.
|
||||||
@(cd cli; make clean ; echo "CLI cleanup done" )
|
@(cd cli; make clean ; echo "CLI cleanup done" )
|
||||||
|
|
||||||
clean-docker:
|
clean-docker:
|
||||||
@(echo "DOCKER cleanup - NOT IMPLEMENTED YET " )
|
@(echo "DOCKER cleanup - NOT IMPLEMENTED YET " )
|
||||||
|
|
||||||
|
extensions:
|
||||||
|
./build_extensions.sh
|
||||||
|
|
||||||
|
test-cli:
|
||||||
|
@echo "running cli tests"; cd cli && $(MAKE) test
|
||||||
|
|
||||||
|
test-agent:
|
||||||
|
@echo "running agent tests"; cd agent && $(MAKE) test
|
||||||
|
|
||||||
|
acceptance-test:
|
||||||
|
@echo "running acceptance tests"; cd acceptanceTests && $(MAKE) test
|
||||||
|
|||||||
328
PERMISSIONS.md
Normal file
328
PERMISSIONS.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|

|
||||||
|
# Kubernetes permissions for MIZU
|
||||||
|
|
||||||
|
This document describes in details all permissions required for full and correct operation of Mizu
|
||||||
|
|
||||||
|
We broke down this list into few categories:
|
||||||
|
- Required - what is needed for `mizu` to run properly on your k8s cluster
|
||||||
|
- Optional - permissions needed for proper name resolving for service & pod IPs
|
||||||
|
- addition required for policy validation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Required permissions
|
||||||
|
|
||||||
|
Mizu needs following permissions on your Kubernetes cluster to run properly
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- daemonsets
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- namespaces
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services/proxy
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
```
|
||||||
|
|
||||||
|
## Permissions required for service / pod name resolving (opt)
|
||||||
|
|
||||||
|
Optionally, for proper resolving of IP addresses to Kubernetes service name, Mizu needs below permissions:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- daemonsets
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- namespaces
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services/proxy
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- serviceaccounts
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- rbac.authorization.k8s.io
|
||||||
|
resources:
|
||||||
|
- clusterroles
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- rbac.authorization.k8s.io
|
||||||
|
resources:
|
||||||
|
- clusterrolebindings
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- rbac.authorization.k8s.io
|
||||||
|
resources:
|
||||||
|
- roles
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- rbac.authorization.k8s.io
|
||||||
|
resources:
|
||||||
|
- rolebindings
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
- apps
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- endpoints
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Permissions for Policy rules validation feature (opt)
|
||||||
|
|
||||||
|
Optionally, in order to use the policy rules validation feature, Mizu requires the following additional permissions:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
```
|
||||||
|
|
||||||
|
- - -
|
||||||
|
|
||||||
|
## Namespace-Restricted mode
|
||||||
|
|
||||||
|
Alternatively, in order to restrict Mizu to one namespace only (by setting `agent.namespace` in the config file), Mizu needs the following permissions in that namespace:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- daemonsets
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services/proxy
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
```
|
||||||
|
|
||||||
|
### Name resolving in Namespace-Restricted mode (opt)
|
||||||
|
|
||||||
|
To restrict Mizu to one namespace while also resolving IPs, Mizu needs the following permissions in that namespace:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- daemonsets
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services/proxy
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- serviceaccounts
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- rbac.authorization.k8s.io
|
||||||
|
resources:
|
||||||
|
- roles
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- rbac.authorization.k8s.io
|
||||||
|
resources:
|
||||||
|
- rolebindings
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
- apps
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- endpoints
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
```
|
||||||
177
README.md
177
README.md
@@ -1,24 +1,173 @@
|
|||||||
# 水 mizu
|

|
||||||
standalone web app traffic viewer for Kubernetes
|
|
||||||
|
# The API Traffic Viewer for Kubernetes
|
||||||
|
|
||||||
|
A simple-yet-powerful API traffic viewer for Kubernetes enabling you to view all API communication between microservices to help your debug and troubleshoot regressions.
|
||||||
|
|
||||||
|
Think TCPDump and Chrome Dev Tools combined.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Simple and powerful CLI
|
||||||
|
- Real-time view of all HTTP requests, REST and gRPC API calls
|
||||||
|
- No installation or code instrumentation
|
||||||
|
- Works completely on premises
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
Download `mizu` for your platform and operating system
|
Download Mizu for your platform and operating system
|
||||||
|
|
||||||
### Latest stable release
|
### Latest Stable Release
|
||||||
|
|
||||||
* for MacOS - `curl -o mizu https://github.com/up9inc/mizu/releases/download/latest/mizu_darwin_amd64 && chmod 755 mizu`
|
* for MacOS - Intel
|
||||||
* for Linux - `curl -o mizu https://github.com/up9inc/mizu/releases/download/latest/mizu_linux_amd64 && chmod 755 mizu`
|
```
|
||||||
|
curl -Lo mizu \
|
||||||
|
https://github.com/up9inc/mizu/releases/latest/download/mizu_darwin_amd64 \
|
||||||
|
&& chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
|
* for Linux - Intel 64bit
|
||||||
|
```
|
||||||
|
curl -Lo mizu \
|
||||||
|
https://github.com/up9inc/mizu/releases/latest/download/mizu_linux_amd64 \
|
||||||
|
&& chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
### Development (unstable) build
|
SHA256 checksums are available on the [Releases](https://github.com/up9inc/mizu/releases) page
|
||||||
Pick one from the [Releases](https://github.com/up9inc/mizu/releases) page.
|
|
||||||
|
|
||||||
## How to run
|
### Development (unstable) Build
|
||||||
|
Pick one from the [Releases](https://github.com/up9inc/mizu/releases) page
|
||||||
|
|
||||||
1. Find pod you'd like to tap to in your Kubernetes cluster
|
## Kubeconfig & Permissions
|
||||||
2. Run `mizu PODNAME` or `mizu REGEX`
|
While `mizu`most often works out of the box, you can influence its behavior:
|
||||||
3. Open browser on `http://localhost:8899` as instructed ..
|
|
||||||
4. Watch the WebAPI traffic flowing ..
|
1. [OPTIONAL] Set `KUBECONFIG` environment variable to your Kubernetes configuration. If this is not set, Mizu assumes that configuration is at `${HOME}/.kube/config`
|
||||||
|
2. `mizu` assumes user running the command has permissions to create resources (such as pods, services, namespaces) on your Kubernetes cluster (no worries - `mizu` resources are cleaned up upon termination)
|
||||||
|
|
||||||
|
For detailed list of k8s permissions see [PERMISSIONS](PERMISSIONS.md) document
|
||||||
|
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
|
||||||
|
1. Find pods you'd like to tap to in your Kubernetes cluster
|
||||||
|
2. Run `mizu tap` or `mizu tap PODNAME`
|
||||||
|
3. Open browser on `http://localhost:8899/mizu` **or** as instructed in the CLI
|
||||||
|
4. Watch the API traffic flowing
|
||||||
|
5. Type ^C to stop
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
TBD
|
|
||||||
|
Run `mizu help` for usage options
|
||||||
|
|
||||||
|
To tap all pods in current namespace -
|
||||||
|
```
|
||||||
|
$ kubectl get pods
|
||||||
|
NAME READY STATUS RESTARTS AGE
|
||||||
|
carts-66c77f5fbb-fq65r 2/2 Running 0 20m
|
||||||
|
catalogue-5f4cb7cf5-7zrmn 2/2 Running 0 20m
|
||||||
|
front-end-649fc5fd6-kqbtn 2/2 Running 0 20m
|
||||||
|
..
|
||||||
|
|
||||||
|
$ mizu tap
|
||||||
|
+carts-66c77f5fbb-fq65r
|
||||||
|
+catalogue-5f4cb7cf5-7zrmn
|
||||||
|
+front-end-649fc5fd6-kqbtn
|
||||||
|
Web interface is now available at http://localhost:8899
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
To tap specific pod -
|
||||||
|
```bash
|
||||||
|
$ kubectl get pods
|
||||||
|
NAME READY STATUS RESTARTS AGE
|
||||||
|
front-end-649fc5fd6-kqbtn 2/2 Running 0 7m
|
||||||
|
..
|
||||||
|
|
||||||
|
$ mizu tap front-end-649fc5fd6-kqbtn
|
||||||
|
+front-end-649fc5fd6-kqbtn
|
||||||
|
Web interface is now available at http://localhost:8899
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
|
||||||
|
To tap multiple pods using regex -
|
||||||
|
```bash
|
||||||
|
$ kubectl get pods
|
||||||
|
NAME READY STATUS RESTARTS AGE
|
||||||
|
carts-66c77f5fbb-fq65r 2/2 Running 0 20m
|
||||||
|
catalogue-5f4cb7cf5-7zrmn 2/2 Running 0 20m
|
||||||
|
front-end-649fc5fd6-kqbtn 2/2 Running 0 20m
|
||||||
|
..
|
||||||
|
|
||||||
|
$ mizu tap "^ca.*"
|
||||||
|
+carts-66c77f5fbb-fq65r
|
||||||
|
+catalogue-5f4cb7cf5-7zrmn
|
||||||
|
Web interface is now available at http://localhost:8899
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Mizu can work with config file which should be stored in ${HOME}/.mizu/config.yaml (macOS: ~/.mizu/config.yaml) <br />
|
||||||
|
In case no config file found, defaults will be used <br />
|
||||||
|
In case of partial configuration defined, all other fields will be used with defaults <br />
|
||||||
|
You can always override the defaults or config file with CLI flags
|
||||||
|
|
||||||
|
To get the default config params run `mizu config` <br />
|
||||||
|
To generate a new config file with default values use `mizu config -r`
|
||||||
|
|
||||||
|
### Telemetry
|
||||||
|
|
||||||
|
By default, mizu reports usage telemetry. It can be disabled by adding a line of `telemetry: false` in the `${HOME}/.mizu/config.yaml` file
|
||||||
|
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Namespace-Restricted Mode
|
||||||
|
|
||||||
|
Some users have permission to only manage resources in one particular namespace assigned to them
|
||||||
|
By default `mizu tap` creates a new namespace `mizu` for all of its Kubernetes resources. In order to instead install
|
||||||
|
Mizu in an existing namespace, set the `mizu-resources-namespace` config option
|
||||||
|
|
||||||
|
If `mizu-resources-namespace` is set to a value other than the default `mizu`, Mizu will operate in a
|
||||||
|
Namespace-Restricted mode. It will only tap pods in `mizu-resources-namespace`. This way Mizu only requires permissions
|
||||||
|
to the namespace set by `mizu-resources-namespace`. The user must set the tapped namespace to the same namespace by
|
||||||
|
using the `--namespace` flag or by setting `tap.namespaces` in the config file
|
||||||
|
|
||||||
|
Setting `mizu-resources-namespace=mizu` resets Mizu to its default behavior
|
||||||
|
|
||||||
|
### User agent filtering
|
||||||
|
|
||||||
|
User-agent filtering (like health checks) - can be configured using command-line options:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ mizu tap "^ca.*" --set ignored-user-agents=kube-probe --set ignored-user-agents=prometheus
|
||||||
|
+carts-66c77f5fbb-fq65r
|
||||||
|
+catalogue-5f4cb7cf5-7zrmn
|
||||||
|
Web interface is now available at http://localhost:8899
|
||||||
|
^C
|
||||||
|
|
||||||
|
```
|
||||||
|
Any request that contains `User-Agent` header with one of the specified values (`kube-probe` or `prometheus`) will not be captured
|
||||||
|
|
||||||
|
### API Rules validation
|
||||||
|
|
||||||
|
This feature allows you to define set of simple rules, and test the API against them.
|
||||||
|
Such validation may test response for specific JSON fields, headers, etc.
|
||||||
|
|
||||||
|
Please see [API RULES](docs/POLICY_RULES.md) page for more details and syntax.
|
||||||
|
|
||||||
|
|
||||||
|
## How to Run local UI
|
||||||
|
|
||||||
|
- run from mizu/agent `go run main.go --hars-read --hars-dir <folder>`
|
||||||
|
|
||||||
|
- copy Har files into the folder from last command
|
||||||
|
|
||||||
|
- change `MizuWebsocketURL` and `apiURL` in `api.js` file
|
||||||
|
|
||||||
|
- run from mizu/ui - `npm run start`
|
||||||
|
|
||||||
|
- open browser on `localhost:3000`
|
||||||
|
|||||||
15
TESTING.md
Normal file
15
TESTING.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|

|
||||||
|
# TESTING
|
||||||
|
Testing guidelines for Mizu project
|
||||||
|
|
||||||
|
## Unit-tests
|
||||||
|
* TBD
|
||||||
|
* TBD
|
||||||
|
* TBD
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## System tests
|
||||||
|
* TBD
|
||||||
|
* TBD
|
||||||
|
* TBD
|
||||||
2
acceptanceTests/Makefile
Normal file
2
acceptanceTests/Makefile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
test: ## Run acceptance tests.
|
||||||
|
@go test ./... -timeout 1h
|
||||||
283
acceptanceTests/config_test.go
Normal file
283
acceptanceTests/config_test.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package acceptanceTests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tapConfig struct {
|
||||||
|
GuiPort uint16 `yaml:"gui-port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type configStruct struct {
|
||||||
|
Tap tapConfig `yaml:"tap"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigRegenerate(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath, configPathErr := getConfigPath()
|
||||||
|
if configPathErr != nil {
|
||||||
|
t.Errorf("failed to get config path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configCmdArgs := getDefaultConfigCommandArgs()
|
||||||
|
|
||||||
|
configCmdArgs = append(configCmdArgs, "-r")
|
||||||
|
|
||||||
|
configCmd := exec.Command(cliPath, configCmdArgs...)
|
||||||
|
t.Logf("running command: %v", configCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := os.Remove(configPath); err != nil {
|
||||||
|
t.Logf("failed to delete config file, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := configCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start config command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := configCmd.Wait(); err != nil {
|
||||||
|
t.Errorf("failed to wait config command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, readFileErr := ioutil.ReadFile(configPath)
|
||||||
|
if readFileErr != nil {
|
||||||
|
t.Errorf("failed to read config file, err: %v", readFileErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigGuiPort(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []uint16{8898}
|
||||||
|
|
||||||
|
for _, guiPort := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", guiPort), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath, configPathErr := getConfigPath()
|
||||||
|
if configPathErr != nil {
|
||||||
|
t.Errorf("failed to get config path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := configStruct{}
|
||||||
|
config.Tap.GuiPort = guiPort
|
||||||
|
|
||||||
|
configBytes, marshalErr := yaml.Marshal(config)
|
||||||
|
if marshalErr != nil {
|
||||||
|
t.Errorf("failed to marshal config, err: %v", marshalErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil {
|
||||||
|
t.Errorf("failed to write config to file, err: %v", writeErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(configPath); err != nil {
|
||||||
|
t.Logf("failed to delete config file, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(guiPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSetGuiPort(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ConfigFileGuiPort uint16
|
||||||
|
SetGuiPort uint16
|
||||||
|
}{
|
||||||
|
{ConfigFileGuiPort: 8898, SetGuiPort: 8897},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, guiPortStruct := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", guiPortStruct.SetGuiPort), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath, configPathErr := getConfigPath()
|
||||||
|
if configPathErr != nil {
|
||||||
|
t.Errorf("failed to get config path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := configStruct{}
|
||||||
|
config.Tap.GuiPort = guiPortStruct.ConfigFileGuiPort
|
||||||
|
|
||||||
|
configBytes, marshalErr := yaml.Marshal(config)
|
||||||
|
if marshalErr != nil {
|
||||||
|
t.Errorf("failed to marshal config, err: %v", marshalErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil {
|
||||||
|
t.Errorf("failed to write config to file, err: %v", writeErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "--set", fmt.Sprintf("tap.gui-port=%v", guiPortStruct.SetGuiPort))
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(configPath); err != nil {
|
||||||
|
t.Logf("failed to delete config file, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(guiPortStruct.SetGuiPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFlagGuiPort(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ConfigFileGuiPort uint16
|
||||||
|
FlagGuiPort uint16
|
||||||
|
}{
|
||||||
|
{ConfigFileGuiPort: 8898, FlagGuiPort: 8896},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, guiPortStruct := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", guiPortStruct.FlagGuiPort), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath, configPathErr := getConfigPath()
|
||||||
|
if configPathErr != nil {
|
||||||
|
t.Errorf("failed to get config path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := configStruct{}
|
||||||
|
config.Tap.GuiPort = guiPortStruct.ConfigFileGuiPort
|
||||||
|
|
||||||
|
configBytes, marshalErr := yaml.Marshal(config)
|
||||||
|
if marshalErr != nil {
|
||||||
|
t.Errorf("failed to marshal config, err: %v", marshalErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil {
|
||||||
|
t.Errorf("failed to write config to file, err: %v", writeErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "-p", fmt.Sprintf("%v", guiPortStruct.FlagGuiPort))
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(configPath); err != nil {
|
||||||
|
t.Logf("failed to delete config file, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(guiPortStruct.FlagGuiPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
5
acceptanceTests/go.mod
Normal file
5
acceptanceTests/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/up9inc/mizu/tests
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
4
acceptanceTests/go.sum
Normal file
4
acceptanceTests/go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
55
acceptanceTests/setup.sh
Normal file
55
acceptanceTests/setup.sh
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PREFIX=$HOME/local/bin
|
||||||
|
VERSION=v1.22.0
|
||||||
|
|
||||||
|
echo "Attempting to install minikube and assorted tools to $PREFIX"
|
||||||
|
|
||||||
|
if ! [ -x "$(command -v kubectl)" ]; then
|
||||||
|
echo "Installing kubectl version $VERSION"
|
||||||
|
curl -LO "https://storage.googleapis.com/kubernetes-release/release/$VERSION/bin/linux/amd64/kubectl"
|
||||||
|
chmod +x kubectl
|
||||||
|
mv kubectl "$PREFIX"
|
||||||
|
else
|
||||||
|
echo "kubetcl is already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [ -x "$(command -v minikube)" ]; then
|
||||||
|
echo "Installing minikube version $VERSION"
|
||||||
|
curl -Lo minikube https://storage.googleapis.com/minikube/releases/$VERSION/minikube-linux-amd64
|
||||||
|
chmod +x minikube
|
||||||
|
mv minikube "$PREFIX"
|
||||||
|
else
|
||||||
|
echo "minikube is already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting minikube..."
|
||||||
|
minikube start
|
||||||
|
|
||||||
|
echo "Creating mizu tests namespaces"
|
||||||
|
kubectl create namespace mizu-tests
|
||||||
|
kubectl create namespace mizu-tests2
|
||||||
|
|
||||||
|
echo "Creating httpbin deployments"
|
||||||
|
kubectl create deployment httpbin --image=kennethreitz/httpbin -n mizu-tests
|
||||||
|
kubectl create deployment httpbin2 --image=kennethreitz/httpbin -n mizu-tests
|
||||||
|
|
||||||
|
kubectl create deployment httpbin --image=kennethreitz/httpbin -n mizu-tests2
|
||||||
|
|
||||||
|
echo "Creating httpbin services"
|
||||||
|
kubectl expose deployment httpbin --type=NodePort --port=80 -n mizu-tests
|
||||||
|
kubectl expose deployment httpbin2 --type=NodePort --port=80 -n mizu-tests
|
||||||
|
|
||||||
|
kubectl expose deployment httpbin --type=NodePort --port=80 -n mizu-tests2
|
||||||
|
|
||||||
|
echo "Starting proxy"
|
||||||
|
kubectl proxy --port=8080 &
|
||||||
|
|
||||||
|
echo "Setting minikube docker env"
|
||||||
|
eval $(minikube docker-env)
|
||||||
|
|
||||||
|
echo "Build agent image"
|
||||||
|
make build-docker-ci
|
||||||
|
|
||||||
|
echo "Build cli"
|
||||||
|
make build-cli-ci
|
||||||
765
acceptanceTests/tap_test.go
Normal file
765
acceptanceTests/tap_test.go
Normal file
@@ -0,0 +1,765 @@
|
|||||||
|
package acceptanceTests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTapAndFetch(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []int{50}
|
||||||
|
|
||||||
|
for _, entriesCount := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", entriesCount), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
|
||||||
|
for i := 0; i < entriesCount; i++ {
|
||||||
|
if _, requestErr := executeHttpGetRequest(fmt.Sprintf("%v/get", proxyUrl)); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entriesCheckFunc := func() error {
|
||||||
|
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, entriesCount, timestamp)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entries, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := requestResult.([]interface{})
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return fmt.Errorf("unexpected entries result - Expected more than 0 entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := entries[0].(map[string]interface{})
|
||||||
|
|
||||||
|
entryUrl := fmt.Sprintf("%v/api/entries/%v", apiServerUrl, entry["id"])
|
||||||
|
requestResult, requestErr = executeHttpGetRequest(entryUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestResult == nil {
|
||||||
|
return fmt.Errorf("unexpected nil entry result")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := retriesExecute(shortRetriesCount, entriesCheckFunc); err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCmdArgs := getDefaultFetchCommandArgs()
|
||||||
|
fetchCmd := exec.Command(cliPath, fetchCmdArgs...)
|
||||||
|
t.Logf("running command: %v", fetchCmd.String())
|
||||||
|
|
||||||
|
if err := fetchCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start fetch command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
harCheckFunc := func() error {
|
||||||
|
harBytes, readFileErr := ioutil.ReadFile("./unknown_source.har")
|
||||||
|
if readFileErr != nil {
|
||||||
|
return fmt.Errorf("failed to read har file, err: %v", readFileErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
harEntries, err := getEntriesFromHarBytes(harBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get entries from har, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(harEntries) == 0 {
|
||||||
|
return fmt.Errorf("unexpected har entries result - Expected more than 0 entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := retriesExecute(shortRetriesCount, harCheckFunc); err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapGuiPort(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []uint16{8898}
|
||||||
|
|
||||||
|
for _, guiPort := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", guiPort), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "-p", fmt.Sprintf("%d", guiPort))
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(guiPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapAllNamespaces(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPods := []struct{
|
||||||
|
Name string
|
||||||
|
Namespace string
|
||||||
|
}{
|
||||||
|
{Name: "httpbin", Namespace: "mizu-tests"},
|
||||||
|
{Name: "httpbin", Namespace: "mizu-tests2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "-A")
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
podsUrl := fmt.Sprintf("%v/api/tapStatus", apiServerUrl)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(podsUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
t.Errorf("failed to get tap status, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pods, err := getPods(requestResult)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get pods, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expectedPod := range expectedPods {
|
||||||
|
podFound := false
|
||||||
|
|
||||||
|
for _, pod := range pods {
|
||||||
|
podNamespace := pod["namespace"].(string)
|
||||||
|
podName := pod["name"].(string)
|
||||||
|
|
||||||
|
if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) {
|
||||||
|
podFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !podFound {
|
||||||
|
t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapMultipleNamespaces(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPods := []struct{
|
||||||
|
Name string
|
||||||
|
Namespace string
|
||||||
|
}{
|
||||||
|
{Name: "httpbin", Namespace: "mizu-tests"},
|
||||||
|
{Name: "httpbin2", Namespace: "mizu-tests"},
|
||||||
|
{Name: "httpbin", Namespace: "mizu-tests2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
var namespacesCmd []string
|
||||||
|
for _, expectedPod := range expectedPods {
|
||||||
|
namespacesCmd = append(namespacesCmd, "-n", expectedPod.Namespace)
|
||||||
|
}
|
||||||
|
tapCmdArgs = append(tapCmdArgs, namespacesCmd...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
podsUrl := fmt.Sprintf("%v/api/tapStatus", apiServerUrl)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(podsUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
t.Errorf("failed to get tap status, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pods, err := getPods(requestResult)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get pods, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expectedPods) != len(pods) {
|
||||||
|
t.Errorf("unexpected result - expected pods length: %v, actual pods length: %v", len(expectedPods), len(pods))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expectedPod := range expectedPods {
|
||||||
|
podFound := false
|
||||||
|
|
||||||
|
for _, pod := range pods {
|
||||||
|
podNamespace := pod["namespace"].(string)
|
||||||
|
podName := pod["name"].(string)
|
||||||
|
|
||||||
|
if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) {
|
||||||
|
podFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !podFound {
|
||||||
|
t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapRegex(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
regexPodName := "httpbin2"
|
||||||
|
expectedPods := []struct{
|
||||||
|
Name string
|
||||||
|
Namespace string
|
||||||
|
}{
|
||||||
|
{Name: regexPodName, Namespace: "mizu-tests"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgsWithRegex(regexPodName)
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
podsUrl := fmt.Sprintf("%v/api/tapStatus", apiServerUrl)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(podsUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
t.Errorf("failed to get tap status, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pods, err := getPods(requestResult)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get pods, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expectedPods) != len(pods) {
|
||||||
|
t.Errorf("unexpected result - expected pods length: %v, actual pods length: %v", len(expectedPods), len(pods))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expectedPod := range expectedPods {
|
||||||
|
podFound := false
|
||||||
|
|
||||||
|
for _, pod := range pods {
|
||||||
|
podNamespace := pod["namespace"].(string)
|
||||||
|
podName := pod["name"].(string)
|
||||||
|
|
||||||
|
if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) {
|
||||||
|
podFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !podFound {
|
||||||
|
t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapDryRun(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "--dry-run")
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resultChannel := make(chan string, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := tapCmd.Wait(); err != nil {
|
||||||
|
resultChannel <- "fail"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resultChannel <- "success"
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(shortRetriesCount * time.Second)
|
||||||
|
resultChannel <- "fail"
|
||||||
|
}()
|
||||||
|
|
||||||
|
testResult := <- resultChannel
|
||||||
|
if testResult != "success" {
|
||||||
|
t.Errorf("unexpected result - dry run cmd not done")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapRedact(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
|
||||||
|
requestBody := map[string]string{"User": "Mizu"}
|
||||||
|
for i := 0; i < defaultEntriesCount; i++ {
|
||||||
|
if _, requestErr := executeHttpPostRequest(fmt.Sprintf("%v/post", proxyUrl), requestBody); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redactCheckFunc := func() error {
|
||||||
|
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, defaultEntriesCount, timestamp)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entries, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := requestResult.([]interface{})
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return fmt.Errorf("unexpected entries result - Expected more than 0 entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstEntry := entries[0].(map[string]interface{})
|
||||||
|
|
||||||
|
entryUrl := fmt.Sprintf("%v/api/entries/%v", apiServerUrl, firstEntry["id"])
|
||||||
|
requestResult, requestErr = executeHttpGetRequest(entryUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := requestResult.(map[string]interface{})["entry"].(map[string]interface{})
|
||||||
|
entryRequest := entry["request"].(map[string]interface{})
|
||||||
|
|
||||||
|
headers := entryRequest["headers"].([]interface{})
|
||||||
|
for _, headerInterface := range headers {
|
||||||
|
header := headerInterface.(map[string]interface{})
|
||||||
|
if header["name"].(string) != "User-Agent" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent := header["value"].(string)
|
||||||
|
if userAgent != "[REDACTED]" {
|
||||||
|
return fmt.Errorf("unexpected result - user agent is not redacted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := entryRequest["postData"].(map[string]interface{})
|
||||||
|
textDataStr := data["text"].(string)
|
||||||
|
|
||||||
|
var textData map[string]string
|
||||||
|
if parseErr := json.Unmarshal([]byte(textDataStr), &textData); parseErr != nil {
|
||||||
|
return fmt.Errorf("failed to parse text data, err: %v", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if textData["User"] != "[REDACTED]" {
|
||||||
|
return fmt.Errorf("unexpected result - user in body is not redacted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapNoRedact(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "--no-redact")
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
|
||||||
|
requestBody := map[string]string{"User": "Mizu"}
|
||||||
|
for i := 0; i < defaultEntriesCount; i++ {
|
||||||
|
if _, requestErr := executeHttpPostRequest(fmt.Sprintf("%v/post", proxyUrl), requestBody); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redactCheckFunc := func() error {
|
||||||
|
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, defaultEntriesCount, timestamp)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entries, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := requestResult.([]interface{})
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return fmt.Errorf("unexpected entries result - Expected more than 0 entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstEntry := entries[0].(map[string]interface{})
|
||||||
|
|
||||||
|
entryUrl := fmt.Sprintf("%v/api/entries/%v", apiServerUrl, firstEntry["id"])
|
||||||
|
requestResult, requestErr = executeHttpGetRequest(entryUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := requestResult.(map[string]interface{})["entry"].(map[string]interface{})
|
||||||
|
entryRequest := entry["request"].(map[string]interface{})
|
||||||
|
|
||||||
|
headers := entryRequest["headers"].([]interface{})
|
||||||
|
for _, headerInterface := range headers {
|
||||||
|
header := headerInterface.(map[string]interface{})
|
||||||
|
if header["name"].(string) != "User-Agent" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent := header["value"].(string)
|
||||||
|
if userAgent == "[REDACTED]" {
|
||||||
|
return fmt.Errorf("unexpected result - user agent is redacted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := entryRequest["postData"].(map[string]interface{})
|
||||||
|
textDataStr := data["text"].(string)
|
||||||
|
|
||||||
|
var textData map[string]string
|
||||||
|
if parseErr := json.Unmarshal([]byte(textDataStr), &textData); parseErr != nil {
|
||||||
|
return fmt.Errorf("failed to parse text data, err: %v", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if textData["User"] == "[REDACTED]" {
|
||||||
|
return fmt.Errorf("unexpected result - user in body is redacted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapRegexMasking(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "-r", "Mizu")
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
|
||||||
|
for i := 0; i < defaultEntriesCount; i++ {
|
||||||
|
response, requestErr := http.Post(fmt.Sprintf("%v/post", proxyUrl), "text/plain", bytes.NewBufferString("Mizu"))
|
||||||
|
if _, requestErr = executeHttpRequest(response, requestErr); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redactCheckFunc := func() error {
|
||||||
|
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, defaultEntriesCount, timestamp)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entries, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := requestResult.([]interface{})
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return fmt.Errorf("unexpected entries result - Expected more than 0 entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstEntry := entries[0].(map[string]interface{})
|
||||||
|
|
||||||
|
entryUrl := fmt.Sprintf("%v/api/entries/%v", apiServerUrl, firstEntry["id"])
|
||||||
|
requestResult, requestErr = executeHttpGetRequest(entryUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := requestResult.(map[string]interface{})["entry"].(map[string]interface{})
|
||||||
|
entryRequest := entry["request"].(map[string]interface{})
|
||||||
|
|
||||||
|
data := entryRequest["postData"].(map[string]interface{})
|
||||||
|
textData := data["text"].(string)
|
||||||
|
|
||||||
|
if textData != "[REDACTED]" {
|
||||||
|
return fmt.Errorf("unexpected result - body is not redacted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
205
acceptanceTests/testsUtils.go
Normal file
205
acceptanceTests/testsUtils.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package acceptanceTests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
longRetriesCount = 100
|
||||||
|
shortRetriesCount = 10
|
||||||
|
defaultApiServerPort = 8899
|
||||||
|
defaultNamespaceName = "mizu-tests"
|
||||||
|
defaultServiceName = "httpbin"
|
||||||
|
defaultEntriesCount = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCliPath() (string, error) {
|
||||||
|
dir, filePathErr := os.Getwd()
|
||||||
|
if filePathErr != nil {
|
||||||
|
return "", filePathErr
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath := path.Join(dir, "../cli/bin/mizu_ci")
|
||||||
|
return cliPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfigPath() (string, error) {
|
||||||
|
home, homeDirErr := os.UserHomeDir()
|
||||||
|
if homeDirErr != nil {
|
||||||
|
return "", homeDirErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Join(home, ".mizu", "config.yaml"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProxyUrl(namespace string, service string) string {
|
||||||
|
return fmt.Sprintf("http://localhost:8080/api/v1/namespaces/%v/services/%v/proxy", namespace, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getApiServerUrl(port uint16) string {
|
||||||
|
return fmt.Sprintf("http://localhost:%v/mizu", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultCommandArgs() []string {
|
||||||
|
setFlag := "--set"
|
||||||
|
telemetry := "telemetry=false"
|
||||||
|
agentImage := "agent-image=gcr.io/up9-docker-hub/mizu/ci:0.0.0"
|
||||||
|
imagePullPolicy := "image-pull-policy=Never"
|
||||||
|
|
||||||
|
return []string{setFlag, telemetry, setFlag, agentImage, setFlag, imagePullPolicy}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultTapCommandArgs() []string {
|
||||||
|
tapCommand := "tap"
|
||||||
|
defaultCmdArgs := getDefaultCommandArgs()
|
||||||
|
|
||||||
|
return append([]string{tapCommand}, defaultCmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultTapCommandArgsWithRegex(regex string) []string {
|
||||||
|
tapCommand := "tap"
|
||||||
|
defaultCmdArgs := getDefaultCommandArgs()
|
||||||
|
|
||||||
|
return append([]string{tapCommand, regex}, defaultCmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultTapNamespace() []string {
|
||||||
|
return []string{"-n", "mizu-tests"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultFetchCommandArgs() []string {
|
||||||
|
fetchCommand := "fetch"
|
||||||
|
defaultCmdArgs := getDefaultCommandArgs()
|
||||||
|
|
||||||
|
return append([]string{fetchCommand}, defaultCmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultConfigCommandArgs() []string {
|
||||||
|
configCommand := "config"
|
||||||
|
defaultCmdArgs := getDefaultCommandArgs()
|
||||||
|
|
||||||
|
return append([]string{configCommand}, defaultCmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func retriesExecute(retriesCount int, executeFunc func() error) error {
|
||||||
|
var lastError error
|
||||||
|
|
||||||
|
for i := 0; i < retriesCount; i++ {
|
||||||
|
if err := executeFunc(); err != nil {
|
||||||
|
lastError = err
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("reached max retries count, retries count: %v, last err: %v", retriesCount, lastError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitTapPodsReady(apiServerUrl string) error {
|
||||||
|
resolvingUrl := fmt.Sprintf("%v/status/tappersCount", apiServerUrl)
|
||||||
|
tapPodsReadyFunc := func() error {
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(resolvingUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return requestErr
|
||||||
|
}
|
||||||
|
|
||||||
|
tappersCount := requestResult.(float64)
|
||||||
|
if tappersCount == 0 {
|
||||||
|
return fmt.Errorf("no tappers running")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return retriesExecute(longRetriesCount, tapPodsReadyFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonBytesToInterface(jsonBytes []byte) (interface{}, error) {
|
||||||
|
var result interface{}
|
||||||
|
if parseErr := json.Unmarshal(jsonBytes, &result); parseErr != nil {
|
||||||
|
return nil, parseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeHttpRequest(response *http.Response, requestErr error) (interface{}, error) {
|
||||||
|
if requestErr != nil {
|
||||||
|
return nil, requestErr
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("invalid status code %v", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { response.Body.Close() }()
|
||||||
|
|
||||||
|
data, readErr := ioutil.ReadAll(response.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonBytesToInterface(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeHttpGetRequest(url string) (interface{}, error) {
|
||||||
|
response, requestErr := http.Get(url)
|
||||||
|
return executeHttpRequest(response, requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeHttpPostRequest(url string, body interface{}) (interface{}, error) {
|
||||||
|
requestBody, jsonErr := json.Marshal(body)
|
||||||
|
if jsonErr != nil {
|
||||||
|
return nil, jsonErr
|
||||||
|
}
|
||||||
|
|
||||||
|
response, requestErr := http.Post(url, "application/json", bytes.NewBuffer(requestBody))
|
||||||
|
return executeHttpRequest(response, requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupCommand(cmd *exec.Cmd) error {
|
||||||
|
if err := cmd.Process.Signal(syscall.SIGQUIT); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEntriesFromHarBytes(harBytes []byte) ([]interface{}, error) {
|
||||||
|
harInterface, convertErr := jsonBytesToInterface(harBytes)
|
||||||
|
if convertErr != nil {
|
||||||
|
return nil, convertErr
|
||||||
|
}
|
||||||
|
|
||||||
|
har := harInterface.(map[string]interface{})
|
||||||
|
harLog := har["log"].(map[string]interface{})
|
||||||
|
harEntries := harLog["entries"].([]interface{})
|
||||||
|
|
||||||
|
return harEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPods(tapStatusInterface interface{}) ([]map[string]interface{}, error) {
|
||||||
|
tapStatus := tapStatusInterface.(map[string]interface{})
|
||||||
|
podsInterface := tapStatus["pods"].([]interface{})
|
||||||
|
|
||||||
|
var pods []map[string]interface{}
|
||||||
|
for _, podInterface := range podsInterface {
|
||||||
|
pods = append(pods, podInterface.(map[string]interface{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return pods, nil
|
||||||
|
}
|
||||||
2
agent/Makefile
Normal file
2
agent/Makefile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
test: ## Run agent tests.
|
||||||
|
@go test ./... -coverpkg=./... -race -coverprofile=coverage.out -covermode=atomic
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# mizu API server
|
# mizu agent
|
||||||
API server for MIZU
|
Agent for MIZU (API server and tapper)
|
||||||
Basic APIs:
|
Basic APIs:
|
||||||
* /fetch - retrieve traffic data
|
* /fetch - retrieve traffic data
|
||||||
* /stats - retrieve statistics of collected data
|
* /stats - retrieve statistics of collected data
|
||||||
@@ -14,7 +14,7 @@ Basic APIs:
|
|||||||
|
|
||||||
### Connecting
|
### Connecting
|
||||||
1. Start mizu using the cli with the debug image `mizu tap --mizu-image gcr.io/up9-docker-hub/mizu/debug:latest {tapped_pod_name}`
|
1. Start mizu using the cli with the debug image `mizu tap --mizu-image gcr.io/up9-docker-hub/mizu/debug:latest {tapped_pod_name}`
|
||||||
2. Forward the debug port using `kubectl port-forward -n default mizu-collector 2345:2345`
|
2. Forward the debug port using `kubectl port-forward -n default mizu-api-server 2345:2345`
|
||||||
3. Run the run/debug configuration you've created earlier in Intellij.
|
3. Run the run/debug configuration you've created earlier in Intellij.
|
||||||
|
|
||||||
<small>Do note that dlv won't start the api until a debugger connects to it.</small>
|
<small>Do note that dlv won't start the api until a debugger connects to it.</small>
|
||||||
@@ -3,23 +3,23 @@ module mizuserver
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/antoniodipinto/ikisocket v0.0.0-20210417133349-f1502512d69a
|
|
||||||
github.com/beevik/etree v1.1.0
|
|
||||||
github.com/djherbis/atime v1.0.0
|
github.com/djherbis/atime v1.0.0
|
||||||
github.com/fasthttp/websocket v1.4.3-beta.1 // indirect
|
github.com/fsnotify/fsnotify v1.4.9
|
||||||
|
github.com/gin-contrib/static v0.0.1
|
||||||
|
github.com/gin-gonic/gin v1.7.2
|
||||||
github.com/go-playground/locales v0.13.0
|
github.com/go-playground/locales v0.13.0
|
||||||
github.com/go-playground/universal-translator v0.17.0
|
github.com/go-playground/universal-translator v0.17.0
|
||||||
github.com/go-playground/validator/v10 v10.5.0
|
github.com/go-playground/validator/v10 v10.5.0
|
||||||
github.com/gofiber/fiber/v2 v2.8.0
|
|
||||||
github.com/google/gopacket v1.1.19
|
|
||||||
github.com/google/martian v2.1.0+incompatible
|
github.com/google/martian v2.1.0+incompatible
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
|
||||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
|
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
|
github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7
|
||||||
github.com/up9inc/mizu/shared v0.0.0
|
github.com/up9inc/mizu/shared v0.0.0
|
||||||
go.mongodb.org/mongo-driver v1.5.1
|
github.com/up9inc/mizu/tap v0.0.0
|
||||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758
|
github.com/up9inc/mizu/tap/api v0.0.0
|
||||||
|
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0
|
||||||
|
go.mongodb.org/mongo-driver v1.7.1
|
||||||
gorm.io/driver/sqlite v1.1.4
|
gorm.io/driver/sqlite v1.1.4
|
||||||
gorm.io/gorm v1.21.8
|
gorm.io/gorm v1.21.8
|
||||||
k8s.io/api v0.21.0
|
k8s.io/api v0.21.0
|
||||||
@@ -28,3 +28,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/up9inc/mizu/shared v0.0.0 => ../shared
|
replace github.com/up9inc/mizu/shared v0.0.0 => ../shared
|
||||||
|
|
||||||
|
replace github.com/up9inc/mizu/tap v0.0.0 => ../tap
|
||||||
|
|
||||||
|
replace github.com/up9inc/mizu/tap/api v0.0.0 => ../tap/api
|
||||||
@@ -41,15 +41,9 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
|||||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
|
||||||
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
|
|
||||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
|
||||||
github.com/antoniodipinto/ikisocket v0.0.0-20210417133349-f1502512d69a h1:76llBleIE3fkdqaJFDzdirtiYhQPdIQem8H8r2iwA1Q=
|
|
||||||
github.com/antoniodipinto/ikisocket v0.0.0-20210417133349-f1502512d69a/go.mod h1:QvDfsDQDmGxUsvEeWabVZ5pp2FMXpOkwQV0L6SE6cp0=
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||||
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
|
github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4 h1:NJOOlc6ZJjix0A1rAU+nxruZtR8KboG1848yqpIUo4M=
|
||||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4/go.mod h1:DQPxZS994Ld1Y8uwnJT+dRL04XPD0cElP/pHH/zEBHM=
|
||||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
@@ -61,18 +55,26 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/djherbis/atime v1.0.0 h1:ySLvBAM0EvOGaX7TI4dAM5lWj+RdJUCKtGSEHN8SGBg=
|
github.com/djherbis/atime v1.0.0 h1:ySLvBAM0EvOGaX7TI4dAM5lWj+RdJUCKtGSEHN8SGBg=
|
||||||
github.com/djherbis/atime v1.0.0/go.mod h1:5W+KBIuTwVGcqjIfaTwt+KSYX1o6uep8dtevevQP/f8=
|
github.com/djherbis/atime v1.0.0/go.mod h1:5W+KBIuTwVGcqjIfaTwt+KSYX1o6uep8dtevevQP/f8=
|
||||||
|
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||||
|
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
github.com/fasthttp/websocket v1.4.2/go.mod h1:smsv/h4PBEBaU0XDTY5UwJTpZv69fQ0FfcLJr21mA6Y=
|
|
||||||
github.com/fasthttp/websocket v1.4.3-beta.1 h1:stc4P2aoxYKsdmbe1AJ5mAm73Fxc1NOgrZpPftvZIXQ=
|
|
||||||
github.com/fasthttp/websocket v1.4.3-beta.1/go.mod h1:JGrgLaT02bL9NuJkZbHN8mVV2tkCJZQh7yJ5/XCXO2g=
|
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
|
||||||
|
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
|
||||||
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
|
||||||
|
github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
@@ -92,9 +94,10 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
|
|||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
github.com/go-playground/validator/v10 v10.5.0 h1:X9rflw/KmpACwT8zdrm1upefpvdy6ur8d1kWyq6sg3E=
|
github.com/go-playground/validator/v10 v10.5.0 h1:X9rflw/KmpACwT8zdrm1upefpvdy6ur8d1kWyq6sg3E=
|
||||||
github.com/go-playground/validator/v10 v10.5.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
|
github.com/go-playground/validator/v10 v10.5.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
|
||||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
|
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
|
||||||
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
|
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
|
||||||
@@ -120,12 +123,6 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe
|
|||||||
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
|
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
|
||||||
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
||||||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||||
github.com/gofiber/fiber/v2 v2.1.3/go.mod h1:MMiSv1HrDkN8Pv7NeVDYK+T/lwXOEKAvPBbLvJPCEfA=
|
|
||||||
github.com/gofiber/fiber/v2 v2.7.1/go.mod h1:f8BRRIMjMdRyt2qmJ/0Sea3j3rwwfufPrh9WNBRiVZ0=
|
|
||||||
github.com/gofiber/fiber/v2 v2.8.0 h1:BdWvZmg/WY/Vjtjm38aXOp1Lks1BhuyS2b7lSWSPAzk=
|
|
||||||
github.com/gofiber/fiber/v2 v2.8.0/go.mod h1:Ah3IJikrKNRepl/HuVawppS25X7FWohwfCSRn7kJG28=
|
|
||||||
github.com/gofiber/websocket/v2 v2.0.3 h1:nqPGHB4LQhxKX5KJUjayOd2xiiENieS/dn6TPfCL8uk=
|
|
||||||
github.com/gofiber/websocket/v2 v2.0.3/go.mod h1:/OTEImCxORKE5unw0dWqJYovid6vZF+wB1W0aaMKs2M=
|
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
@@ -186,7 +183,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
|
|
||||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
@@ -194,10 +190,9 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
|||||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
|
||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
@@ -206,13 +201,7 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR
|
|||||||
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
|
||||||
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
|
||||||
github.com/klauspost/compress v1.11.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
|
||||||
github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4=
|
|
||||||
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
|
||||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -222,13 +211,14 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
|
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
|
||||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
@@ -264,9 +254,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
|
|||||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY=
|
github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 h1:jkvpcEatpwuMF5O5LVxTnehj6YZ/aEZN4NWD/Xml4pI=
|
||||||
github.com/savsgio/gotils v0.0.0-20200616100644-13ff1fd2c28c h1:KKqhycXW1WVNkX7r4ekTV2gFkbhdyihlWD8c0/FiWmk=
|
github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7/go.mod h1:KTrHyWpO1sevuXPZwyeZc72ddWRFqNSKDFl7uVWKpg0=
|
||||||
github.com/savsgio/gotils v0.0.0-20200616100644-13ff1fd2c28c/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8=
|
|
||||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
@@ -278,33 +267,27 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
github.com/valyala/fasthttp v1.15.1/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
|
||||||
github.com/valyala/fasthttp v1.18.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A=
|
|
||||||
github.com/valyala/fasthttp v1.23.0 h1:0ufwSD9BhWa6f8HWdmdq4FHQ23peRo3Ng/Qs8m5NcFs=
|
|
||||||
github.com/valyala/fasthttp v1.23.0/go.mod h1:0mw2RjXGOzxf4NL2jni3gUQ7LfjjUSiG5sskOUUSEpU=
|
|
||||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc=
|
|
||||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||||
|
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
|
||||||
|
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI=
|
go.mongodb.org/mongo-driver v1.7.1 h1:jwqTeEM3x6L9xDXrCxN0Hbg7vdGfPBOTIkr0+/LYZDA=
|
||||||
go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=
|
go.mongodb.org/mongo-driver v1.7.1/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
@@ -372,13 +355,9 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 h1:OgUuv8lsRpBibGNbSizVwKWlysjaNzmC9gYMhPVfqFM=
|
||||||
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210226101413-39120d07d75e/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758 h1:aEpZnXcAmXkd6AvLb2OPt+EN1Zu/8Ne3pCqPjja5PXY=
|
|
||||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -410,24 +389,22 @@ golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201210223839-7e3030f88018/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M=
|
||||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe h1:WdX7u8s3yOigWAhHEaDl8r9G+4XwFQEQFtBMYyN+kXQ=
|
|
||||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
|
||||||
@@ -438,9 +415,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
|||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -555,8 +531,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
||||||
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
||||||
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
326
agent/main.go
Normal file
326
agent/main.go
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"mizuserver/pkg/api"
|
||||||
|
"mizuserver/pkg/controllers"
|
||||||
|
"mizuserver/pkg/models"
|
||||||
|
"mizuserver/pkg/routes"
|
||||||
|
"mizuserver/pkg/utils"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"plugin"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/static"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/romana/rlog"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/tap"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tapperMode = flag.Bool("tap", false, "Run in tapper mode without API")
|
||||||
|
var apiServerMode = flag.Bool("api-server", false, "Run in API server mode with API")
|
||||||
|
var standaloneMode = flag.Bool("standalone", false, "Run in standalone tapper and API mode")
|
||||||
|
var apiServerAddress = flag.String("api-server-address", "", "Address of mizu API server")
|
||||||
|
var namespace = flag.String("namespace", "", "Resolve IPs if they belong to resources in this namespace (default is all)")
|
||||||
|
var harsReaderMode = flag.Bool("hars-read", false, "Run in hars-read mode")
|
||||||
|
var harsDir = flag.String("hars-dir", "", "Directory to read hars from")
|
||||||
|
|
||||||
|
var extensions []*tapApi.Extension // global
|
||||||
|
var extensionsMap map[string]*tapApi.Extension // global
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
loadExtensions()
|
||||||
|
hostMode := os.Getenv(shared.HostModeEnvVar) == "1"
|
||||||
|
tapOpts := &tap.TapOpts{HostMode: hostMode}
|
||||||
|
|
||||||
|
if !*tapperMode && !*apiServerMode && !*standaloneMode && !*harsReaderMode {
|
||||||
|
panic("One of the flags --tap, --api or --standalone or --hars-read must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *standaloneMode {
|
||||||
|
api.StartResolving(*namespace)
|
||||||
|
|
||||||
|
outputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
filteredOutputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
tap.StartPassiveTapper(tapOpts, outputItemsChannel, extensions)
|
||||||
|
|
||||||
|
go filterItems(outputItemsChannel, filteredOutputItemsChannel, getTrafficFilteringOptions())
|
||||||
|
go api.StartReadingEntries(filteredOutputItemsChannel, nil, extensionsMap)
|
||||||
|
// go api.StartReadingOutbound(outboundLinkOutputChannel)
|
||||||
|
|
||||||
|
hostApi(nil)
|
||||||
|
} else if *tapperMode {
|
||||||
|
if *apiServerAddress == "" {
|
||||||
|
panic("API server address must be provided with --api-server-address when using --tap")
|
||||||
|
}
|
||||||
|
|
||||||
|
tapTargets := getTapTargets()
|
||||||
|
if tapTargets != nil {
|
||||||
|
tap.SetFilterAuthorities(tapTargets)
|
||||||
|
rlog.Infof("Filtering for the following authorities: %v", tap.GetFilterIPs())
|
||||||
|
}
|
||||||
|
|
||||||
|
// harOutputChannel, outboundLinkOutputChannel := tap.StartPassiveTapper(tapOpts)
|
||||||
|
filteredOutputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
tap.StartPassiveTapper(tapOpts, filteredOutputItemsChannel, extensions)
|
||||||
|
socketConnection, err := shared.ConnectToSocketServer(*apiServerAddress, shared.DEFAULT_SOCKET_RETRIES, shared.DEFAULT_SOCKET_RETRY_SLEEP_TIME, false)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Error connecting to socket server at %s %v", *apiServerAddress, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
go pipeTapChannelToSocket(socketConnection, filteredOutputItemsChannel)
|
||||||
|
// go pipeOutboundLinksChannelToSocket(socketConnection, outboundLinkOutputChannel)
|
||||||
|
} else if *apiServerMode {
|
||||||
|
api.StartResolving(*namespace)
|
||||||
|
|
||||||
|
outputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
filteredOutputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
|
||||||
|
go filterItems(outputItemsChannel, filteredOutputItemsChannel, getTrafficFilteringOptions())
|
||||||
|
go api.StartReadingEntries(filteredOutputItemsChannel, nil, extensionsMap)
|
||||||
|
|
||||||
|
hostApi(outputItemsChannel)
|
||||||
|
} else if *harsReaderMode {
|
||||||
|
outputItemsChannel := make(chan *tapApi.OutputChannelItem, 1000)
|
||||||
|
filteredHarChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
|
||||||
|
go filterItems(outputItemsChannel, filteredHarChannel, getTrafficFilteringOptions())
|
||||||
|
go api.StartReadingEntries(filteredHarChannel, harsDir, extensionsMap)
|
||||||
|
hostApi(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, os.Interrupt)
|
||||||
|
<-signalChan
|
||||||
|
|
||||||
|
rlog.Info("Exiting")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadExtensions() {
|
||||||
|
dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||||
|
extensionsDir := path.Join(dir, "./extensions/")
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(extensionsDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
extensions = make([]*tapApi.Extension, len(files))
|
||||||
|
extensionsMap = make(map[string]*tapApi.Extension)
|
||||||
|
for i, file := range files {
|
||||||
|
filename := file.Name()
|
||||||
|
log.Printf("Loading extension: %s\n", filename)
|
||||||
|
extension := &tapApi.Extension{
|
||||||
|
Path: path.Join(extensionsDir, filename),
|
||||||
|
}
|
||||||
|
plug, _ := plugin.Open(extension.Path)
|
||||||
|
extension.Plug = plug
|
||||||
|
symDissector, err := plug.Lookup("Dissector")
|
||||||
|
|
||||||
|
var dissector tapApi.Dissector
|
||||||
|
var ok bool
|
||||||
|
dissector, ok = symDissector.(tapApi.Dissector)
|
||||||
|
if err != nil || !ok {
|
||||||
|
panic(fmt.Sprintf("Failed to load the extension: %s\n", extension.Path))
|
||||||
|
}
|
||||||
|
dissector.Register(extension)
|
||||||
|
extension.Dissector = dissector
|
||||||
|
extensions[i] = extension
|
||||||
|
extensionsMap[extension.Protocol.Name] = extension
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(extensions, func(i, j int) bool {
|
||||||
|
return extensions[i].Protocol.Priority < extensions[j].Protocol.Priority
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, extension := range extensions {
|
||||||
|
log.Printf("Extension Properties: %+v\n", extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
controllers.InitExtensionsMap(extensionsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostApi(socketHarOutputChannel chan<- *tapApi.OutputChannelItem) {
|
||||||
|
app := gin.Default()
|
||||||
|
|
||||||
|
app.GET("/echo", func(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, "Here is Mizu agent")
|
||||||
|
})
|
||||||
|
|
||||||
|
eventHandlers := api.RoutesEventHandlers{
|
||||||
|
SocketOutChannel: socketHarOutputChannel,
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Use(DisableRootStaticCache())
|
||||||
|
app.Use(static.ServeRoot("/", "./site"))
|
||||||
|
app.Use(CORSMiddleware()) // This has to be called after the static middleware, does not work if its called before
|
||||||
|
|
||||||
|
api.WebSocketRoutes(app, &eventHandlers)
|
||||||
|
routes.EntriesRoutes(app)
|
||||||
|
routes.MetadataRoutes(app)
|
||||||
|
routes.StatusRoutes(app)
|
||||||
|
routes.NotFoundRoute(app)
|
||||||
|
|
||||||
|
utils.StartServer(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DisableRootStaticCache() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if c.Request.RequestURI == "/" {
|
||||||
|
// Disable cache only for the main static route
|
||||||
|
c.Writer.Header().Set("Cache-Control", "no-store")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CORSMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
|
||||||
|
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(204)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEnvVar(env string) map[string][]string {
|
||||||
|
var mapOfList map[string][]string
|
||||||
|
|
||||||
|
val, present := os.LookupEnv(env)
|
||||||
|
|
||||||
|
if !present {
|
||||||
|
return mapOfList
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(val), &mapOfList)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("env var %s's value of %s is invalid! must be map[string][]string %v", env, mapOfList, err))
|
||||||
|
}
|
||||||
|
return mapOfList
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTapTargets() []string {
|
||||||
|
nodeName := os.Getenv(shared.NodeNameEnvVar)
|
||||||
|
tappedAddressesPerNodeDict := parseEnvVar(shared.TappedAddressesPerNodeDictEnvVar)
|
||||||
|
return tappedAddressesPerNodeDict[nodeName]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTrafficFilteringOptions() *shared.TrafficFilteringOptions {
|
||||||
|
filteringOptionsJson := os.Getenv(shared.MizuFilteringOptionsEnvVar)
|
||||||
|
if filteringOptionsJson == "" {
|
||||||
|
return &shared.TrafficFilteringOptions{
|
||||||
|
HealthChecksUserAgentHeaders: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var filteringOptions shared.TrafficFilteringOptions
|
||||||
|
err := json.Unmarshal([]byte(filteringOptionsJson), &filteringOptions)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("env var %s's value of %s is invalid! json must match the shared.TrafficFilteringOptions struct %v", shared.MizuFilteringOptionsEnvVar, filteringOptionsJson, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &filteringOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterItems(inChannel <-chan *tapApi.OutputChannelItem, outChannel chan *tapApi.OutputChannelItem, filterOptions *shared.TrafficFilteringOptions) {
|
||||||
|
for message := range inChannel {
|
||||||
|
if message.ConnectionInfo.IsOutgoing && api.CheckIsServiceIP(message.ConnectionInfo.ServerIP) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// TODO: move this to tappers https://up9.atlassian.net/browse/TRA-3441
|
||||||
|
if isHealthCheckByUserAgent(message, filterOptions.HealthChecksUserAgentHeaders) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if !filterOptions.DisableRedaction {
|
||||||
|
// sensitiveDataFiltering.FilterSensitiveInfoFromHarRequest(message, filterOptions)
|
||||||
|
// }
|
||||||
|
|
||||||
|
outChannel <- message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHealthCheckByUserAgent(item *tapApi.OutputChannelItem, userAgentsToIgnore []string) bool {
|
||||||
|
if item.Protocol.Name != "http" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
request := item.Pair.Request.Payload.(map[string]interface{})
|
||||||
|
reqDetails := request["details"].(map[string]interface{})
|
||||||
|
|
||||||
|
for _, header := range reqDetails["headers"].([]interface{}) {
|
||||||
|
h := header.(map[string]interface{})
|
||||||
|
if strings.ToLower(h["name"].(string)) == "user-agent" {
|
||||||
|
for _, userAgent := range userAgentsToIgnore {
|
||||||
|
if strings.Contains(strings.ToLower(h["value"].(string)), strings.ToLower(userAgent)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipeTapChannelToSocket(connection *websocket.Conn, messageDataChannel <-chan *tapApi.OutputChannelItem) {
|
||||||
|
if connection == nil {
|
||||||
|
panic("Websocket connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if messageDataChannel == nil {
|
||||||
|
panic("Channel of captured messages is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
for messageData := range messageDataChannel {
|
||||||
|
marshaledData, err := models.CreateWebsocketTappedEntryMessage(messageData)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Infof("error converting message to json %s, (%v,%+v)\n", err, err, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This is where the `*tapApi.OutputChannelItem` leaves the code
|
||||||
|
// and goes into the intermediate WebSocket.
|
||||||
|
err = connection.WriteMessage(websocket.TextMessage, marshaledData)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Infof("error sending message through socket server %s, (%v,%+v)\n", err, err, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipeOutboundLinksChannelToSocket(connection *websocket.Conn, outboundLinkChannel <-chan *tap.OutboundLink) {
|
||||||
|
for outboundLink := range outboundLinkChannel {
|
||||||
|
if outboundLink.SuggestedProtocol == tap.TLSProtocol {
|
||||||
|
marshaledData, err := models.CreateWebsocketOutboundLinkMessage(outboundLink)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Infof("Error converting outbound link to json %s, (%v,%+v)", err, err, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = connection.WriteMessage(websocket.TextMessage, marshaledData)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Infof("error sending outbound link message through socket server %s, (%v,%+v)", err, err, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
182
agent/pkg/api/main.go
Normal file
182
agent/pkg/api/main.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mizuserver/pkg/database"
|
||||||
|
"mizuserver/pkg/holder"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
|
||||||
|
"github.com/google/martian/har"
|
||||||
|
"github.com/romana/rlog"
|
||||||
|
"github.com/up9inc/mizu/tap"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
|
||||||
|
"mizuserver/pkg/models"
|
||||||
|
"mizuserver/pkg/resolver"
|
||||||
|
"mizuserver/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var k8sResolver *resolver.Resolver
|
||||||
|
|
||||||
|
func StartResolving(namespace string) {
|
||||||
|
errOut := make(chan error, 100)
|
||||||
|
res, err := resolver.NewFromInCluster(errOut, namespace)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Infof("error creating k8s resolver %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
res.Start(ctx)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-errOut:
|
||||||
|
rlog.Infof("name resolving error %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
k8sResolver = res
|
||||||
|
holder.SetResolver(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartReadingEntries(harChannel <-chan *tapApi.OutputChannelItem, workingDir *string, extensionsMap map[string]*tapApi.Extension) {
|
||||||
|
if workingDir != nil && *workingDir != "" {
|
||||||
|
startReadingFiles(*workingDir)
|
||||||
|
} else {
|
||||||
|
startReadingChannel(harChannel, extensionsMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startReadingFiles(workingDir string) {
|
||||||
|
err := os.MkdirAll(workingDir, os.ModePerm)
|
||||||
|
utils.CheckErr(err)
|
||||||
|
|
||||||
|
for true {
|
||||||
|
dir, _ := os.Open(workingDir)
|
||||||
|
dirFiles, _ := dir.Readdir(-1)
|
||||||
|
|
||||||
|
var harFiles []os.FileInfo
|
||||||
|
for _, fileInfo := range dirFiles {
|
||||||
|
if strings.HasSuffix(fileInfo.Name(), ".har") {
|
||||||
|
harFiles = append(harFiles, fileInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(utils.ByModTime(harFiles))
|
||||||
|
|
||||||
|
if len(harFiles) == 0 {
|
||||||
|
rlog.Infof("Waiting for new files\n")
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fileInfo := harFiles[0]
|
||||||
|
inputFilePath := path.Join(workingDir, fileInfo.Name())
|
||||||
|
file, err := os.Open(inputFilePath)
|
||||||
|
utils.CheckErr(err)
|
||||||
|
|
||||||
|
var inputHar har.HAR
|
||||||
|
decErr := json.NewDecoder(bufio.NewReader(file)).Decode(&inputHar)
|
||||||
|
utils.CheckErr(decErr)
|
||||||
|
|
||||||
|
// for _, entry := range inputHar.Log.Entries {
|
||||||
|
// time.Sleep(time.Millisecond * 250)
|
||||||
|
// // connectionInfo := &tap.ConnectionInfo{
|
||||||
|
// // ClientIP: fileInfo.Name(),
|
||||||
|
// // ClientPort: "",
|
||||||
|
// // ServerIP: "",
|
||||||
|
// // ServerPort: "",
|
||||||
|
// // IsOutgoing: false,
|
||||||
|
// // }
|
||||||
|
// // saveHarToDb(entry, connectionInfo)
|
||||||
|
// }
|
||||||
|
rmErr := os.Remove(inputFilePath)
|
||||||
|
utils.CheckErr(rmErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extensionsMap map[string]*tapApi.Extension) {
|
||||||
|
if outputItems == nil {
|
||||||
|
panic("Channel of captured messages is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
for item := range outputItems {
|
||||||
|
extension := extensionsMap[item.Protocol.Name]
|
||||||
|
resolvedSource, resolvedDestionation := resolveIP(item.ConnectionInfo)
|
||||||
|
mizuEntry := extension.Dissector.Analyze(item, primitive.NewObjectID().Hex(), resolvedSource, resolvedDestionation)
|
||||||
|
baseEntry := extension.Dissector.Summarize(mizuEntry)
|
||||||
|
mizuEntry.EstimatedSizeBytes = getEstimatedEntrySizeBytes(mizuEntry)
|
||||||
|
database.CreateEntry(mizuEntry)
|
||||||
|
baseEntryBytes, _ := models.CreateBaseEntryWebSocketMessage(baseEntry)
|
||||||
|
BroadcastToBrowserClients(baseEntryBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartReadingOutbound(outboundLinkChannel <-chan *tap.OutboundLink) {
|
||||||
|
// tcpStreamFactory will block on write to channel. Empty channel to unblock.
|
||||||
|
// TODO: Make write to channel optional.
|
||||||
|
for range outboundLinkChannel {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveIP(connectionInfo *tapApi.ConnectionInfo) (resolvedSource string, resolvedDestination string) {
|
||||||
|
if k8sResolver != nil {
|
||||||
|
unresolvedSource := connectionInfo.ClientIP
|
||||||
|
resolvedSource = k8sResolver.Resolve(unresolvedSource)
|
||||||
|
if resolvedSource == "" {
|
||||||
|
rlog.Debugf("Cannot find resolved name to source: %s\n", unresolvedSource)
|
||||||
|
if os.Getenv("SKIP_NOT_RESOLVED_SOURCE") == "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unresolvedDestination := fmt.Sprintf("%s:%s", connectionInfo.ServerIP, connectionInfo.ServerPort)
|
||||||
|
resolvedDestination = k8sResolver.Resolve(unresolvedDestination)
|
||||||
|
if resolvedDestination == "" {
|
||||||
|
rlog.Debugf("Cannot find resolved name to dest: %s\n", unresolvedDestination)
|
||||||
|
if os.Getenv("SKIP_NOT_RESOLVED_DEST") == "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolvedSource, resolvedDestination
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServiceNameFromUrl(inputUrl string) (string, string) {
|
||||||
|
parsed, err := url.Parse(inputUrl)
|
||||||
|
utils.CheckErr(err)
|
||||||
|
return fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host), parsed.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckIsServiceIP(address string) bool {
|
||||||
|
if k8sResolver == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return k8sResolver.CheckIsServiceIP(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gives a rough estimate of the size this will take up in the db, good enough for maintaining db size limit accurately
|
||||||
|
func getEstimatedEntrySizeBytes(mizuEntry *tapApi.MizuEntry) int {
|
||||||
|
sizeBytes := len(mizuEntry.Entry)
|
||||||
|
sizeBytes += len(mizuEntry.EntryId)
|
||||||
|
sizeBytes += len(mizuEntry.Service)
|
||||||
|
sizeBytes += len(mizuEntry.Url)
|
||||||
|
sizeBytes += len(mizuEntry.Method)
|
||||||
|
sizeBytes += len(mizuEntry.RequestSenderIp)
|
||||||
|
sizeBytes += len(mizuEntry.ResolvedDestination)
|
||||||
|
sizeBytes += len(mizuEntry.ResolvedSource)
|
||||||
|
sizeBytes += 8 // Status bytes (sqlite integer is always 8 bytes)
|
||||||
|
sizeBytes += 8 // Timestamp bytes
|
||||||
|
sizeBytes += 8 // SizeBytes bytes
|
||||||
|
sizeBytes += 1 // IsOutgoing bytes
|
||||||
|
|
||||||
|
return sizeBytes
|
||||||
|
}
|
||||||
118
agent/pkg/api/socket_routes.go
Normal file
118
agent/pkg/api/socket_routes.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/romana/rlog"
|
||||||
|
"github.com/up9inc/mizu/shared/debounce"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventHandlers interface {
|
||||||
|
WebSocketConnect(socketId int, isTapper bool)
|
||||||
|
WebSocketDisconnect(socketId int, isTapper bool)
|
||||||
|
WebSocketMessage(socketId int, message []byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocketConnection struct {
|
||||||
|
connection *websocket.Conn
|
||||||
|
lock *sync.Mutex
|
||||||
|
eventHandlers EventHandlers
|
||||||
|
isTapper bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var websocketUpgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
}
|
||||||
|
|
||||||
|
var websocketIdsLock = sync.Mutex{}
|
||||||
|
var connectedWebsockets map[int]*SocketConnection
|
||||||
|
var connectedWebsocketIdCounter = 0
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
websocketUpgrader.CheckOrigin = func(r *http.Request) bool { return true } // like cors for web socket
|
||||||
|
connectedWebsockets = make(map[int]*SocketConnection, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers) {
|
||||||
|
app.GET("/ws", func(c *gin.Context) {
|
||||||
|
websocketHandler(c.Writer, c.Request, eventHandlers, false)
|
||||||
|
})
|
||||||
|
app.GET("/wsTapper", func(c *gin.Context) {
|
||||||
|
websocketHandler(c.Writer, c.Request, eventHandlers, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers EventHandlers, isTapper bool) {
|
||||||
|
conn, err := websocketUpgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Errorf("Failed to set websocket upgrade: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
websocketIdsLock.Lock()
|
||||||
|
|
||||||
|
connectedWebsocketIdCounter++
|
||||||
|
socketId := connectedWebsocketIdCounter
|
||||||
|
connectedWebsockets[socketId] = &SocketConnection{connection: conn, lock: &sync.Mutex{}, eventHandlers: eventHandlers, isTapper: isTapper}
|
||||||
|
|
||||||
|
websocketIdsLock.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
socketCleanup(socketId, connectedWebsockets[socketId])
|
||||||
|
}()
|
||||||
|
|
||||||
|
eventHandlers.WebSocketConnect(socketId, isTapper)
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, msg, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
rlog.Errorf("Error reading message, socket id: %d, error: %v", socketId, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
eventHandlers.WebSocketMessage(socketId, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func socketCleanup(socketId int, socketConnection *SocketConnection) {
|
||||||
|
err := socketConnection.connection.Close()
|
||||||
|
if err != nil {
|
||||||
|
rlog.Errorf("Error closing socket connection for socket id %d: %v\n", socketId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
websocketIdsLock.Lock()
|
||||||
|
connectedWebsockets[socketId] = nil
|
||||||
|
websocketIdsLock.Unlock()
|
||||||
|
|
||||||
|
socketConnection.eventHandlers.WebSocketDisconnect(socketId, socketConnection.isTapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
var db = debounce.NewDebouncer(time.Second*5, func() {
|
||||||
|
rlog.Error("Successfully sent to socket")
|
||||||
|
})
|
||||||
|
|
||||||
|
func SendToSocket(socketId int, message []byte) error {
|
||||||
|
socketObj := connectedWebsockets[socketId]
|
||||||
|
if socketObj == nil {
|
||||||
|
return errors.New("Socket is disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sent = false
|
||||||
|
time.AfterFunc(time.Second*5, func() {
|
||||||
|
if !sent {
|
||||||
|
rlog.Error("Socket timed out")
|
||||||
|
socketCleanup(socketId, socketObj)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socketObj.lock.Lock() // gorilla socket panics from concurrent writes to a single socket
|
||||||
|
err := socketObj.connection.WriteMessage(1, message)
|
||||||
|
socketObj.lock.Unlock()
|
||||||
|
|
||||||
|
sent = true
|
||||||
|
return err
|
||||||
|
}
|
||||||
134
agent/pkg/api/socket_server_handlers.go
Normal file
134
agent/pkg/api/socket_server_handlers.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mizuserver/pkg/models"
|
||||||
|
"mizuserver/pkg/providers"
|
||||||
|
"mizuserver/pkg/up9"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
|
||||||
|
"github.com/romana/rlog"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
var browserClientSocketUUIDs = make([]int, 0)
|
||||||
|
var socketListLock = sync.Mutex{}
|
||||||
|
|
||||||
|
type RoutesEventHandlers struct {
|
||||||
|
EventHandlers
|
||||||
|
SocketOutChannel chan<- *tapApi.OutputChannelItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go up9.UpdateAnalyzeStatus(BroadcastToBrowserClients)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) {
|
||||||
|
if isTapper {
|
||||||
|
rlog.Infof("Websocket event - Tapper connected, socket ID: %d", socketId)
|
||||||
|
providers.TapperAdded()
|
||||||
|
} else {
|
||||||
|
rlog.Infof("Websocket event - Browser socket connected, socket ID: %d", socketId)
|
||||||
|
socketListLock.Lock()
|
||||||
|
browserClientSocketUUIDs = append(browserClientSocketUUIDs, socketId)
|
||||||
|
socketListLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RoutesEventHandlers) WebSocketDisconnect(socketId int, isTapper bool) {
|
||||||
|
if isTapper {
|
||||||
|
rlog.Infof("Websocket event - Tapper disconnected, socket ID: %d", socketId)
|
||||||
|
providers.TapperRemoved()
|
||||||
|
} else {
|
||||||
|
rlog.Infof("Websocket event - Browser socket disconnected, socket ID: %d", socketId)
|
||||||
|
socketListLock.Lock()
|
||||||
|
removeSocketUUIDFromBrowserSlice(socketId)
|
||||||
|
socketListLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BroadcastToBrowserClients(message []byte) {
|
||||||
|
for _, socketId := range browserClientSocketUUIDs {
|
||||||
|
go func(socketId int) {
|
||||||
|
err := SendToSocket(socketId, message)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Errorf("error sending message to socket ID %d: %v", socketId, err)
|
||||||
|
}
|
||||||
|
}(socketId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
|
||||||
|
var socketMessageBase shared.WebSocketMessageMetadata
|
||||||
|
err := json.Unmarshal(message, &socketMessageBase)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Infof("Could not unmarshal websocket message %v\n", err)
|
||||||
|
} else {
|
||||||
|
switch socketMessageBase.MessageType {
|
||||||
|
case shared.WebSocketMessageTypeTappedEntry:
|
||||||
|
var tappedEntryMessage models.WebSocketTappedEntryMessage
|
||||||
|
err := json.Unmarshal(message, &tappedEntryMessage)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Infof("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
||||||
|
} else {
|
||||||
|
// NOTE: This is where the message comes back from the intermediate WebSocket to code.
|
||||||
|
h.SocketOutChannel <- tappedEntryMessage.Data
|
||||||
|
}
|
||||||
|
case shared.WebSocketMessageTypeUpdateStatus:
|
||||||
|
var statusMessage shared.WebSocketStatusMessage
|
||||||
|
err := json.Unmarshal(message, &statusMessage)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Infof("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
||||||
|
} else {
|
||||||
|
providers.TapStatus.Pods = statusMessage.TappingStatus.Pods
|
||||||
|
BroadcastToBrowserClients(message)
|
||||||
|
}
|
||||||
|
case shared.WebsocketMessageTypeOutboundLink:
|
||||||
|
var outboundLinkMessage models.WebsocketOutboundLinkMessage
|
||||||
|
err := json.Unmarshal(message, &outboundLinkMessage)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Infof("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
||||||
|
} else {
|
||||||
|
handleTLSLink(outboundLinkMessage)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
rlog.Infof("Received socket message of type %s for which no handlers are defined", socketMessageBase.MessageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTLSLink(outboundLinkMessage models.WebsocketOutboundLinkMessage) {
|
||||||
|
resolvedName := k8sResolver.Resolve(outboundLinkMessage.Data.DstIP)
|
||||||
|
if resolvedName != "" {
|
||||||
|
outboundLinkMessage.Data.DstIP = resolvedName
|
||||||
|
} else if outboundLinkMessage.Data.SuggestedResolvedName != "" {
|
||||||
|
outboundLinkMessage.Data.DstIP = outboundLinkMessage.Data.SuggestedResolvedName
|
||||||
|
}
|
||||||
|
cacheKey := fmt.Sprintf("%s -> %s:%d", outboundLinkMessage.Data.Src, outboundLinkMessage.Data.DstIP, outboundLinkMessage.Data.DstPort)
|
||||||
|
_, isInCache := providers.RecentTLSLinks.Get(cacheKey)
|
||||||
|
if isInCache {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
providers.RecentTLSLinks.SetDefault(cacheKey, outboundLinkMessage.Data)
|
||||||
|
}
|
||||||
|
marshaledMessage, err := json.Marshal(outboundLinkMessage)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Errorf("Error marshaling outbound link message for broadcasting: %v", err)
|
||||||
|
} else {
|
||||||
|
rlog.Errorf("Broadcasting outboundlink message %s", string(marshaledMessage))
|
||||||
|
BroadcastToBrowserClients(marshaledMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSocketUUIDFromBrowserSlice(uuidToRemove int) {
|
||||||
|
newUUIDSlice := make([]int, 0, len(browserClientSocketUUIDs))
|
||||||
|
for _, uuid := range browserClientSocketUUIDs {
|
||||||
|
if uuid != uuidToRemove {
|
||||||
|
newUUIDSlice = append(newUUIDSlice, uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
browserClientSocketUUIDs = newUUIDSlice
|
||||||
|
}
|
||||||
274
agent/pkg/controllers/entries_controller.go
Normal file
274
agent/pkg/controllers/entries_controller.go
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mizuserver/pkg/database"
|
||||||
|
"mizuserver/pkg/models"
|
||||||
|
"mizuserver/pkg/providers"
|
||||||
|
"mizuserver/pkg/up9"
|
||||||
|
"mizuserver/pkg/utils"
|
||||||
|
"mizuserver/pkg/validation"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/martian/har"
|
||||||
|
"github.com/romana/rlog"
|
||||||
|
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var extensionsMap map[string]*tapApi.Extension // global
|
||||||
|
|
||||||
|
func InitExtensionsMap(ref map[string]*tapApi.Extension) {
|
||||||
|
extensionsMap = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntries(c *gin.Context) {
|
||||||
|
entriesFilter := &models.EntriesFilter{}
|
||||||
|
|
||||||
|
if err := c.BindQuery(entriesFilter); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
err := validation.Validate(entriesFilter)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
order := database.OperatorToOrderMapping[entriesFilter.Operator]
|
||||||
|
operatorSymbol := database.OperatorToSymbolMapping[entriesFilter.Operator]
|
||||||
|
var entries []tapApi.MizuEntry
|
||||||
|
database.GetEntriesTable().
|
||||||
|
Order(fmt.Sprintf("timestamp %s", order)).
|
||||||
|
Where(fmt.Sprintf("timestamp %s %v", operatorSymbol, entriesFilter.Timestamp)).
|
||||||
|
Omit("entry"). // remove the "big" entry field
|
||||||
|
Limit(entriesFilter.Limit).
|
||||||
|
Find(&entries)
|
||||||
|
|
||||||
|
if len(entries) > 0 && order == database.OrderDesc {
|
||||||
|
// the entries always order from oldest to newest so we should revers
|
||||||
|
utils.ReverseSlice(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseEntries := make([]tapApi.BaseEntryDetails, 0)
|
||||||
|
for _, data := range entries {
|
||||||
|
harEntry := tapApi.BaseEntryDetails{}
|
||||||
|
if err := models.GetEntry(&data, &harEntry); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
baseEntries = append(baseEntries, harEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, baseEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHARs(c *gin.Context) {
|
||||||
|
entriesFilter := &models.HarFetchRequestQuery{}
|
||||||
|
order := database.OrderDesc
|
||||||
|
if err := c.BindQuery(entriesFilter); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
err := validation.Validate(entriesFilter)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var timestampFrom, timestampTo int64
|
||||||
|
|
||||||
|
if entriesFilter.From < 0 {
|
||||||
|
timestampFrom = 0
|
||||||
|
} else {
|
||||||
|
timestampFrom = entriesFilter.From
|
||||||
|
}
|
||||||
|
if entriesFilter.To <= 0 {
|
||||||
|
timestampTo = time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
} else {
|
||||||
|
timestampTo = entriesFilter.To
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []tapApi.MizuEntry
|
||||||
|
database.GetEntriesTable().
|
||||||
|
Where(fmt.Sprintf("timestamp BETWEEN %v AND %v", timestampFrom, timestampTo)).
|
||||||
|
Order(fmt.Sprintf("timestamp %s", order)).
|
||||||
|
Find(&entries)
|
||||||
|
|
||||||
|
if len(entries) > 0 {
|
||||||
|
// the entries always order from oldest to newest so we should revers
|
||||||
|
utils.ReverseSlice(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
harsObject := map[string]*models.ExtendedHAR{}
|
||||||
|
|
||||||
|
for _, entryData := range entries {
|
||||||
|
var harEntry har.Entry
|
||||||
|
_ = json.Unmarshal([]byte(entryData.Entry), &harEntry)
|
||||||
|
if entryData.ResolvedDestination != "" {
|
||||||
|
harEntry.Request.URL = utils.SetHostname(harEntry.Request.URL, entryData.ResolvedDestination)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName string
|
||||||
|
sourceOfEntry := entryData.ResolvedSource
|
||||||
|
if sourceOfEntry != "" {
|
||||||
|
// naively assumes the proper service source is http
|
||||||
|
sourceOfEntry = fmt.Sprintf("http://%s", sourceOfEntry)
|
||||||
|
//replace / from the file name cause they end up creating a corrupted folder
|
||||||
|
fileName = fmt.Sprintf("%s.har", strings.ReplaceAll(sourceOfEntry, "/", "_"))
|
||||||
|
} else {
|
||||||
|
fileName = "unknown_source.har"
|
||||||
|
}
|
||||||
|
if harOfSource, ok := harsObject[fileName]; ok {
|
||||||
|
harOfSource.Log.Entries = append(harOfSource.Log.Entries, &harEntry)
|
||||||
|
} else {
|
||||||
|
var entriesHar []*har.Entry
|
||||||
|
entriesHar = append(entriesHar, &harEntry)
|
||||||
|
harsObject[fileName] = &models.ExtendedHAR{
|
||||||
|
Log: &models.ExtendedLog{
|
||||||
|
Version: "1.2",
|
||||||
|
Creator: &models.ExtendedCreator{
|
||||||
|
Creator: &har.Creator{
|
||||||
|
Name: "mizu",
|
||||||
|
Version: "0.0.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Entries: entriesHar,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// leave undefined when no source is present, otherwise modeler assumes source is empty string ""
|
||||||
|
if sourceOfEntry != "" {
|
||||||
|
harsObject[fileName].Log.Creator.Source = &sourceOfEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retObj := map[string][]byte{}
|
||||||
|
for k, v := range harsObject {
|
||||||
|
bytesData, _ := json.Marshal(v)
|
||||||
|
retObj[k] = bytesData
|
||||||
|
}
|
||||||
|
buffer := utils.ZipData(retObj)
|
||||||
|
c.Data(http.StatusOK, "application/octet-stream", buffer.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadEntries(c *gin.Context) {
|
||||||
|
rlog.Infof("Upload entries - started\n")
|
||||||
|
|
||||||
|
uploadParams := &models.UploadEntriesRequestQuery{}
|
||||||
|
if err := c.BindQuery(uploadParams); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.Validate(uploadParams); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if up9.GetAnalyzeInfo().IsAnalyzing {
|
||||||
|
c.String(http.StatusBadRequest, "Cannot analyze, mizu is already analyzing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rlog.Infof("Upload entries - creating token. dest %s\n", uploadParams.Dest)
|
||||||
|
token, err := up9.CreateAnonymousToken(uploadParams.Dest)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusServiceUnavailable, "Cannot analyze, mizu is already analyzing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rlog.Infof("Upload entries - uploading. token: %s model: %s\n", token.Token, token.Model)
|
||||||
|
go up9.UploadEntriesImpl(token.Token, token.Model, uploadParams.Dest, uploadParams.SleepIntervalSec)
|
||||||
|
c.String(http.StatusOK, "OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFullEntries(c *gin.Context) {
|
||||||
|
entriesFilter := &models.HarFetchRequestQuery{}
|
||||||
|
if err := c.BindQuery(entriesFilter); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
err := validation.Validate(entriesFilter)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var timestampFrom, timestampTo int64
|
||||||
|
|
||||||
|
if entriesFilter.From < 0 {
|
||||||
|
timestampFrom = 0
|
||||||
|
} else {
|
||||||
|
timestampFrom = entriesFilter.From
|
||||||
|
}
|
||||||
|
if entriesFilter.To <= 0 {
|
||||||
|
timestampTo = time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
} else {
|
||||||
|
timestampTo = entriesFilter.To
|
||||||
|
}
|
||||||
|
|
||||||
|
entriesArray := database.GetEntriesFromDb(timestampFrom, timestampTo)
|
||||||
|
result := make([]models.FullEntryDetails, 0)
|
||||||
|
for _, data := range entriesArray {
|
||||||
|
harEntry := models.FullEntryDetails{}
|
||||||
|
if err := models.GetEntry(&data, &harEntry); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, harEntry)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntry(c *gin.Context) {
|
||||||
|
var entryData tapApi.MizuEntry
|
||||||
|
database.GetEntriesTable().
|
||||||
|
Where(map[string]string{"entryId": c.Param("entryId")}).
|
||||||
|
First(&entryData)
|
||||||
|
|
||||||
|
fullEntry := models.FullEntryDetails{}
|
||||||
|
if err := models.GetEntry(&entryData, &fullEntry); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||||
|
"error": true,
|
||||||
|
"msg": "Can't get entry details",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Fix the part below
|
||||||
|
// fullEntryWithPolicy := models.FullEntryWithPolicy{}
|
||||||
|
// if err := models.GetEntry(&entryData, &fullEntryWithPolicy); err != nil {
|
||||||
|
// c.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||||
|
// "error": true,
|
||||||
|
// "msg": "Can't get entry details",
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
extension := extensionsMap[entryData.ProtocolName]
|
||||||
|
protocol, representation, bodySize, _ := extension.Dissector.Represent(&entryData)
|
||||||
|
c.JSON(http.StatusOK, tapApi.MizuEntryWrapper{
|
||||||
|
Protocol: protocol,
|
||||||
|
Representation: string(representation),
|
||||||
|
BodySize: bodySize,
|
||||||
|
Data: entryData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteAllEntries(c *gin.Context) {
|
||||||
|
database.GetEntriesTable().
|
||||||
|
Where("1 = 1").
|
||||||
|
Delete(&tapApi.MizuEntry{})
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, map[string]string{
|
||||||
|
"msg": "Success",
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGeneralStats(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, providers.GetGeneralStats())
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTappingStatus(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, providers.TapStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalyzeInformation(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, up9.GetAnalyzeInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRecentTLSLinks(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, providers.GetAllRecentTLSAddresses())
|
||||||
|
}
|
||||||
13
agent/pkg/controllers/metadata_controller.go
Normal file
13
agent/pkg/controllers/metadata_controller.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"mizuserver/pkg/version"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetVersion(c *gin.Context) {
|
||||||
|
resp := shared.VersionResponse{SemVer: version.SemVer}
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
12
agent/pkg/controllers/resolving_controller.go
Normal file
12
agent/pkg/controllers/resolving_controller.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"mizuserver/pkg/holder"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCurrentResolvingInformation(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, holder.GetResolver().GetMap())
|
||||||
|
}
|
||||||
|
|
||||||
36
agent/pkg/controllers/status_controller.go
Normal file
36
agent/pkg/controllers/status_controller.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/romana/rlog"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"mizuserver/pkg/api"
|
||||||
|
"mizuserver/pkg/providers"
|
||||||
|
"mizuserver/pkg/validation"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostTappedPods(c *gin.Context) {
|
||||||
|
tapStatus := &shared.TapStatus{}
|
||||||
|
if err := c.Bind(tapStatus); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.Validate(tapStatus); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rlog.Infof("[Status] POST request: %d tapped pods", len(tapStatus.Pods))
|
||||||
|
providers.TapStatus.Pods = tapStatus.Pods
|
||||||
|
message := shared.CreateWebSocketStatusMessage(*tapStatus)
|
||||||
|
if jsonBytes, err := json.Marshal(message); err != nil {
|
||||||
|
rlog.Errorf("Could not Marshal message %v\n", err)
|
||||||
|
} else {
|
||||||
|
api.BroadcastToBrowserClients(jsonBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTappersCount(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, providers.TappersCount)
|
||||||
|
}
|
||||||
73
agent/pkg/database/main.go
Normal file
73
agent/pkg/database/main.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mizuserver/pkg/utils"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DBPath = "./entries.db"
|
||||||
|
OrderDesc = "desc"
|
||||||
|
OrderAsc = "asc"
|
||||||
|
LT = "lt"
|
||||||
|
GT = "gt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DB *gorm.DB
|
||||||
|
IsDBLocked = false
|
||||||
|
OperatorToSymbolMapping = map[string]string{
|
||||||
|
LT: "<",
|
||||||
|
GT: ">",
|
||||||
|
}
|
||||||
|
OperatorToOrderMapping = map[string]string{
|
||||||
|
LT: OrderDesc,
|
||||||
|
GT: OrderAsc,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DB = initDataBase(DBPath)
|
||||||
|
go StartEnforcingDatabaseSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntriesTable() *gorm.DB {
|
||||||
|
return DB.Table("mizu_entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateEntry(entry *tapApi.MizuEntry) {
|
||||||
|
if IsDBLocked {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
GetEntriesTable().Create(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDataBase(databasePath string) *gorm.DB {
|
||||||
|
temp, _ := gorm.Open(sqlite.Open(databasePath), &gorm.Config{
|
||||||
|
Logger: &utils.TruncatingLogger{LogLevel: logger.Warn, SlowThreshold: 500 * time.Millisecond},
|
||||||
|
})
|
||||||
|
_ = temp.AutoMigrate(&tapApi.MizuEntry{}) // this will ensure table is created
|
||||||
|
return temp
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntriesFromDb(timestampFrom int64, timestampTo int64) []tapApi.MizuEntry {
|
||||||
|
order := OrderDesc
|
||||||
|
var entries []tapApi.MizuEntry
|
||||||
|
GetEntriesTable().
|
||||||
|
Where(fmt.Sprintf("timestamp BETWEEN %v AND %v", timestampFrom, timestampTo)).
|
||||||
|
Order(fmt.Sprintf("timestamp %s", order)).
|
||||||
|
Find(&entries)
|
||||||
|
|
||||||
|
if len(entries) > 0 {
|
||||||
|
// the entries always order from oldest to newest so we should revers
|
||||||
|
utils.ReverseSlice(entries)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
122
agent/pkg/database/size_enforcer.go
Normal file
122
agent/pkg/database/size_enforcer.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/romana/rlog"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/debounce"
|
||||||
|
"github.com/up9inc/mizu/shared/units"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const percentageOfMaxSizeBytesToPrune = 15
|
||||||
|
const defaultMaxDatabaseSizeBytes int64 = 200 * 1000 * 1000
|
||||||
|
|
||||||
|
func StartEnforcingDatabaseSize() {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error creating filesystem watcher for db size enforcement: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxEntriesDBByteSize, err := getMaxEntriesDBByteSize()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error parsing max db size: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkFileSizeDebouncer := debounce.NewDebouncer(5*time.Second, func() {
|
||||||
|
checkFileSize(maxEntriesDBByteSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return // closed channel
|
||||||
|
}
|
||||||
|
if event.Op == fsnotify.Write {
|
||||||
|
checkFileSizeDebouncer.SetOn()
|
||||||
|
}
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return // closed channel
|
||||||
|
}
|
||||||
|
rlog.Errorf("filesystem watcher encountered error:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = watcher.Add(DBPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error adding %s to filesystem watcher for db size enforcement: %v\n", DBPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMaxEntriesDBByteSize() (int64, error) {
|
||||||
|
maxEntriesDBByteSize := defaultMaxDatabaseSizeBytes
|
||||||
|
var err error
|
||||||
|
|
||||||
|
maxEntriesDBSizeByteSEnvVarValue := os.Getenv(shared.MaxEntriesDBSizeBytesEnvVar)
|
||||||
|
if maxEntriesDBSizeByteSEnvVarValue != "" {
|
||||||
|
maxEntriesDBByteSize, err = strconv.ParseInt(maxEntriesDBSizeByteSEnvVarValue, 10, 64)
|
||||||
|
}
|
||||||
|
return maxEntriesDBByteSize, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFileSize(maxSizeBytes int64) {
|
||||||
|
fileStat, err := os.Stat(DBPath)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Errorf("Error checking %s file size: %v", DBPath, err)
|
||||||
|
} else {
|
||||||
|
if fileStat.Size() > maxSizeBytes {
|
||||||
|
pruneOldEntries(fileStat.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pruneOldEntries(currentFileSize int64) {
|
||||||
|
// sqlite locks the database while delete or VACUUM are running and sqlite is terrible at handling its own db lock while a lot of inserts are attempted, we prevent a significant bottleneck by handling the db lock ourselves here
|
||||||
|
IsDBLocked = true
|
||||||
|
defer func() { IsDBLocked = false }()
|
||||||
|
|
||||||
|
amountOfBytesToTrim := currentFileSize / (100 / percentageOfMaxSizeBytesToPrune)
|
||||||
|
|
||||||
|
rows, err := GetEntriesTable().Limit(10000).Order("id").Rows()
|
||||||
|
if err != nil {
|
||||||
|
rlog.Errorf("Error getting 10000 first db rows: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entryIdsToRemove := make([]uint, 0)
|
||||||
|
bytesToBeRemoved := int64(0)
|
||||||
|
for rows.Next() {
|
||||||
|
if bytesToBeRemoved >= amountOfBytesToTrim {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var entry tapApi.MizuEntry
|
||||||
|
err = DB.ScanRows(rows, &entry)
|
||||||
|
if err != nil {
|
||||||
|
rlog.Errorf("Error scanning db row: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
entryIdsToRemove = append(entryIdsToRemove, entry.ID)
|
||||||
|
bytesToBeRemoved += int64(entry.EstimatedSizeBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entryIdsToRemove) > 0 {
|
||||||
|
GetEntriesTable().Where(entryIdsToRemove).Delete(tapApi.MizuEntry{})
|
||||||
|
// VACUUM causes sqlite to shrink the db file after rows have been deleted, the db file will not shrink without this
|
||||||
|
DB.Exec("VACUUM")
|
||||||
|
rlog.Errorf("Removed %d rows and cleared %s", len(entryIdsToRemove), units.BytesToHumanReadable(bytesToBeRemoved))
|
||||||
|
} else {
|
||||||
|
rlog.Error("Found no rows to remove when pruning")
|
||||||
|
}
|
||||||
|
}
|
||||||
14
agent/pkg/holder/main.go
Normal file
14
agent/pkg/holder/main.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package holder
|
||||||
|
|
||||||
|
import "mizuserver/pkg/resolver"
|
||||||
|
|
||||||
|
var k8sResolver *resolver.Resolver
|
||||||
|
|
||||||
|
func SetResolver(param *resolver.Resolver) {
|
||||||
|
k8sResolver = param
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetResolver() *resolver.Resolver {
|
||||||
|
return k8sResolver
|
||||||
|
}
|
||||||
|
|
||||||
165
agent/pkg/models/models.go
Normal file
165
agent/pkg/models/models.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
|
||||||
|
"mizuserver/pkg/rules"
|
||||||
|
"mizuserver/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/google/martian/har"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/tap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetEntry(r *tapApi.MizuEntry, v tapApi.DataUnmarshaler) error {
|
||||||
|
return v.UnmarshalData(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApplicableRules(status bool, latency int64, number int) tapApi.ApplicableRules {
|
||||||
|
ar := tapApi.ApplicableRules{}
|
||||||
|
ar.Status = status
|
||||||
|
ar.Latency = latency
|
||||||
|
ar.NumberOfRules = number
|
||||||
|
return ar
|
||||||
|
}
|
||||||
|
|
||||||
|
type FullEntryDetails struct {
|
||||||
|
har.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
type FullEntryDetailsExtra struct {
|
||||||
|
har.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fed *FullEntryDetails) UnmarshalData(entry *tapApi.MizuEntry) error {
|
||||||
|
if err := json.Unmarshal([]byte(entry.Entry), &fed.Entry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.ResolvedDestination != "" {
|
||||||
|
fed.Entry.Request.URL = utils.SetHostname(fed.Entry.Request.URL, entry.ResolvedDestination)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fedex *FullEntryDetailsExtra) UnmarshalData(entry *tapApi.MizuEntry) error {
|
||||||
|
if err := json.Unmarshal([]byte(entry.Entry), &fedex.Entry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.ResolvedSource != "" {
|
||||||
|
fedex.Entry.Request.Headers = append(fedex.Request.Headers, har.Header{Name: "x-mizu-source", Value: entry.ResolvedSource})
|
||||||
|
}
|
||||||
|
if entry.ResolvedDestination != "" {
|
||||||
|
fedex.Entry.Request.Headers = append(fedex.Request.Headers, har.Header{Name: "x-mizu-destination", Value: entry.ResolvedDestination})
|
||||||
|
fedex.Entry.Request.URL = utils.SetHostname(fedex.Entry.Request.URL, entry.ResolvedDestination)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type EntriesFilter struct {
|
||||||
|
Limit int `form:"limit" validate:"required,min=1,max=200"`
|
||||||
|
Operator string `form:"operator" validate:"required,oneof='lt' 'gt'"`
|
||||||
|
Timestamp int64 `form:"timestamp" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadEntriesRequestQuery struct {
|
||||||
|
Dest string `form:"dest"`
|
||||||
|
SleepIntervalSec int `form:"interval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HarFetchRequestQuery struct {
|
||||||
|
From int64 `form:"from"`
|
||||||
|
To int64 `form:"to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebSocketEntryMessage struct {
|
||||||
|
*shared.WebSocketMessageMetadata
|
||||||
|
Data *tapApi.BaseEntryDetails `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebSocketTappedEntryMessage struct {
|
||||||
|
*shared.WebSocketMessageMetadata
|
||||||
|
Data *tapApi.OutputChannelItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsocketOutboundLinkMessage struct {
|
||||||
|
*shared.WebSocketMessageMetadata
|
||||||
|
Data *tap.OutboundLink
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateBaseEntryWebSocketMessage(base *tapApi.BaseEntryDetails) ([]byte, error) {
|
||||||
|
message := &WebSocketEntryMessage{
|
||||||
|
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
||||||
|
MessageType: shared.WebSocketMessageTypeEntry,
|
||||||
|
},
|
||||||
|
Data: base,
|
||||||
|
}
|
||||||
|
return json.Marshal(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateWebsocketTappedEntryMessage(base *tapApi.OutputChannelItem) ([]byte, error) {
|
||||||
|
message := &WebSocketTappedEntryMessage{
|
||||||
|
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
||||||
|
MessageType: shared.WebSocketMessageTypeTappedEntry,
|
||||||
|
},
|
||||||
|
Data: base,
|
||||||
|
}
|
||||||
|
return json.Marshal(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateWebsocketOutboundLinkMessage(base *tap.OutboundLink) ([]byte, error) {
|
||||||
|
message := &WebsocketOutboundLinkMessage{
|
||||||
|
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
||||||
|
MessageType: shared.WebsocketMessageTypeOutboundLink,
|
||||||
|
},
|
||||||
|
Data: base,
|
||||||
|
}
|
||||||
|
return json.Marshal(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedHAR is the top level object of a HAR log.
|
||||||
|
type ExtendedHAR struct {
|
||||||
|
Log *ExtendedLog `json:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedLog is the HAR HTTP request and response log.
|
||||||
|
type ExtendedLog struct {
|
||||||
|
// Version number of the HAR format.
|
||||||
|
Version string `json:"version"`
|
||||||
|
// Creator holds information about the log creator application.
|
||||||
|
Creator *ExtendedCreator `json:"creator"`
|
||||||
|
// Entries is a list containing requests and responses.
|
||||||
|
Entries []*har.Entry `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtendedCreator struct {
|
||||||
|
*har.Creator
|
||||||
|
Source *string `json:"_source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FullEntryWithPolicy struct {
|
||||||
|
RulesMatched []rules.RulesMatched `json:"rulesMatched,omitempty"`
|
||||||
|
Entry har.Entry `json:"entry"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fewp *FullEntryWithPolicy) UnmarshalData(entry *tapApi.MizuEntry) error {
|
||||||
|
if err := json.Unmarshal([]byte(entry.Entry), &fewp.Entry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, resultPolicyToSend := rules.MatchRequestPolicy(fewp.Entry, entry.Service)
|
||||||
|
fewp.RulesMatched = resultPolicyToSend
|
||||||
|
fewp.Service = entry.Service
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunValidationRulesState(harEntry har.Entry, service string) tapApi.ApplicableRules {
|
||||||
|
numberOfRules, resultPolicyToSend := rules.MatchRequestPolicy(harEntry, service)
|
||||||
|
statusPolicyToSend, latency, numberOfRules := rules.PassedValidationRules(resultPolicyToSend, numberOfRules)
|
||||||
|
ar := NewApplicableRules(statusPolicyToSend, latency, numberOfRules)
|
||||||
|
return ar
|
||||||
|
}
|
||||||
36
agent/pkg/providers/stats_provider.go
Normal file
36
agent/pkg/providers/stats_provider.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeneralStats struct {
|
||||||
|
EntriesCount int
|
||||||
|
FirstEntryTimestamp int
|
||||||
|
LastEntryTimestamp int
|
||||||
|
}
|
||||||
|
|
||||||
|
var generalStats = GeneralStats{}
|
||||||
|
|
||||||
|
func ResetGeneralStats() {
|
||||||
|
generalStats = GeneralStats{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGeneralStats() GeneralStats {
|
||||||
|
return generalStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func EntryAdded() {
|
||||||
|
generalStats.EntriesCount++
|
||||||
|
|
||||||
|
currentTimestamp := int(time.Now().Unix())
|
||||||
|
|
||||||
|
if reflect.Value.IsZero(reflect.ValueOf(generalStats.FirstEntryTimestamp)) {
|
||||||
|
generalStats.FirstEntryTimestamp = currentTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
generalStats.LastEntryTimestamp = currentTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
35
agent/pkg/providers/stats_provider_test.go
Normal file
35
agent/pkg/providers/stats_provider_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package providers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mizuserver/pkg/providers"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNoEntryAddedCount(t *testing.T) {
|
||||||
|
entriesStats := providers.GetGeneralStats()
|
||||||
|
|
||||||
|
if entriesStats.EntriesCount != 0 {
|
||||||
|
t.Errorf("unexpected result - expected: %v, actual: %v", 0, entriesStats.EntriesCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryAddedCount(t *testing.T) {
|
||||||
|
tests := []int{1, 5, 10, 100, 500, 1000}
|
||||||
|
|
||||||
|
for _, entriesCount := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", entriesCount), func(t *testing.T) {
|
||||||
|
for i := 0; i < entriesCount; i++ {
|
||||||
|
providers.EntryAdded()
|
||||||
|
}
|
||||||
|
|
||||||
|
entriesStats := providers.GetGeneralStats()
|
||||||
|
|
||||||
|
if entriesStats.EntriesCount != entriesCount {
|
||||||
|
t.Errorf("unexpected result - expected: %v, actual: %v", entriesCount, entriesStats.EntriesCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(providers.ResetGeneralStats)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
44
agent/pkg/providers/status_provider.go
Normal file
44
agent/pkg/providers/status_provider.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/tap"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tlsLinkRetainmentTime = time.Minute * 15
|
||||||
|
|
||||||
|
var (
|
||||||
|
TappersCount int
|
||||||
|
TapStatus shared.TapStatus
|
||||||
|
RecentTLSLinks = cache.New(tlsLinkRetainmentTime, tlsLinkRetainmentTime)
|
||||||
|
|
||||||
|
tappersCountLock = sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAllRecentTLSAddresses() []string {
|
||||||
|
recentTLSLinks := make([]string, 0)
|
||||||
|
|
||||||
|
for _, outboundLinkItem := range RecentTLSLinks.Items() {
|
||||||
|
outboundLink, castOk := outboundLinkItem.Object.(*tap.OutboundLink)
|
||||||
|
if castOk {
|
||||||
|
recentTLSLinks = append(recentTLSLinks, outboundLink.DstIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recentTLSLinks
|
||||||
|
}
|
||||||
|
|
||||||
|
func TapperAdded() {
|
||||||
|
tappersCountLock.Lock()
|
||||||
|
TappersCount++
|
||||||
|
tappersCountLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TapperRemoved() {
|
||||||
|
tappersCountLock.Lock()
|
||||||
|
TappersCount--
|
||||||
|
tappersCountLock.Unlock()
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ Now you will be able to import `github.com/up9inc/mizu/resolver` in any `.go` fi
|
|||||||
errOut := make(chan error, 100)
|
errOut := make(chan error, 100)
|
||||||
k8sResolver, err := resolver.NewFromOutOfCluster("", errOut)
|
k8sResolver, err := resolver.NewFromOutOfCluster("", errOut)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error creating k8s resolver %s", err)
|
rlog.Errorf("error creating k8s resolver %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -40,15 +40,15 @@ k8sResolver.Start(ctx)
|
|||||||
|
|
||||||
resolvedName := k8sResolver.Resolve("10.107.251.91") // will always return `nil` in real scenarios as the internal map takes a moment to populate after `Start` is called
|
resolvedName := k8sResolver.Resolve("10.107.251.91") // will always return `nil` in real scenarios as the internal map takes a moment to populate after `Start` is called
|
||||||
if resolvedName != nil {
|
if resolvedName != nil {
|
||||||
fmt.Printf("resolved 10.107.251.91=%s", *resolvedName)
|
rlog.Errorf("resolved 10.107.251.91=%s", *resolvedName)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Could not find a resolved name for 10.107.251.91")
|
rlog.Error("Could not find a resolved name for 10.107.251.91")
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case err := <- errOut:
|
case err := <- errOut:
|
||||||
fmt.Printf("name resolving error %s", err)
|
rlog.Errorf("name resolving error %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
23
agent/pkg/resolver/loader.go
Normal file
23
agent/pkg/resolver/loader.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
cmap "github.com/orcaman/concurrent-map"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/openstack"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFromInCluster(errOut chan error, namesapce string) (*Resolver, error) {
|
||||||
|
config, err := restclient.InClusterConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clientset, err := kubernetes.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Resolver{clientConfig: config, clientSet: clientset, nameMap: cmap.New(), serviceMap: cmap.New(), errOut: errOut, namespace: namesapce}, nil
|
||||||
|
}
|
||||||
193
agent/pkg/resolver/resolver.go
Normal file
193
agent/pkg/resolver/resolver.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/romana/rlog"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
||||||
|
cmap "github.com/orcaman/concurrent-map"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
kubClientNullString = "None"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resolver struct {
|
||||||
|
clientConfig *restclient.Config
|
||||||
|
clientSet *kubernetes.Clientset
|
||||||
|
nameMap cmap.ConcurrentMap
|
||||||
|
serviceMap cmap.ConcurrentMap
|
||||||
|
isStarted bool
|
||||||
|
errOut chan error
|
||||||
|
namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) Start(ctx context.Context) {
|
||||||
|
if !resolver.isStarted {
|
||||||
|
resolver.isStarted = true
|
||||||
|
|
||||||
|
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchServices)
|
||||||
|
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchEndpoints)
|
||||||
|
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchPods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) Resolve(name string) string {
|
||||||
|
resolvedName, isFound := resolver.nameMap.Get(name)
|
||||||
|
if !isFound {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return resolvedName.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) GetMap() cmap.ConcurrentMap {
|
||||||
|
return resolver.nameMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) CheckIsServiceIP(address string) bool {
|
||||||
|
_, isFound := resolver.serviceMap.Get(address)
|
||||||
|
return isFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) watchPods(ctx context.Context) error {
|
||||||
|
// empty namespace makes the client watch all namespaces
|
||||||
|
watcher, err := resolver.clientSet.CoreV1().Pods(resolver.namespace).Watch(ctx, metav1.ListOptions{Watch: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.ResultChan():
|
||||||
|
if event.Object == nil {
|
||||||
|
return errors.New("error in kubectl pod watch")
|
||||||
|
}
|
||||||
|
if event.Type == watch.Deleted {
|
||||||
|
pod := event.Object.(*corev1.Pod)
|
||||||
|
resolver.saveResolvedName(pod.Status.PodIP, "", event.Type)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
watcher.Stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) watchEndpoints(ctx context.Context) error {
|
||||||
|
// empty namespace makes the client watch all namespaces
|
||||||
|
watcher, err := resolver.clientSet.CoreV1().Endpoints(resolver.namespace).Watch(ctx, metav1.ListOptions{Watch: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.ResultChan():
|
||||||
|
if event.Object == nil {
|
||||||
|
return errors.New("error in kubectl endpoint watch")
|
||||||
|
}
|
||||||
|
endpoint := event.Object.(*corev1.Endpoints)
|
||||||
|
serviceHostname := fmt.Sprintf("%s.%s", endpoint.Name, endpoint.Namespace)
|
||||||
|
if endpoint.Subsets != nil {
|
||||||
|
for _, subset := range endpoint.Subsets {
|
||||||
|
var ports []int32
|
||||||
|
if subset.Ports != nil {
|
||||||
|
for _, portMapping := range subset.Ports {
|
||||||
|
if portMapping.Port > 0 {
|
||||||
|
ports = append(ports, portMapping.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subset.Addresses != nil {
|
||||||
|
for _, address := range subset.Addresses {
|
||||||
|
resolver.saveResolvedName(address.IP, serviceHostname, event.Type)
|
||||||
|
for _, port := range ports {
|
||||||
|
ipWithPort := fmt.Sprintf("%s:%d", address.IP, port)
|
||||||
|
resolver.saveResolvedName(ipWithPort, serviceHostname, event.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
watcher.Stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) watchServices(ctx context.Context) error {
|
||||||
|
// empty namespace makes the client watch all namespaces
|
||||||
|
watcher, err := resolver.clientSet.CoreV1().Services(resolver.namespace).Watch(ctx, metav1.ListOptions{Watch: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.ResultChan():
|
||||||
|
if event.Object == nil {
|
||||||
|
return errors.New("error in kubectl service watch")
|
||||||
|
}
|
||||||
|
|
||||||
|
service := event.Object.(*corev1.Service)
|
||||||
|
serviceHostname := fmt.Sprintf("%s.%s", service.Name, service.Namespace)
|
||||||
|
if service.Spec.ClusterIP != "" && service.Spec.ClusterIP != kubClientNullString {
|
||||||
|
resolver.saveResolvedName(service.Spec.ClusterIP, serviceHostname, event.Type)
|
||||||
|
resolver.saveServiceIP(service.Spec.ClusterIP, serviceHostname, event.Type)
|
||||||
|
}
|
||||||
|
if service.Status.LoadBalancer.Ingress != nil {
|
||||||
|
for _, ingress := range service.Status.LoadBalancer.Ingress {
|
||||||
|
resolver.saveResolvedName(ingress.IP, serviceHostname, event.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
watcher.Stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) saveResolvedName(key string, resolved string, eventType watch.EventType) {
|
||||||
|
if eventType == watch.Deleted {
|
||||||
|
resolver.nameMap.Remove(key)
|
||||||
|
rlog.Infof("setting %s=nil\n", key)
|
||||||
|
} else {
|
||||||
|
resolver.nameMap.Set(key, resolved)
|
||||||
|
rlog.Infof("setting %s=%s\n", key, resolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) saveServiceIP(key string, resolved string, eventType watch.EventType) {
|
||||||
|
if eventType == watch.Deleted {
|
||||||
|
resolver.serviceMap.Remove(key)
|
||||||
|
} else {
|
||||||
|
resolver.serviceMap.Set(key, resolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) infiniteErrorHandleRetryFunc(ctx context.Context, fun func(ctx context.Context) error) {
|
||||||
|
for {
|
||||||
|
err := fun(ctx)
|
||||||
|
if err != nil {
|
||||||
|
resolver.errOut <- err
|
||||||
|
|
||||||
|
var statusError *k8serrors.StatusError
|
||||||
|
if errors.As(err, &statusError) {
|
||||||
|
if statusError.ErrStatus.Reason == metav1.StatusReasonForbidden {
|
||||||
|
rlog.Infof("Resolver loop encountered permission error, aborting event listening - %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil { // context was cancelled or errored
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
agent/pkg/routes/entries_routes.go
Normal file
26
agent/pkg/routes/entries_routes.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"mizuserver/pkg/controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EntriesRoutes defines the group of har entries routes.
|
||||||
|
func EntriesRoutes(ginApp *gin.Engine) {
|
||||||
|
routeGroup := ginApp.Group("/api")
|
||||||
|
|
||||||
|
routeGroup.GET("/entries", controllers.GetEntries) // get entries (base/thin entries)
|
||||||
|
routeGroup.GET("/entries/:entryId", controllers.GetEntry) // get single (full) entry
|
||||||
|
routeGroup.GET("/exportEntries", controllers.GetFullEntries)
|
||||||
|
routeGroup.GET("/uploadEntries", controllers.UploadEntries)
|
||||||
|
routeGroup.GET("/resolving", controllers.GetCurrentResolvingInformation)
|
||||||
|
|
||||||
|
routeGroup.GET("/har", controllers.GetHARs)
|
||||||
|
|
||||||
|
routeGroup.GET("/resetDB", controllers.DeleteAllEntries) // get single (full) entry
|
||||||
|
routeGroup.GET("/generalStats", controllers.GetGeneralStats) // get general stats about entries in DB
|
||||||
|
|
||||||
|
routeGroup.GET("/tapStatus", controllers.GetTappingStatus) // get tapping status
|
||||||
|
routeGroup.GET("/analyzeStatus", controllers.AnalyzeInformation)
|
||||||
|
routeGroup.GET("/recentTLSLinks", controllers.GetRecentTLSLinks)
|
||||||
|
}
|
||||||
13
agent/pkg/routes/metadata_routes.go
Normal file
13
agent/pkg/routes/metadata_routes.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"mizuserver/pkg/controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetadataRoutes defines the group of metadata routes.
|
||||||
|
func MetadataRoutes(app *gin.Engine) {
|
||||||
|
routeGroup := app.Group("/metadata")
|
||||||
|
|
||||||
|
routeGroup.GET("/version", controllers.GetVersion)
|
||||||
|
}
|
||||||
18
agent/pkg/routes/not_found_route.go
Normal file
18
agent/pkg/routes/not_found_route.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotFoundRoute defines the 404 Error route.
|
||||||
|
func NotFoundRoute(app *gin.Engine) {
|
||||||
|
app.Use(
|
||||||
|
func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusNotFound, map[string]interface{}{
|
||||||
|
"error": true,
|
||||||
|
"msg": "sorry, endpoint is not found",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
14
agent/pkg/routes/status_routes.go
Normal file
14
agent/pkg/routes/status_routes.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"mizuserver/pkg/controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StatusRoutes(ginApp *gin.Engine) {
|
||||||
|
routeGroup := ginApp.Group("/status")
|
||||||
|
|
||||||
|
routeGroup.POST("/tappedPods", controllers.PostTappedPods)
|
||||||
|
|
||||||
|
routeGroup.GET("/tappersCount", controllers.GetTappersCount)
|
||||||
|
}
|
||||||
110
agent/pkg/rules/models.go
Normal file
110
agent/pkg/rules/models.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/martian/har"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
jsonpath "github.com/yalp/jsonpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RulesMatched struct {
|
||||||
|
Matched bool `json:"matched"`
|
||||||
|
Rule shared.RulePolicy `json:"rule"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendRulesMatched(rulesMatched []RulesMatched, matched bool, rule shared.RulePolicy) []RulesMatched {
|
||||||
|
return append(rulesMatched, RulesMatched{Matched: matched, Rule: rule})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidatePath(URLFromRule string, URL string) bool {
|
||||||
|
if URLFromRule != "" {
|
||||||
|
matchPath, err := regexp.MatchString(URLFromRule, URL)
|
||||||
|
if err != nil || !matchPath {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateService(serviceFromRule string, service string) bool {
|
||||||
|
if serviceFromRule != "" {
|
||||||
|
matchService, err := regexp.MatchString(serviceFromRule, service)
|
||||||
|
if err != nil || !matchService {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchRequestPolicy(harEntry har.Entry, service string) (int, []RulesMatched) {
|
||||||
|
enforcePolicy, _ := shared.DecodeEnforcePolicy(fmt.Sprintf("%s/%s", shared.RulePolicyPath, shared.RulePolicyFileName))
|
||||||
|
var resultPolicyToSend []RulesMatched
|
||||||
|
for _, rule := range enforcePolicy.Rules {
|
||||||
|
if !ValidatePath(rule.Path, harEntry.Request.URL) || !ValidateService(rule.Service, service) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rule.Type == "json" {
|
||||||
|
var bodyJsonMap interface{}
|
||||||
|
if err := json.Unmarshal(harEntry.Response.Content.Text, &bodyJsonMap); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out, err := jsonpath.Read(bodyJsonMap, rule.Key)
|
||||||
|
if err != nil || out == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var matchValue bool
|
||||||
|
if reflect.TypeOf(out).Kind() == reflect.String {
|
||||||
|
matchValue, err = regexp.MatchString(rule.Value, out.(string))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val := fmt.Sprint(out)
|
||||||
|
matchValue, err = regexp.MatchString(rule.Value, val)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultPolicyToSend = appendRulesMatched(resultPolicyToSend, matchValue, rule)
|
||||||
|
} else if rule.Type == "header" {
|
||||||
|
for j := range harEntry.Response.Headers {
|
||||||
|
matchKey, err := regexp.MatchString(rule.Key, harEntry.Response.Headers[j].Name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matchKey {
|
||||||
|
matchValue, err := regexp.MatchString(rule.Value, harEntry.Response.Headers[j].Value)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resultPolicyToSend = appendRulesMatched(resultPolicyToSend, matchValue, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultPolicyToSend = appendRulesMatched(resultPolicyToSend, true, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(enforcePolicy.Rules), resultPolicyToSend
|
||||||
|
}
|
||||||
|
|
||||||
|
func PassedValidationRules(rulesMatched []RulesMatched, numberOfRules int) (bool, int64, int) {
|
||||||
|
if len(rulesMatched) == 0 {
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
for _, rule := range rulesMatched {
|
||||||
|
if rule.Matched == false {
|
||||||
|
return false, -1, len(rulesMatched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, rule := range rulesMatched {
|
||||||
|
if strings.ToLower(rule.Rule.Type) == "latency" {
|
||||||
|
return true, rule.Rule.Latency, len(rulesMatched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, -1, len(rulesMatched)
|
||||||
|
}
|
||||||
201
agent/pkg/up9/main.go
Normal file
201
agent/pkg/up9/main.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package up9
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/romana/rlog"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"mizuserver/pkg/database"
|
||||||
|
"mizuserver/pkg/models"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AnalyzeCheckSleepTime = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type GuestToken struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelStatus struct {
|
||||||
|
LastMajorGeneration float64 `json:"lastMajorGeneration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGuestToken(url string, target *GuestToken) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
rlog.Infof("Got token from the server, starting to json decode... status code: %v", resp.StatusCode)
|
||||||
|
return json.NewDecoder(resp.Body).Decode(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateAnonymousToken(envPrefix string) (*GuestToken, error) {
|
||||||
|
tokenUrl := fmt.Sprintf("https://trcc.%s/anonymous/token", envPrefix)
|
||||||
|
if strings.HasPrefix(envPrefix, "http") {
|
||||||
|
tokenUrl = fmt.Sprintf("%s/api/token", envPrefix)
|
||||||
|
}
|
||||||
|
token := &GuestToken{}
|
||||||
|
if err := getGuestToken(tokenUrl, token); err != nil {
|
||||||
|
rlog.Infof("Failed to get token, %s", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRemoteUrl(analyzeDestination string, analyzeToken string) string {
|
||||||
|
return fmt.Sprintf("https://%s/share/%s", analyzeDestination, analyzeToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckIfModelReady(analyzeDestination string, analyzeModel string, analyzeToken string) bool {
|
||||||
|
statusUrl, _ := url.Parse(fmt.Sprintf("https://trcc.%s/models/%s/status", analyzeDestination, analyzeModel))
|
||||||
|
req := &http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: statusUrl,
|
||||||
|
Header: map[string][]string{
|
||||||
|
"Content-Type": {"application/json"},
|
||||||
|
"Guest-Auth": {analyzeToken},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
statusResp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer statusResp.Body.Close()
|
||||||
|
|
||||||
|
target := &ModelStatus{}
|
||||||
|
_ = json.NewDecoder(statusResp.Body).Decode(&target)
|
||||||
|
|
||||||
|
return target.LastMajorGeneration > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTrafficDumpUrl(analyzeDestination string, analyzeModel string) *url.URL {
|
||||||
|
strUrl := fmt.Sprintf("https://traffic.%s/dumpTrafficBulk/%s", analyzeDestination, analyzeModel)
|
||||||
|
if strings.HasPrefix(analyzeDestination, "http") {
|
||||||
|
strUrl = fmt.Sprintf("%s/api/workspace/dumpTrafficBulk", analyzeDestination)
|
||||||
|
}
|
||||||
|
postUrl, _ := url.Parse(strUrl)
|
||||||
|
return postUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyzeInformation struct {
|
||||||
|
IsAnalyzing bool
|
||||||
|
SentCount int
|
||||||
|
AnalyzedModel string
|
||||||
|
AnalyzeToken string
|
||||||
|
AnalyzeDestination string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info *AnalyzeInformation) Reset() {
|
||||||
|
info.IsAnalyzing = false
|
||||||
|
info.AnalyzedModel = ""
|
||||||
|
info.AnalyzeToken = ""
|
||||||
|
info.AnalyzeDestination = ""
|
||||||
|
info.SentCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var analyzeInformation = &AnalyzeInformation{}
|
||||||
|
|
||||||
|
func GetAnalyzeInfo() *shared.AnalyzeStatus {
|
||||||
|
return &shared.AnalyzeStatus{
|
||||||
|
IsAnalyzing: analyzeInformation.IsAnalyzing,
|
||||||
|
RemoteUrl: GetRemoteUrl(analyzeInformation.AnalyzeDestination, analyzeInformation.AnalyzeToken),
|
||||||
|
IsRemoteReady: CheckIfModelReady(analyzeInformation.AnalyzeDestination, analyzeInformation.AnalyzedModel, analyzeInformation.AnalyzeToken),
|
||||||
|
SentCount: analyzeInformation.SentCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadEntriesImpl(token string, model string, envPrefix string, sleepIntervalSec int) {
|
||||||
|
analyzeInformation.IsAnalyzing = true
|
||||||
|
analyzeInformation.AnalyzedModel = model
|
||||||
|
analyzeInformation.AnalyzeToken = token
|
||||||
|
analyzeInformation.AnalyzeDestination = envPrefix
|
||||||
|
analyzeInformation.SentCount = 0
|
||||||
|
|
||||||
|
sleepTime := time.Second * time.Duration(sleepIntervalSec)
|
||||||
|
|
||||||
|
var timestampFrom int64 = 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
timestampTo := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
rlog.Infof("Getting entries from %v, to %v\n", timestampFrom, timestampTo)
|
||||||
|
entriesArray := database.GetEntriesFromDb(timestampFrom, timestampTo)
|
||||||
|
|
||||||
|
if len(entriesArray) > 0 {
|
||||||
|
|
||||||
|
fullEntriesExtra := make([]models.FullEntryDetailsExtra, 0)
|
||||||
|
for _, data := range entriesArray {
|
||||||
|
harEntry := models.FullEntryDetailsExtra{}
|
||||||
|
if err := models.GetEntry(&data, &harEntry); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fullEntriesExtra = append(fullEntriesExtra, harEntry)
|
||||||
|
}
|
||||||
|
rlog.Infof("About to upload %v entries\n", len(fullEntriesExtra))
|
||||||
|
|
||||||
|
body, jMarshalErr := json.Marshal(fullEntriesExtra)
|
||||||
|
if jMarshalErr != nil {
|
||||||
|
analyzeInformation.Reset()
|
||||||
|
rlog.Infof("Stopping analyzing")
|
||||||
|
log.Fatal(jMarshalErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var in bytes.Buffer
|
||||||
|
w := zlib.NewWriter(&in)
|
||||||
|
_, _ = w.Write(body)
|
||||||
|
_ = w.Close()
|
||||||
|
reqBody := ioutil.NopCloser(bytes.NewReader(in.Bytes()))
|
||||||
|
|
||||||
|
req := &http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: GetTrafficDumpUrl(envPrefix, model),
|
||||||
|
Header: map[string][]string{
|
||||||
|
"Content-Encoding": {"deflate"},
|
||||||
|
"Content-Type": {"application/octet-stream"},
|
||||||
|
"Guest-Auth": {token},
|
||||||
|
},
|
||||||
|
Body: reqBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, postErr := http.DefaultClient.Do(req); postErr != nil {
|
||||||
|
analyzeInformation.Reset()
|
||||||
|
rlog.Info("Stopping analyzing")
|
||||||
|
log.Fatal(postErr)
|
||||||
|
}
|
||||||
|
analyzeInformation.SentCount += len(entriesArray)
|
||||||
|
rlog.Infof("Finish uploading %v entries to %s\n", len(entriesArray), GetTrafficDumpUrl(envPrefix, model))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
rlog.Infof("Nothing to upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
rlog.Infof("Sleeping for %v...\n", sleepTime)
|
||||||
|
time.Sleep(sleepTime)
|
||||||
|
timestampFrom = timestampTo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateAnalyzeStatus(callback func(data []byte)) {
|
||||||
|
for {
|
||||||
|
if !analyzeInformation.IsAnalyzing {
|
||||||
|
time.Sleep(AnalyzeCheckSleepTime)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
analyzeStatus := GetAnalyzeInfo()
|
||||||
|
socketMessage := shared.CreateWebSocketMessageTypeAnalyzeStatus(*analyzeStatus)
|
||||||
|
|
||||||
|
jsonMessage, _ := json.Marshal(socketMessage)
|
||||||
|
callback(jsonMessage)
|
||||||
|
time.Sleep(AnalyzeCheckSleepTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
agent/pkg/utils/truncating_logger.go
Normal file
59
agent/pkg/utils/truncating_logger.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/romana/rlog"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
"gorm.io/gorm/utils"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TruncatingLogger implements the gorm logger.Interface interface. Its purpose is to act as gorm's logger while truncating logs to a max of 50 characters to minimise the performance impact
|
||||||
|
type TruncatingLogger struct {
|
||||||
|
LogLevel logger.LogLevel
|
||||||
|
SlowThreshold time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (truncatingLogger *TruncatingLogger) LogMode(logLevel logger.LogLevel) logger.Interface {
|
||||||
|
truncatingLogger.LogLevel = logLevel
|
||||||
|
return truncatingLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (truncatingLogger *TruncatingLogger) Info(_ context.Context, message string, __ ...interface{}) {
|
||||||
|
if truncatingLogger.LogLevel < logger.Info {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rlog.Errorf("gorm info: %.150s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (truncatingLogger *TruncatingLogger) Warn(_ context.Context, message string, __ ...interface{}) {
|
||||||
|
if truncatingLogger.LogLevel < logger.Warn {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rlog.Errorf("gorm warning: %.150s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (truncatingLogger *TruncatingLogger) Error(_ context.Context, message string, __ ...interface{}) {
|
||||||
|
if truncatingLogger.LogLevel < logger.Error {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rlog.Errorf("gorm error: %.150s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (truncatingLogger *TruncatingLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
||||||
|
if truncatingLogger.LogLevel == logger.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elapsed := time.Since(begin)
|
||||||
|
if err != nil {
|
||||||
|
sql, rows := fc() // copied into every condition as this is a potentially heavy operation best done only when necessary
|
||||||
|
truncatingLogger.Error(ctx, fmt.Sprintf("Error in %s: %v - elapsed: %fs affected rows: %d, sql: %s", utils.FileWithLineNum(), err, elapsed.Seconds(), rows, sql))
|
||||||
|
} else if truncatingLogger.LogLevel >= logger.Warn && elapsed > truncatingLogger.SlowThreshold {
|
||||||
|
sql, rows := fc()
|
||||||
|
truncatingLogger.Warn(ctx, fmt.Sprintf("Slow sql query - elapse: %fs rows: %d, sql: %s", elapsed.Seconds(), rows, sql))
|
||||||
|
} else if truncatingLogger.LogLevel >= logger.Info {
|
||||||
|
sql, rows := fc()
|
||||||
|
truncatingLogger.Info(ctx, fmt.Sprintf("Sql query - elapse: %fs rows: %d, sql: %s", elapsed.Seconds(), rows, sql))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,42 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"context"
|
||||||
"fmt"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/romana/rlog"
|
||||||
"log"
|
"log"
|
||||||
"mizuserver/pkg/models"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"reflect"
|
"reflect"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartServer starts the server with a graceful shutdown
|
// StartServer starts the server with a graceful shutdown
|
||||||
func StartServer(app *fiber.App) {
|
func StartServer(app *gin.Engine) {
|
||||||
signals := make(chan os.Signal, 2)
|
signals := make(chan os.Signal, 2)
|
||||||
signal.Notify(signals,
|
signal.Notify(signals,
|
||||||
os.Interrupt, // this catch ctrl + c
|
os.Interrupt, // this catch ctrl + c
|
||||||
syscall.SIGTSTP, // this catch ctrl + z
|
syscall.SIGTSTP, // this catch ctrl + z
|
||||||
)
|
)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":8080",
|
||||||
|
Handler: app,
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
_ = <-signals
|
_ = <-signals
|
||||||
fmt.Println("Shutting down...")
|
rlog.Infof("Shutting down...")
|
||||||
_ = app.Shutdown()
|
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
_ = srv.Shutdown(ctx)
|
||||||
|
os.Exit(0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Run server.
|
// Run server.
|
||||||
if err := app.Listen(":8899"); err != nil {
|
if err := app.Run(":8899"); err != nil {
|
||||||
log.Printf("Oops... Server is not running! Reason: %v", err)
|
log.Printf("Oops... Server is not running! Reason: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,27 +69,3 @@ func SetHostname(address, newHostname string) string {
|
|||||||
return replacedUrl.String()
|
return replacedUrl.String()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetResolvedBaseEntry(entry models.MizuEntry) models.BaseEntryDetails {
|
|
||||||
entryUrl := entry.Url
|
|
||||||
service := entry.Service
|
|
||||||
if entry.ResolvedDestination != "" {
|
|
||||||
entryUrl = SetHostname(entryUrl, entry.ResolvedDestination)
|
|
||||||
service = SetHostname(service, entry.ResolvedDestination)
|
|
||||||
}
|
|
||||||
return models.BaseEntryDetails{
|
|
||||||
Id: entry.EntryId,
|
|
||||||
Url: entryUrl,
|
|
||||||
Service: service,
|
|
||||||
Path: entry.Path,
|
|
||||||
StatusCode: entry.Status,
|
|
||||||
Method: entry.Method,
|
|
||||||
Timestamp: entry.Timestamp,
|
|
||||||
RequestSenderIp: entry.RequestSenderIp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetBytesFromStruct(v interface{}) []byte{
|
|
||||||
a, _ := json.Marshal(v)
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
8
agent/pkg/version/consts.go
Normal file
8
agent/pkg/version/consts.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
var (
|
||||||
|
SemVer = "0.0.1"
|
||||||
|
Branch = "develop"
|
||||||
|
GitCommitHash = "" // this var is overridden using ldflags in makefile when building
|
||||||
|
BuildTimestamp = "" // this var is overridden using ldflags in makefile when building
|
||||||
|
)
|
||||||
147
api/main.go
147
api/main.go
@@ -1,147 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
"mizuserver/pkg/api"
|
|
||||||
"mizuserver/pkg/middleware"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
"mizuserver/pkg/routes"
|
|
||||||
"mizuserver/pkg/sensitiveDataFiltering"
|
|
||||||
"mizuserver/pkg/tap"
|
|
||||||
"mizuserver/pkg/utils"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
)
|
|
||||||
|
|
||||||
var shouldTap = flag.Bool("tap", false, "Run in tapper mode without API")
|
|
||||||
var aggregator = flag.Bool("aggregator", false, "Run in aggregator mode with API")
|
|
||||||
var standalone = flag.Bool("standalone", false, "Run in standalone tapper and API mode")
|
|
||||||
var aggregatorAddress = flag.String("aggregator-address", "", "Address of mizu collector for tapping")
|
|
||||||
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if !*shouldTap && !*aggregator && !*standalone{
|
|
||||||
panic("One of the flags --tap, --api or --standalone must be provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *standalone {
|
|
||||||
harOutputChannel := tap.StartPassiveTapper()
|
|
||||||
filteredHarChannel := make(chan *tap.OutputChannelItem)
|
|
||||||
go filterHarHeaders(harOutputChannel, filteredHarChannel, getTrafficFilteringOptions())
|
|
||||||
go api.StartReadingEntries(filteredHarChannel, nil)
|
|
||||||
hostApi(nil)
|
|
||||||
} else if *shouldTap {
|
|
||||||
if *aggregatorAddress == "" {
|
|
||||||
panic("Aggregator address must be provided with --aggregator-address when using --tap")
|
|
||||||
}
|
|
||||||
|
|
||||||
tapTargets := getTapTargets()
|
|
||||||
if tapTargets != nil {
|
|
||||||
tap.HostAppAddresses = tapTargets
|
|
||||||
fmt.Println("Filtering for the following addresses:", tap.HostAppAddresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
harOutputChannel := tap.StartPassiveTapper()
|
|
||||||
socketConnection, err := shared.ConnectToSocketServer(*aggregatorAddress, shared.DEFAULT_SOCKET_RETRIES, shared.DEFAULT_SOCKET_RETRY_SLEEP_TIME, false)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Error connecting to socket server at %s %v", *aggregatorAddress, err))
|
|
||||||
}
|
|
||||||
go pipeChannelToSocket(socketConnection, harOutputChannel)
|
|
||||||
} else if *aggregator {
|
|
||||||
socketHarOutChannel := make(chan *tap.OutputChannelItem, 1000)
|
|
||||||
filteredHarChannel := make(chan *tap.OutputChannelItem)
|
|
||||||
go api.StartReadingEntries(filteredHarChannel, nil)
|
|
||||||
go filterHarHeaders(socketHarOutChannel, filteredHarChannel, getTrafficFilteringOptions())
|
|
||||||
hostApi(socketHarOutChannel)
|
|
||||||
}
|
|
||||||
|
|
||||||
signalChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(signalChan, os.Interrupt)
|
|
||||||
<-signalChan
|
|
||||||
|
|
||||||
fmt.Println("Exiting")
|
|
||||||
}
|
|
||||||
|
|
||||||
func hostApi(socketHarOutputChannel chan<- *tap.OutputChannelItem) {
|
|
||||||
app := fiber.New()
|
|
||||||
|
|
||||||
|
|
||||||
middleware.FiberMiddleware(app) // Register Fiber's middleware for app.
|
|
||||||
app.Static("/", "./site")
|
|
||||||
|
|
||||||
//Simple route to know server is running
|
|
||||||
app.Get("/echo", func(c *fiber.Ctx) error {
|
|
||||||
return c.SendString("Hello, World 👋!")
|
|
||||||
})
|
|
||||||
eventHandlers := api.RoutesEventHandlers{
|
|
||||||
SocketHarOutChannel: socketHarOutputChannel,
|
|
||||||
}
|
|
||||||
routes.WebSocketRoutes(app, &eventHandlers)
|
|
||||||
routes.EntriesRoutes(app)
|
|
||||||
routes.NotFoundRoute(app)
|
|
||||||
|
|
||||||
utils.StartServer(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func getTapTargets() []string {
|
|
||||||
nodeName := os.Getenv(shared.NodeNameEnvVar)
|
|
||||||
var tappedAddressesPerNodeDict map[string][]string
|
|
||||||
err := json.Unmarshal([]byte(os.Getenv(shared.TappedAddressesPerNodeDictEnvVar)), &tappedAddressesPerNodeDict)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("env var %s's value of %s is invalid! must be map[string][]string %v", shared.TappedAddressesPerNodeDictEnvVar, tappedAddressesPerNodeDict, err))
|
|
||||||
}
|
|
||||||
return tappedAddressesPerNodeDict[nodeName]
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTrafficFilteringOptions() *shared.TrafficFilteringOptions {
|
|
||||||
filteringOptionsJson := os.Getenv(shared.MizuFilteringOptionsEnvVar)
|
|
||||||
if filteringOptionsJson == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var filteringOptions shared.TrafficFilteringOptions
|
|
||||||
err := json.Unmarshal([]byte(filteringOptionsJson), &filteringOptions)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("env var %s's value of %s is invalid! json must match the shared.TrafficFilteringOptions struct %v", shared.MizuFilteringOptionsEnvVar, filteringOptionsJson, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &filteringOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterHarHeaders(inChannel <- chan *tap.OutputChannelItem, outChannel chan *tap.OutputChannelItem, filterOptions *shared.TrafficFilteringOptions) {
|
|
||||||
for message := range inChannel {
|
|
||||||
sensitiveDataFiltering.FilterSensitiveInfoFromHarRequest(message, filterOptions)
|
|
||||||
outChannel <- message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pipeChannelToSocket(connection *websocket.Conn, messageDataChannel <-chan *tap.OutputChannelItem) {
|
|
||||||
if connection == nil {
|
|
||||||
panic("Websocket connection is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if messageDataChannel == nil {
|
|
||||||
panic("Channel of captured messages is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
for messageData := range messageDataChannel {
|
|
||||||
marshaledData, err := models.CreateWebsocketTappedEntryMessage(messageData)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error converting message to json %s, (%v,%+v)\n", err, err, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = connection.WriteMessage(websocket.TextMessage, marshaledData)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error sending message through socket server %s, (%v,%+v)\n", err, err, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
||||||
"mizuserver/pkg/database"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
"mizuserver/pkg/resolver"
|
|
||||||
"mizuserver/pkg/tap"
|
|
||||||
"mizuserver/pkg/utils"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var k8sResolver *resolver.Resolver
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
errOut := make(chan error, 100)
|
|
||||||
res, err := resolver.NewFromInCluster(errOut)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error creating k8s resolver %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
res.Start(ctx)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case err := <-errOut:
|
|
||||||
fmt.Printf("name resolving error %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
k8sResolver = res
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartReadingEntries(harChannel <-chan *tap.OutputChannelItem, workingDir *string) {
|
|
||||||
if workingDir != nil && *workingDir != "" {
|
|
||||||
startReadingFiles(*workingDir)
|
|
||||||
} else {
|
|
||||||
startReadingChannel(harChannel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startReadingFiles(workingDir string) {
|
|
||||||
err := os.MkdirAll(workingDir, os.ModePerm)
|
|
||||||
utils.CheckErr(err)
|
|
||||||
|
|
||||||
for true {
|
|
||||||
dir, _ := os.Open(workingDir)
|
|
||||||
dirFiles, _ := dir.Readdir(-1)
|
|
||||||
sort.Sort(utils.ByModTime(dirFiles))
|
|
||||||
|
|
||||||
if len(dirFiles) == 0 {
|
|
||||||
fmt.Printf("Waiting for new files\n")
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fileInfo := dirFiles[0]
|
|
||||||
inputFilePath := path.Join(workingDir, fileInfo.Name())
|
|
||||||
file, err := os.Open(inputFilePath)
|
|
||||||
utils.CheckErr(err)
|
|
||||||
|
|
||||||
var inputHar har.HAR
|
|
||||||
decErr := json.NewDecoder(bufio.NewReader(file)).Decode(&inputHar)
|
|
||||||
utils.CheckErr(decErr)
|
|
||||||
|
|
||||||
for _, entry := range inputHar.Log.Entries {
|
|
||||||
time.Sleep(time.Millisecond * 250)
|
|
||||||
saveHarToDb(entry, fileInfo.Name())
|
|
||||||
}
|
|
||||||
rmErr := os.Remove(inputFilePath)
|
|
||||||
utils.CheckErr(rmErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startReadingChannel(outputItems <-chan *tap.OutputChannelItem) {
|
|
||||||
if outputItems == nil {
|
|
||||||
panic("Channel of captured messages is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
for item := range outputItems {
|
|
||||||
saveHarToDb(item.HarEntry, item.RequestSenderIp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveHarToDb(entry *har.Entry, sender string) {
|
|
||||||
entryBytes, _ := json.Marshal(entry)
|
|
||||||
serviceName, urlPath, serviceHostName := getServiceNameFromUrl(entry.Request.URL)
|
|
||||||
entryId := primitive.NewObjectID().Hex()
|
|
||||||
var (
|
|
||||||
resolvedSource string
|
|
||||||
resolvedDestination string
|
|
||||||
)
|
|
||||||
if k8sResolver != nil {
|
|
||||||
resolvedSource = k8sResolver.Resolve(sender)
|
|
||||||
resolvedDestination = k8sResolver.Resolve(serviceHostName)
|
|
||||||
}
|
|
||||||
mizuEntry := models.MizuEntry{
|
|
||||||
EntryId: entryId,
|
|
||||||
Entry: string(entryBytes), // simple way to store it and not convert to bytes
|
|
||||||
Service: serviceName,
|
|
||||||
Url: entry.Request.URL,
|
|
||||||
Path: urlPath,
|
|
||||||
Method: entry.Request.Method,
|
|
||||||
Status: entry.Response.Status,
|
|
||||||
RequestSenderIp: sender,
|
|
||||||
Timestamp: entry.StartedDateTime.UnixNano() / int64(time.Millisecond),
|
|
||||||
ResolvedSource: resolvedSource,
|
|
||||||
ResolvedDestination: resolvedDestination,
|
|
||||||
}
|
|
||||||
database.GetEntriesTable().Create(&mizuEntry)
|
|
||||||
|
|
||||||
baseEntry := utils.GetResolvedBaseEntry(mizuEntry)
|
|
||||||
baseEntryBytes, _ := models.CreateBaseEntryWebSocketMessage(&baseEntry)
|
|
||||||
broadcastToBrowserClients(baseEntryBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServiceNameFromUrl(inputUrl string) (string, string, string) {
|
|
||||||
parsed, err := url.Parse(inputUrl)
|
|
||||||
utils.CheckErr(err)
|
|
||||||
return fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host), parsed.Path, parsed.Host
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/antoniodipinto/ikisocket"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
"mizuserver/pkg/controllers"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
"mizuserver/pkg/routes"
|
|
||||||
"mizuserver/pkg/tap"
|
|
||||||
)
|
|
||||||
|
|
||||||
var browserClientSocketUUIDs = make([]string, 0)
|
|
||||||
|
|
||||||
type RoutesEventHandlers struct {
|
|
||||||
routes.EventHandlers
|
|
||||||
SocketHarOutChannel chan<- *tap.OutputChannelItem
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (h *RoutesEventHandlers) WebSocketConnect(ep *ikisocket.EventPayload) {
|
|
||||||
if ep.Kws.GetAttribute("is_tapper") == true {
|
|
||||||
fmt.Println(fmt.Sprintf("Websocket Connection event - Tapper connected: %s", ep.SocketUUID))
|
|
||||||
} else {
|
|
||||||
fmt.Println(fmt.Sprintf("Websocket Connection event - Browser socket connected: %s", ep.SocketUUID))
|
|
||||||
browserClientSocketUUIDs = append(browserClientSocketUUIDs, ep.SocketUUID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RoutesEventHandlers) WebSocketDisconnect(ep *ikisocket.EventPayload) {
|
|
||||||
if ep.Kws.GetAttribute("is_tapper") == true {
|
|
||||||
fmt.Println(fmt.Sprintf("Disconnection event - Tapper connected: %s", ep.SocketUUID))
|
|
||||||
} else {
|
|
||||||
fmt.Println(fmt.Sprintf("Disconnection event - Browser socket connected: %s", ep.SocketUUID))
|
|
||||||
removeSocketUUIDFromBrowserSlice(ep.SocketUUID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func broadcastToBrowserClients(message []byte) {
|
|
||||||
ikisocket.EmitToList(browserClientSocketUUIDs, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RoutesEventHandlers) WebSocketClose(ep *ikisocket.EventPayload) {
|
|
||||||
if ep.Kws.GetAttribute("is_tapper") == true {
|
|
||||||
fmt.Println(fmt.Sprintf("Websocket Close event - Tapper connected: %s", ep.SocketUUID))
|
|
||||||
} else {
|
|
||||||
fmt.Println(fmt.Sprintf("Websocket Close event - Browser socket connected: %s", ep.SocketUUID))
|
|
||||||
removeSocketUUIDFromBrowserSlice(ep.SocketUUID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RoutesEventHandlers) WebSocketError(ep *ikisocket.EventPayload) {
|
|
||||||
fmt.Println(fmt.Sprintf("Socket error - Socket uuid : %s %v", ep.SocketUUID, ep.Error))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RoutesEventHandlers) WebSocketMessage(ep *ikisocket.EventPayload) {
|
|
||||||
var socketMessageBase shared.WebSocketMessageMetadata
|
|
||||||
err := json.Unmarshal(ep.Data, &socketMessageBase)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Could not unmarshal websocket message %v\n", err)
|
|
||||||
} else {
|
|
||||||
switch socketMessageBase.MessageType {
|
|
||||||
case shared.WebSocketMessageTypeTappedEntry:
|
|
||||||
var tappedEntryMessage models.WebSocketTappedEntryMessage
|
|
||||||
err := json.Unmarshal(ep.Data, &tappedEntryMessage)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
|
||||||
} else {
|
|
||||||
h.SocketHarOutChannel <- tappedEntryMessage.Data
|
|
||||||
}
|
|
||||||
case shared.WebSocketMessageTypeUpdateStatus:
|
|
||||||
var statusMessage shared.WebSocketStatusMessage
|
|
||||||
err := json.Unmarshal(ep.Data, &statusMessage)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
|
||||||
} else {
|
|
||||||
controllers.TapStatus = statusMessage.TappingStatus
|
|
||||||
broadcastToBrowserClients(ep.Data)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
fmt.Printf("Received socket message of type %s for which no handlers are defined", socketMessageBase.MessageType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func removeSocketUUIDFromBrowserSlice(uuidToRemove string) {
|
|
||||||
newUUIDSlice := make([]string, 0, len(browserClientSocketUUIDs))
|
|
||||||
for _, uuid := range browserClientSocketUUIDs {
|
|
||||||
if uuid != uuidToRemove {
|
|
||||||
newUUIDSlice = append(newUUIDSlice, uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
browserClientSocketUUIDs = newUUIDSlice
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
"mizuserver/pkg/database"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
"mizuserver/pkg/utils"
|
|
||||||
"mizuserver/pkg/validation"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
OrderDesc = "desc"
|
|
||||||
OrderAsc = "asc"
|
|
||||||
LT = "lt"
|
|
||||||
GT = "gt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
operatorToSymbolMapping = map[string]string{
|
|
||||||
LT: "<",
|
|
||||||
GT: ">",
|
|
||||||
}
|
|
||||||
operatorToOrderMapping = map[string]string{
|
|
||||||
LT: OrderDesc,
|
|
||||||
GT: OrderAsc,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetEntries(c *fiber.Ctx) error {
|
|
||||||
entriesFilter := &models.EntriesFilter{}
|
|
||||||
|
|
||||||
if err := c.QueryParser(entriesFilter); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(err)
|
|
||||||
}
|
|
||||||
err := validation.Validate(entriesFilter)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
order := operatorToOrderMapping[entriesFilter.Operator]
|
|
||||||
operatorSymbol := operatorToSymbolMapping[entriesFilter.Operator]
|
|
||||||
var entries []models.MizuEntry
|
|
||||||
database.GetEntriesTable().
|
|
||||||
Order(fmt.Sprintf("timestamp %s", order)).
|
|
||||||
Where(fmt.Sprintf("timestamp %s %v", operatorSymbol, entriesFilter.Timestamp)).
|
|
||||||
Omit("entry"). // remove the "big" entry field
|
|
||||||
Limit(entriesFilter.Limit).
|
|
||||||
Find(&entries)
|
|
||||||
|
|
||||||
if len(entries) > 0 && order == OrderDesc {
|
|
||||||
// the entries always order from oldest to newest so we should revers
|
|
||||||
utils.ReverseSlice(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to base entries
|
|
||||||
baseEntries := make([]models.BaseEntryDetails, 0, entriesFilter.Limit)
|
|
||||||
for _, entry := range entries {
|
|
||||||
baseEntries = append(baseEntries, utils.GetResolvedBaseEntry(entry))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(baseEntries)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetHARs(c *fiber.Ctx) error {
|
|
||||||
entriesFilter := &models.HarFetchRequestBody{}
|
|
||||||
order := OrderDesc
|
|
||||||
if err := c.QueryParser(entriesFilter); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(err)
|
|
||||||
}
|
|
||||||
err := validation.Validate(entriesFilter)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var entries []models.MizuEntry
|
|
||||||
database.GetEntriesTable().
|
|
||||||
Order(fmt.Sprintf("timestamp %s", order)).
|
|
||||||
// Where(fmt.Sprintf("timestamp %s %v", operatorSymbol, entriesFilter.Timestamp)).
|
|
||||||
Limit(1000).
|
|
||||||
Find(&entries)
|
|
||||||
|
|
||||||
if len(entries) > 0 {
|
|
||||||
// the entries always order from oldest to newest so we should revers
|
|
||||||
utils.ReverseSlice(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
harsObject := map[string]*models.ExtendedHAR{}
|
|
||||||
|
|
||||||
for _, entryData := range entries {
|
|
||||||
var harEntry har.Entry
|
|
||||||
_ = json.Unmarshal([]byte(entryData.Entry), &harEntry)
|
|
||||||
|
|
||||||
sourceOfEntry := entryData.ResolvedSource
|
|
||||||
fileName := fmt.Sprintf("%s.har", sourceOfEntry)
|
|
||||||
if harOfSource, ok := harsObject[fileName]; ok {
|
|
||||||
harOfSource.Log.Entries = append(harOfSource.Log.Entries, &harEntry)
|
|
||||||
} else {
|
|
||||||
var entriesHar []*har.Entry
|
|
||||||
entriesHar = append(entriesHar, &harEntry)
|
|
||||||
harsObject[fileName] = &models.ExtendedHAR{
|
|
||||||
Log: &models.ExtendedLog{
|
|
||||||
Version: "1.2",
|
|
||||||
Creator: &models.ExtendedCreator{
|
|
||||||
Creator: &har.Creator{
|
|
||||||
Name: "mizu",
|
|
||||||
Version: "0.0.2",
|
|
||||||
},
|
|
||||||
Source: sourceOfEntry,
|
|
||||||
},
|
|
||||||
Entries: entriesHar,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
retObj := map[string][]byte{}
|
|
||||||
for k, v := range harsObject {
|
|
||||||
bytesData, _ := json.Marshal(v)
|
|
||||||
retObj[k] = bytesData
|
|
||||||
}
|
|
||||||
buffer := utils.ZipData(retObj)
|
|
||||||
return c.Status(fiber.StatusOK).SendStream(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetEntry(c *fiber.Ctx) error {
|
|
||||||
var entryData models.EntryData
|
|
||||||
database.GetEntriesTable().
|
|
||||||
Select("entry", "resolvedDestination").
|
|
||||||
Where(map[string]string{"entryId": c.Params("entryId")}).
|
|
||||||
First(&entryData)
|
|
||||||
|
|
||||||
var fullEntry har.Entry
|
|
||||||
unmarshallErr := json.Unmarshal([]byte(entryData.Entry), &fullEntry)
|
|
||||||
utils.CheckErr(unmarshallErr)
|
|
||||||
|
|
||||||
if entryData.ResolvedDestination != "" {
|
|
||||||
fullEntry.Request.URL = utils.SetHostname(fullEntry.Request.URL, entryData.ResolvedDestination)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(fullEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteAllEntries(c *fiber.Ctx) error {
|
|
||||||
database.GetEntriesTable().
|
|
||||||
Where("1 = 1").
|
|
||||||
Delete(&models.MizuEntry{})
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
||||||
"msg": "Success",
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetGeneralStats(c *fiber.Ctx) error {
|
|
||||||
sqlQuery := "SELECT count(*) as count, min(timestamp) as min, max(timestamp) as max from mizu_entries"
|
|
||||||
var result struct {
|
|
||||||
Count int
|
|
||||||
Min int
|
|
||||||
Max int
|
|
||||||
}
|
|
||||||
database.GetEntriesTable().Raw(sqlQuery).Scan(&result)
|
|
||||||
return c.Status(fiber.StatusOK).JSON(&result)
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
)
|
|
||||||
|
|
||||||
var TapStatus shared.TapStatus
|
|
||||||
|
|
||||||
func GetTappingStatus(c *fiber.Ctx) error {
|
|
||||||
return c.Status(fiber.StatusOK).JSON(TapStatus)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DBPath = "./entries.db"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
DB = initDataBase(DBPath)
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetEntriesTable() *gorm.DB {
|
|
||||||
return DB.Table("mizu_entries")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDataBase(databasePath string) *gorm.DB {
|
|
||||||
temp, _ := gorm.Open(sqlite.Open(databasePath), &gorm.Config{})
|
|
||||||
_ = temp.AutoMigrate(&models.MizuEntry{}) // this will ensure table is created
|
|
||||||
return temp
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FiberMiddleware provide Fiber's built-in middlewares.
|
|
||||||
// See: https://docs.gofiber.io/api/middleware
|
|
||||||
func FiberMiddleware(a *fiber.App) {
|
|
||||||
a.Use(
|
|
||||||
// Add CORS to each route.
|
|
||||||
cors.New(),
|
|
||||||
// Add simple logger.
|
|
||||||
logger.New(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
"mizuserver/pkg/tap"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MizuEntry struct {
|
|
||||||
ID uint `gorm:"primarykey"`
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
Entry string `json:"entry,omitempty" gorm:"column:entry"`
|
|
||||||
EntryId string `json:"entryId" gorm:"column:entryId"`
|
|
||||||
Url string `json:"url" gorm:"column:url"`
|
|
||||||
Method string `json:"method" gorm:"column:method"`
|
|
||||||
Status int `json:"status" gorm:"column:status"`
|
|
||||||
RequestSenderIp string `json:"requestSenderIp" gorm:"column:requestSenderIp"`
|
|
||||||
Service string `json:"service" gorm:"column:service"`
|
|
||||||
Timestamp int64 `json:"timestamp" gorm:"column:timestamp"`
|
|
||||||
Path string `json:"path" gorm:"column:path"`
|
|
||||||
ResolvedSource string `json:"resolvedSource,omitempty" gorm:"column:resolvedSource"`
|
|
||||||
ResolvedDestination string `json:"resolvedDestination,omitempty" gorm:"column:resolvedDestination"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseEntryDetails struct {
|
|
||||||
Id string `json:"id,omitempty"`
|
|
||||||
Url string `json:"url,omitempty"`
|
|
||||||
RequestSenderIp string `json:"requestSenderIp,omitempty"`
|
|
||||||
Service string `json:"service,omitempty"`
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
StatusCode int `json:"statusCode,omitempty"`
|
|
||||||
Method string `json:"method,omitempty"`
|
|
||||||
Timestamp int64 `json:"timestamp,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EntryData struct {
|
|
||||||
Entry string `json:"entry,omitempty"`
|
|
||||||
ResolvedDestination string `json:"resolvedDestination,omitempty" gorm:"column:resolvedDestination"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EntriesFilter struct {
|
|
||||||
Limit int `query:"limit" validate:"required,min=1,max=200"`
|
|
||||||
Operator string `query:"operator" validate:"required,oneof='lt' 'gt'"`
|
|
||||||
Timestamp int64 `query:"timestamp" validate:"required,min=1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HarFetchRequestBody struct {
|
|
||||||
Limit int `query:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebSocketEntryMessage struct {
|
|
||||||
*shared.WebSocketMessageMetadata
|
|
||||||
Data *BaseEntryDetails `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type WebSocketTappedEntryMessage struct {
|
|
||||||
*shared.WebSocketMessageMetadata
|
|
||||||
Data *tap.OutputChannelItem
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateBaseEntryWebSocketMessage(base *BaseEntryDetails) ([]byte, error) {
|
|
||||||
message := &WebSocketEntryMessage{
|
|
||||||
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
|
||||||
MessageType: shared.WebSocketMessageTypeEntry,
|
|
||||||
},
|
|
||||||
Data: base,
|
|
||||||
}
|
|
||||||
return json.Marshal(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateWebsocketTappedEntryMessage(base *tap.OutputChannelItem) ([]byte, error) {
|
|
||||||
message := &WebSocketTappedEntryMessage{
|
|
||||||
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
|
||||||
MessageType: shared.WebSocketMessageTypeTappedEntry,
|
|
||||||
},
|
|
||||||
Data: base,
|
|
||||||
}
|
|
||||||
return json.Marshal(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ExtendedHAR is the top level object of a HAR log.
|
|
||||||
type ExtendedHAR struct {
|
|
||||||
Log *ExtendedLog `json:"log"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtendedLog is the HAR HTTP request and response log.
|
|
||||||
type ExtendedLog struct {
|
|
||||||
// Version number of the HAR format.
|
|
||||||
Version string `json:"version"`
|
|
||||||
// Creator holds information about the log creator application.
|
|
||||||
Creator *ExtendedCreator `json:"creator"`
|
|
||||||
// Entries is a list containing requests and responses.
|
|
||||||
Entries []*har.Entry `json:"entries"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExtendedCreator struct {
|
|
||||||
*har.Creator
|
|
||||||
Source string `json:"_source"`
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
|
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/openstack"
|
|
||||||
restclient "k8s.io/client-go/rest"
|
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
|
||||||
"k8s.io/client-go/util/homedir"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewFromInCluster(errOut chan error) (*Resolver, error) {
|
|
||||||
config, err := restclient.InClusterConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clientset, err := kubernetes.NewForConfig(config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Resolver{clientConfig: config, clientSet: clientset, nameMap: make(map[string]string), errOut: errOut}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFromOutOfCluster(kubeConfigPath string, errOut chan error) (*Resolver, error) {
|
|
||||||
if kubeConfigPath == "" {
|
|
||||||
home := homedir.HomeDir()
|
|
||||||
kubeConfigPath = filepath.Join(home, ".kube", "config")
|
|
||||||
}
|
|
||||||
|
|
||||||
configPathList := filepath.SplitList(kubeConfigPath)
|
|
||||||
configLoadingRules := &clientcmd.ClientConfigLoadingRules{}
|
|
||||||
if len(configPathList) <= 1 {
|
|
||||||
configLoadingRules.ExplicitPath = kubeConfigPath
|
|
||||||
} else {
|
|
||||||
configLoadingRules.Precedence = configPathList
|
|
||||||
}
|
|
||||||
contextName := ""
|
|
||||||
clientConfigLoader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
|
||||||
configLoadingRules,
|
|
||||||
&clientcmd.ConfigOverrides{
|
|
||||||
CurrentContext: contextName,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
clientConfig, err := clientConfigLoader.ClientConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clientset, err := kubernetes.NewForConfig(clientConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Resolver{clientConfig: clientConfig, clientSet: clientset, nameMap: make(map[string]string), errOut: errOut}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFromExisting(clientConfig *restclient.Config, clientSet *kubernetes.Clientset, errOut chan error) *Resolver {
|
|
||||||
return &Resolver{clientConfig: clientConfig, clientSet: clientSet, nameMap: make(map[string]string), errOut: errOut}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
package resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
restclient "k8s.io/client-go/rest"
|
|
||||||
)
|
|
||||||
const (
|
|
||||||
kubClientNullString = "None"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Resolver struct {
|
|
||||||
clientConfig *restclient.Config
|
|
||||||
clientSet *kubernetes.Clientset
|
|
||||||
nameMap map[string]string
|
|
||||||
isStarted bool
|
|
||||||
errOut chan error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) Start(ctx context.Context) {
|
|
||||||
if !resolver.isStarted {
|
|
||||||
resolver.isStarted = true
|
|
||||||
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchServices)
|
|
||||||
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchEndpoints)
|
|
||||||
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchPods)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) Resolve(name string) string {
|
|
||||||
resolvedName, isFound := resolver.nameMap[name]
|
|
||||||
if !isFound {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return resolvedName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) watchPods(ctx context.Context) error {
|
|
||||||
// empty namespace makes the client watch all namespaces
|
|
||||||
watcher, err := resolver.clientSet.CoreV1().Pods("").Watch(ctx, metav1.ListOptions{Watch: true})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <- watcher.ResultChan():
|
|
||||||
if event.Object == nil {
|
|
||||||
return errors.New("error in kubectl pod watch")
|
|
||||||
}
|
|
||||||
if event.Type == watch.Deleted {
|
|
||||||
pod := event.Object.(*corev1.Pod)
|
|
||||||
resolver.saveResolvedName(pod.Status.PodIP, "", event.Type)
|
|
||||||
}
|
|
||||||
case <- ctx.Done():
|
|
||||||
watcher.Stop()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) watchEndpoints(ctx context.Context) error {
|
|
||||||
// empty namespace makes the client watch all namespaces
|
|
||||||
watcher, err := resolver.clientSet.CoreV1().Endpoints("").Watch(ctx, metav1.ListOptions{Watch: true})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <- watcher.ResultChan():
|
|
||||||
if event.Object == nil {
|
|
||||||
return errors.New("error in kubectl endpoint watch")
|
|
||||||
}
|
|
||||||
endpoint := event.Object.(*corev1.Endpoints)
|
|
||||||
serviceHostname := fmt.Sprintf("%s.%s", endpoint.Name, endpoint.Namespace)
|
|
||||||
if endpoint.Subsets != nil {
|
|
||||||
for _, subset := range endpoint.Subsets {
|
|
||||||
var ports []int32
|
|
||||||
if subset.Ports != nil {
|
|
||||||
for _, portMapping := range subset.Ports {
|
|
||||||
if portMapping.Port > 0 {
|
|
||||||
ports = append(ports, portMapping.Port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if subset.Addresses != nil {
|
|
||||||
for _, address := range subset.Addresses {
|
|
||||||
resolver.saveResolvedName(address.IP, serviceHostname, event.Type)
|
|
||||||
for _, port := range ports {
|
|
||||||
ipWithPort := fmt.Sprintf("%s:%d", address.IP, port)
|
|
||||||
resolver.saveResolvedName(ipWithPort, serviceHostname, event.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <- ctx.Done():
|
|
||||||
watcher.Stop()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) watchServices(ctx context.Context) error {
|
|
||||||
// empty namespace makes the client watch all namespaces
|
|
||||||
watcher, err := resolver.clientSet.CoreV1().Services("").Watch(ctx, metav1.ListOptions{Watch: true})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <- watcher.ResultChan():
|
|
||||||
if event.Object == nil {
|
|
||||||
return errors.New("error in kubectl service watch")
|
|
||||||
}
|
|
||||||
|
|
||||||
service := event.Object.(*corev1.Service)
|
|
||||||
serviceHostname := fmt.Sprintf("%s.%s", service.Name, service.Namespace)
|
|
||||||
if service.Spec.ClusterIP != "" && service.Spec.ClusterIP != kubClientNullString {
|
|
||||||
resolver.saveResolvedName(service.Spec.ClusterIP, serviceHostname, event.Type)
|
|
||||||
}
|
|
||||||
if service.Status.LoadBalancer.Ingress != nil {
|
|
||||||
for _, ingress := range service.Status.LoadBalancer.Ingress {
|
|
||||||
resolver.saveResolvedName(ingress.IP, serviceHostname, event.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <- ctx.Done():
|
|
||||||
watcher.Stop()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) saveResolvedName(key string, resolved string, eventType watch.EventType) {
|
|
||||||
if eventType == watch.Deleted {
|
|
||||||
delete(resolver.nameMap, key)
|
|
||||||
// fmt.Printf("setting %s=nil\n", key)
|
|
||||||
} else {
|
|
||||||
resolver.nameMap[key] = resolved
|
|
||||||
// fmt.Printf("setting %s=%s\n", key, resolved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) infiniteErrorHandleRetryFunc(ctx context.Context, fun func(ctx context.Context) error) {
|
|
||||||
for {
|
|
||||||
err := fun(ctx)
|
|
||||||
if err != nil {
|
|
||||||
resolver.errOut <- err
|
|
||||||
|
|
||||||
var statusError *k8serrors.StatusError
|
|
||||||
if errors.As(err, &statusError) {
|
|
||||||
if statusError.ErrStatus.Reason == metav1.StatusReasonForbidden {
|
|
||||||
fmt.Printf("Resolver loop encountered permission error, aborting event listening - %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ctx.Err() != nil { // context was cancelled or errored
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import "github.com/gofiber/fiber/v2"
|
|
||||||
|
|
||||||
// NotFoundRoute func for describe 404 Error route.
|
|
||||||
func NotFoundRoute(fiberApp *fiber.App) {
|
|
||||||
fiberApp.Use(
|
|
||||||
func(c *fiber.Ctx) error {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
|
||||||
"error": true,
|
|
||||||
"msg": "sorry, endpoint is not found",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"mizuserver/pkg/controllers"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EntriesRoutes func for describe group of public routes.
|
|
||||||
func EntriesRoutes(fiberApp *fiber.App) {
|
|
||||||
routeGroup := fiberApp.Group("/api")
|
|
||||||
|
|
||||||
routeGroup.Get("/entries", controllers.GetEntries) // get entries (base/thin entries)
|
|
||||||
routeGroup.Get("/entries/:entryId", controllers.GetEntry) // get single (full) entry
|
|
||||||
|
|
||||||
routeGroup.Get("/har", controllers.GetHARs)
|
|
||||||
routeGroup.Get("/resetDB", controllers.DeleteAllEntries) // get single (full) entry
|
|
||||||
routeGroup.Get("/generalStats", controllers.GetGeneralStats) // get general stats about entries in DB
|
|
||||||
|
|
||||||
routeGroup.Get("/tapStatus", controllers.GetTappingStatus) // get tapping status
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/antoniodipinto/ikisocket"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EventHandlers interface {
|
|
||||||
WebSocketConnect(ep *ikisocket.EventPayload)
|
|
||||||
WebSocketDisconnect(ep *ikisocket.EventPayload)
|
|
||||||
WebSocketClose(ep *ikisocket.EventPayload)
|
|
||||||
WebSocketError(ep *ikisocket.EventPayload)
|
|
||||||
WebSocketMessage(ep *ikisocket.EventPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func WebSocketRoutes(app *fiber.App, eventHandlers EventHandlers) {
|
|
||||||
app.Get("/ws", ikisocket.New(func(kws *ikisocket.Websocket) {
|
|
||||||
kws.SetAttribute("is_tapper", false)
|
|
||||||
}))
|
|
||||||
|
|
||||||
app.Get("/wsTapper", ikisocket.New(func(kws *ikisocket.Websocket) {
|
|
||||||
// Tapper clients are handled differently, they don't need to receive new message broadcasts.
|
|
||||||
kws.SetAttribute("is_tapper", true)
|
|
||||||
}))
|
|
||||||
|
|
||||||
ikisocket.On(ikisocket.EventMessage, eventHandlers.WebSocketMessage)
|
|
||||||
ikisocket.On(ikisocket.EventConnect, eventHandlers.WebSocketConnect)
|
|
||||||
ikisocket.On(ikisocket.EventDisconnect, eventHandlers.WebSocketDisconnect)
|
|
||||||
ikisocket.On(ikisocket.EventClose, eventHandlers.WebSocketClose) // This event is called when the server disconnects the user actively with .Close() method
|
|
||||||
ikisocket.On(ikisocket.EventError, eventHandlers.WebSocketError) // On error event
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
package sensitiveDataFiltering
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"mizuserver/pkg/tap"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/beevik/etree"
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FilterSensitiveInfoFromHarRequest(harOutputItem *tap.OutputChannelItem, options *shared.TrafficFilteringOptions) {
|
|
||||||
filterHarHeaders(harOutputItem.HarEntry.Request.Headers)
|
|
||||||
filterHarHeaders(harOutputItem.HarEntry.Response.Headers)
|
|
||||||
|
|
||||||
harOutputItem.HarEntry.Request.Cookies = make([]har.Cookie, 0, 0)
|
|
||||||
harOutputItem.HarEntry.Response.Cookies = make([]har.Cookie, 0, 0)
|
|
||||||
|
|
||||||
harOutputItem.HarEntry.Request.URL = filterUrl(harOutputItem.HarEntry.Request.URL)
|
|
||||||
for i, queryString := range harOutputItem.HarEntry.Request.QueryString {
|
|
||||||
if isFieldNameSensitive(queryString.Name) {
|
|
||||||
harOutputItem.HarEntry.Request.QueryString[i].Value = maskedFieldPlaceholderValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if harOutputItem.HarEntry.Request.PostData != nil {
|
|
||||||
requestContentType := getContentTypeHeaderValue(harOutputItem.HarEntry.Request.Headers)
|
|
||||||
filteredRequestBody, err := filterHttpBody([]byte(harOutputItem.HarEntry.Request.PostData.Text), requestContentType, options)
|
|
||||||
if err == nil {
|
|
||||||
harOutputItem.HarEntry.Request.PostData.Text = string(filteredRequestBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if harOutputItem.HarEntry.Response.Content != nil {
|
|
||||||
responseContentType := getContentTypeHeaderValue(harOutputItem.HarEntry.Response.Headers)
|
|
||||||
filteredResponseBody, err := filterHttpBody(harOutputItem.HarEntry.Response.Content.Text, responseContentType, options)
|
|
||||||
if err == nil {
|
|
||||||
harOutputItem.HarEntry.Response.Content.Text = filteredResponseBody
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterHarHeaders(headers []har.Header) {
|
|
||||||
for i, header := range headers {
|
|
||||||
if isFieldNameSensitive(header.Name) {
|
|
||||||
headers[i].Value = maskedFieldPlaceholderValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContentTypeHeaderValue(headers []har.Header) string {
|
|
||||||
for _, header := range headers {
|
|
||||||
if strings.ToLower(header.Name) == "content-type" {
|
|
||||||
return header.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func isFieldNameSensitive(fieldName string) bool {
|
|
||||||
name := strings.ToLower(fieldName)
|
|
||||||
name = strings.ReplaceAll(name, "_", "")
|
|
||||||
name = strings.ReplaceAll(name, "-", "")
|
|
||||||
name = strings.ReplaceAll(name, " ", "")
|
|
||||||
|
|
||||||
for _, sensitiveField := range personallyIdentifiableDataFields {
|
|
||||||
if strings.Contains(name, sensitiveField) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterHttpBody(bytes []byte, contentType string, options *shared.TrafficFilteringOptions) ([]byte, error) {
|
|
||||||
mimeType := strings.Split(contentType, ";")[0]
|
|
||||||
switch strings.ToLower(mimeType) {
|
|
||||||
case "application/json":
|
|
||||||
return filterJsonBody(bytes)
|
|
||||||
case "text/html":
|
|
||||||
fallthrough
|
|
||||||
case "application/xhtml+xml":
|
|
||||||
fallthrough
|
|
||||||
case "text/xml":
|
|
||||||
fallthrough
|
|
||||||
case "application/xml":
|
|
||||||
return filterXmlEtree(bytes)
|
|
||||||
case "text/plain":
|
|
||||||
if options != nil && options.PlainTextMaskingRegexes != nil {
|
|
||||||
return filterPlainText(bytes, options), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterPlainText(bytes []byte, options *shared.TrafficFilteringOptions) []byte {
|
|
||||||
for _, regex := range options.PlainTextMaskingRegexes {
|
|
||||||
bytes = regex.ReplaceAll(bytes, []byte(maskedFieldPlaceholderValue))
|
|
||||||
}
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterXmlEtree(bytes []byte) ([]byte, error) {
|
|
||||||
if !IsValidXML(bytes) {
|
|
||||||
return nil, errors.New("Invalid XML")
|
|
||||||
}
|
|
||||||
xmlDoc := etree.NewDocument()
|
|
||||||
err := xmlDoc.ReadFromBytes(bytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
filterXmlElement(xmlDoc.Root())
|
|
||||||
}
|
|
||||||
return xmlDoc.WriteToBytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsValidXML(data []byte) bool {
|
|
||||||
return xml.Unmarshal(data, new(interface{})) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterXmlElement(element *etree.Element) {
|
|
||||||
for i, attribute := range element.Attr {
|
|
||||||
if isFieldNameSensitive(attribute.Key) {
|
|
||||||
element.Attr[i].Value = maskedFieldPlaceholderValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if element.ChildElements() == nil || len(element.ChildElements()) == 0 {
|
|
||||||
if isFieldNameSensitive(element.Tag) {
|
|
||||||
element.SetText(maskedFieldPlaceholderValue)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, element := range element.ChildElements() {
|
|
||||||
filterXmlElement(element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterJsonBody(bytes []byte) ([]byte, error) {
|
|
||||||
var bodyJsonMap map[string] interface{}
|
|
||||||
err := json.Unmarshal(bytes ,&bodyJsonMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
filterJsonMap(bodyJsonMap)
|
|
||||||
return json.Marshal(bodyJsonMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterJsonMap(jsonMap map[string] interface{}) {
|
|
||||||
for key, value := range jsonMap {
|
|
||||||
if value == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nestedMap, isNested := value.(map[string] interface{})
|
|
||||||
if isNested {
|
|
||||||
filterJsonMap(nestedMap)
|
|
||||||
} else {
|
|
||||||
if isFieldNameSensitive(key) {
|
|
||||||
jsonMap[key] = maskedFieldPlaceholderValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// receives string representing url, returns string url without sensitive query param values (http://service/api?userId=bob&password=123&type=login -> http://service/api?userId=[REDACTED]&password=[REDACTED]&type=login)
|
|
||||||
func filterUrl(originalUrl string) string {
|
|
||||||
parsedUrl, err := url.Parse(originalUrl)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Sprintf("http://%s", maskedFieldPlaceholderValue)
|
|
||||||
} else {
|
|
||||||
if len(parsedUrl.RawQuery) > 0 {
|
|
||||||
newQueryArgs := make([]string, 0)
|
|
||||||
for urlQueryParamName, urlQueryParamValues := range parsedUrl.Query() {
|
|
||||||
newValues := urlQueryParamValues
|
|
||||||
if isFieldNameSensitive(urlQueryParamName) {
|
|
||||||
newValues = []string {maskedFieldPlaceholderValue}
|
|
||||||
}
|
|
||||||
for _, paramValue := range newValues {
|
|
||||||
newQueryArgs = append(newQueryArgs, fmt.Sprintf("%s=%s", urlQueryParamName, paramValue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedUrl.RawQuery = strings.Join(newQueryArgs, "&")
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedUrl.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
)
|
|
||||||
|
|
||||||
const readPermission = 0644
|
|
||||||
const tempFilenamePrefix = "har_writer"
|
|
||||||
|
|
||||||
type PairChanItem struct {
|
|
||||||
Request *http.Request
|
|
||||||
RequestTime time.Time
|
|
||||||
Response *http.Response
|
|
||||||
ResponseTime time.Time
|
|
||||||
RequestSenderIp string
|
|
||||||
}
|
|
||||||
|
|
||||||
func openNewHarFile(filename string) *HarFile {
|
|
||||||
file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, readPermission)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to open output file: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
harFile := HarFile{file: file, entryCount: 0}
|
|
||||||
harFile.writeHeader()
|
|
||||||
|
|
||||||
return &harFile
|
|
||||||
}
|
|
||||||
|
|
||||||
type HarFile struct {
|
|
||||||
file *os.File
|
|
||||||
entryCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEntry(request *http.Request, requestTime time.Time, response *http.Response, responseTime time.Time) (*har.Entry, error) {
|
|
||||||
harRequest, err := har.NewRequest(request, true)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("convert-request-to-har", "Failed converting request to HAR %s (%v,%+v)\n", err, err, err)
|
|
||||||
return nil, errors.New("Failed converting request to HAR")
|
|
||||||
}
|
|
||||||
|
|
||||||
harResponse, err := har.NewResponse(response, true)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("convert-response-to-har", "Failed converting response to HAR %s (%v,%+v)\n", err, err, err)
|
|
||||||
return nil, errors.New("Failed converting response to HAR")
|
|
||||||
}
|
|
||||||
|
|
||||||
if harRequest.PostData != nil && strings.HasPrefix(harRequest.PostData.MimeType, "application/grpc") {
|
|
||||||
// Force HTTP/2 gRPC into HAR template
|
|
||||||
|
|
||||||
harRequest.URL = fmt.Sprintf("%s://%s%s", request.Header.Get(":scheme"), request.Header.Get(":authority"), request.Header.Get(":path"))
|
|
||||||
|
|
||||||
status, err := strconv.Atoi(response.Header.Get(":status"))
|
|
||||||
if err != nil {
|
|
||||||
SilentError("convert-response-status-for-har", "Failed converting status to int %s (%v,%+v)\n", err, err, err)
|
|
||||||
return nil, errors.New("Failed converting response status to int for HAR")
|
|
||||||
}
|
|
||||||
harResponse.Status = status
|
|
||||||
} else {
|
|
||||||
// Martian copies http.Request.URL.String() to har.Request.URL, which usually contains the path.
|
|
||||||
// However, according to the HAR spec, the URL field needs to be the absolute URL.
|
|
||||||
var scheme string
|
|
||||||
if request.URL.Scheme != "" {
|
|
||||||
scheme = request.URL.Scheme
|
|
||||||
} else {
|
|
||||||
scheme = "http"
|
|
||||||
}
|
|
||||||
harRequest.URL = fmt.Sprintf("%s://%s%s", scheme, request.Host, request.URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
totalTime := responseTime.Sub(requestTime).Round(time.Millisecond).Milliseconds()
|
|
||||||
if totalTime < 1 {
|
|
||||||
totalTime = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
harEntry := har.Entry{
|
|
||||||
StartedDateTime: time.Now().UTC(),
|
|
||||||
Time: totalTime,
|
|
||||||
Request: harRequest,
|
|
||||||
Response: harResponse,
|
|
||||||
Cache: &har.Cache{},
|
|
||||||
Timings: &har.Timings{
|
|
||||||
Send: -1,
|
|
||||||
Wait: -1,
|
|
||||||
Receive: totalTime,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return &harEntry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *HarFile) WriteEntry(harEntry *har.Entry) {
|
|
||||||
harEntryJson, err := json.Marshal(harEntry)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("har-entry-marshal", "Failed converting har entry object to JSON%s (%v,%+v)\n", err, err, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var separator string
|
|
||||||
if f.GetEntryCount() > 0 {
|
|
||||||
separator = ","
|
|
||||||
} else {
|
|
||||||
separator = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
harEntryString := append([]byte(separator), harEntryJson...)
|
|
||||||
|
|
||||||
if _, err := f.file.Write(harEntryString); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to write to output file: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
f.entryCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *HarFile) GetEntryCount() int {
|
|
||||||
return f.entryCount
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *HarFile) Close() {
|
|
||||||
f.writeTrailer()
|
|
||||||
|
|
||||||
err := f.file.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to close output file: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f*HarFile) writeHeader() {
|
|
||||||
header := []byte(`{"log": {"version": "1.2", "creator": {"name": "Mizu", "version": "0.0.1"}, "entries": [`)
|
|
||||||
if _, err := f.file.Write(header); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to write header to output file: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f*HarFile) writeTrailer() {
|
|
||||||
trailer := []byte("]}}")
|
|
||||||
if _, err := f.file.Write(trailer); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to write trailer to output file: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHarWriter(outputDir string, maxEntries int) *HarWriter {
|
|
||||||
return &HarWriter{
|
|
||||||
OutputDirPath: outputDir,
|
|
||||||
MaxEntries: maxEntries,
|
|
||||||
PairChan: make(chan *PairChanItem),
|
|
||||||
OutChan: make(chan *OutputChannelItem, 1000),
|
|
||||||
currentFile: nil,
|
|
||||||
done: make(chan bool),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutputChannelItem struct {
|
|
||||||
HarEntry *har.Entry
|
|
||||||
RequestSenderIp string
|
|
||||||
}
|
|
||||||
|
|
||||||
type HarWriter struct {
|
|
||||||
OutputDirPath string
|
|
||||||
MaxEntries int
|
|
||||||
PairChan chan *PairChanItem
|
|
||||||
OutChan chan *OutputChannelItem
|
|
||||||
currentFile *HarFile
|
|
||||||
done chan bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hw *HarWriter) WritePair(request *http.Request, requestTime time.Time, response *http.Response, responseTime time.Time, requestSenderIp string) {
|
|
||||||
hw.PairChan <- &PairChanItem{
|
|
||||||
Request: request,
|
|
||||||
RequestTime: requestTime,
|
|
||||||
Response: response,
|
|
||||||
ResponseTime: responseTime,
|
|
||||||
RequestSenderIp: requestSenderIp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hw *HarWriter) Start() {
|
|
||||||
if hw.OutputDirPath != "" {
|
|
||||||
if err := os.MkdirAll(hw.OutputDirPath, os.ModePerm); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to create output directory: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for pair := range hw.PairChan {
|
|
||||||
harEntry, err := NewEntry(pair.Request, pair.RequestTime, pair.Response, pair.ResponseTime)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if hw.OutputDirPath != "" {
|
|
||||||
if hw.currentFile == nil {
|
|
||||||
hw.openNewFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
hw.currentFile.WriteEntry(harEntry)
|
|
||||||
|
|
||||||
if hw.currentFile.GetEntryCount() >= hw.MaxEntries {
|
|
||||||
hw.closeFile()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hw.OutChan <- &OutputChannelItem{
|
|
||||||
HarEntry: harEntry,
|
|
||||||
RequestSenderIp: pair.RequestSenderIp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hw.currentFile != nil {
|
|
||||||
hw.closeFile()
|
|
||||||
}
|
|
||||||
hw.done <- true
|
|
||||||
} ()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hw *HarWriter) Stop() {
|
|
||||||
close(hw.PairChan)
|
|
||||||
<-hw.done
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hw *HarWriter) openNewFile() {
|
|
||||||
filename := filepath.Join(os.TempDir(), fmt.Sprintf("%s_%d", tempFilenamePrefix, time.Now().UnixNano()))
|
|
||||||
hw.currentFile = openNewHarFile(filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hw *HarWriter) closeFile() {
|
|
||||||
hw.currentFile.Close()
|
|
||||||
tmpFilename := hw.currentFile.file.Name()
|
|
||||||
hw.currentFile = nil
|
|
||||||
|
|
||||||
filename := buildFilename(hw.OutputDirPath, time.Now())
|
|
||||||
err := os.Rename(tmpFilename, filename)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("Rename-file", "cannot rename file: %s (%v,%+v)\n", err, err, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildFilename(dir string, t time.Time) string {
|
|
||||||
// (epoch time in nanoseconds)__(YYYY_Month_DD__hh-mm-ss).har
|
|
||||||
filename := fmt.Sprintf("%d__%s.har", t.UnixNano(), t.Format("2006_Jan_02__15-04-05"))
|
|
||||||
return filepath.Join(dir, filename)
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/orcaman/concurrent-map"
|
|
||||||
)
|
|
||||||
|
|
||||||
type requestResponsePair struct {
|
|
||||||
Request httpMessage `json:"request"`
|
|
||||||
Response httpMessage `json:"response"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type envoyMessageWrapper struct {
|
|
||||||
HttpBufferedTrace requestResponsePair `json:"http_buffered_trace"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type headerKeyVal struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type messageBody struct {
|
|
||||||
Truncated bool `json:"truncated"`
|
|
||||||
AsBytes string `json:"as_bytes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpMessage struct {
|
|
||||||
IsRequest bool
|
|
||||||
Headers []headerKeyVal `json:"headers"`
|
|
||||||
HTTPVersion string `json:"httpVersion"`
|
|
||||||
Body messageBody `json:"body"`
|
|
||||||
captureTime time.Time
|
|
||||||
orig interface {}
|
|
||||||
requestSenderIp string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Key is {client_addr}:{client_port}->{dest_addr}:{dest_port}
|
|
||||||
type requestResponseMatcher struct {
|
|
||||||
openMessagesMap cmap.ConcurrentMap
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func createResponseRequestMatcher() requestResponseMatcher {
|
|
||||||
newMatcher := &requestResponseMatcher{openMessagesMap: cmap.New()}
|
|
||||||
return *newMatcher
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matcher *requestResponseMatcher) registerRequest(ident string, request *http.Request, captureTime time.Time, body string, isHTTP2 bool) *envoyMessageWrapper {
|
|
||||||
split := splitIdent(ident)
|
|
||||||
key := genKey(split)
|
|
||||||
|
|
||||||
messageExtraHeaders := []headerKeyVal{
|
|
||||||
{Key: "x-up9-source", Value: split[0]},
|
|
||||||
{Key: "x-up9-destination", Value: split[1] + ":" + split[3]},
|
|
||||||
}
|
|
||||||
|
|
||||||
requestHTTPMessage := requestToMessage(request, captureTime, body, &messageExtraHeaders, isHTTP2, split[0])
|
|
||||||
|
|
||||||
if response, found := matcher.openMessagesMap.Pop(key); found {
|
|
||||||
// Type assertion always succeeds because all of the map's values are of httpMessage type
|
|
||||||
responseHTTPMessage := response.(*httpMessage)
|
|
||||||
if responseHTTPMessage.IsRequest {
|
|
||||||
SilentError("Request-Duplicate", "Got duplicate request with same identifier\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Debug("Matched open Response for %s\n", key)
|
|
||||||
return matcher.preparePair(&requestHTTPMessage, responseHTTPMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
matcher.openMessagesMap.Set(key, &requestHTTPMessage)
|
|
||||||
Debug("Registered open Request for %s\n", key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matcher *requestResponseMatcher) registerResponse(ident string, response *http.Response, captureTime time.Time, body string, isHTTP2 bool) *envoyMessageWrapper {
|
|
||||||
split := splitIdent(ident)
|
|
||||||
key := genKey(split)
|
|
||||||
|
|
||||||
responseHTTPMessage := responseToMessage(response, captureTime, body, isHTTP2)
|
|
||||||
|
|
||||||
if request, found := matcher.openMessagesMap.Pop(key); found {
|
|
||||||
// Type assertion always succeeds because all of the map's values are of httpMessage type
|
|
||||||
requestHTTPMessage := request.(*httpMessage)
|
|
||||||
if !requestHTTPMessage.IsRequest {
|
|
||||||
SilentError("Response-Duplicate", "Got duplicate response with same identifier\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Debug("Matched open Request for %s\n", key)
|
|
||||||
return matcher.preparePair(requestHTTPMessage, &responseHTTPMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
matcher.openMessagesMap.Set(key, &responseHTTPMessage)
|
|
||||||
Debug("Registered open Response for %s\n", key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matcher *requestResponseMatcher) preparePair(requestHTTPMessage *httpMessage, responseHTTPMessage *httpMessage) *envoyMessageWrapper {
|
|
||||||
matcher.addDuration(requestHTTPMessage, responseHTTPMessage)
|
|
||||||
|
|
||||||
return &envoyMessageWrapper{
|
|
||||||
HttpBufferedTrace: requestResponsePair{
|
|
||||||
Request: *requestHTTPMessage,
|
|
||||||
Response: *responseHTTPMessage,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestToMessage(request *http.Request, captureTime time.Time, body string, messageExtraHeaders *[]headerKeyVal, isHTTP2 bool, requestSenderIp string) httpMessage {
|
|
||||||
messageHeaders := make([]headerKeyVal, 0)
|
|
||||||
|
|
||||||
for key, value := range request.Header {
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: key, Value: value[0]})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isHTTP2 {
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: ":method", Value: request.Method})
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: ":path", Value: request.RequestURI})
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: ":authority", Value: request.Host})
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: ":scheme", Value: "http"})
|
|
||||||
}
|
|
||||||
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: "x-request-start", Value: fmt.Sprintf("%.3f", float64(captureTime.UnixNano()) / float64(1000000000))})
|
|
||||||
|
|
||||||
messageHeaders = append(messageHeaders, *messageExtraHeaders...)
|
|
||||||
|
|
||||||
httpVersion := request.Proto
|
|
||||||
|
|
||||||
requestBody := messageBody{Truncated: false, AsBytes: body}
|
|
||||||
|
|
||||||
return httpMessage{
|
|
||||||
IsRequest: true,
|
|
||||||
Headers: messageHeaders,
|
|
||||||
HTTPVersion: httpVersion,
|
|
||||||
Body: requestBody,
|
|
||||||
captureTime: captureTime,
|
|
||||||
orig: request,
|
|
||||||
requestSenderIp: requestSenderIp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func responseToMessage(response *http.Response, captureTime time.Time, body string, isHTTP2 bool) httpMessage {
|
|
||||||
messageHeaders := make([]headerKeyVal, 0)
|
|
||||||
|
|
||||||
for key, value := range response.Header {
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: key, Value: value[0]})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isHTTP2 {
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: ":status", Value: strconv.Itoa(response.StatusCode)})
|
|
||||||
}
|
|
||||||
|
|
||||||
httpVersion := response.Proto
|
|
||||||
|
|
||||||
requestBody := messageBody{Truncated: false, AsBytes: body}
|
|
||||||
|
|
||||||
return httpMessage{
|
|
||||||
IsRequest: false,
|
|
||||||
Headers: messageHeaders,
|
|
||||||
HTTPVersion: httpVersion,
|
|
||||||
Body: requestBody,
|
|
||||||
captureTime: captureTime,
|
|
||||||
orig: response,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matcher *requestResponseMatcher) addDuration(requestHTTPMessage *httpMessage, responseHTTPMessage *httpMessage) {
|
|
||||||
durationMs := float64(responseHTTPMessage.captureTime.UnixNano() / 1000000) - float64(requestHTTPMessage.captureTime.UnixNano() / 1000000)
|
|
||||||
if durationMs < 1 {
|
|
||||||
durationMs = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
responseHTTPMessage.Headers = append(responseHTTPMessage.Headers, headerKeyVal{Key: "x-up9-duration-ms", Value: fmt.Sprintf("%.0f", durationMs)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitIdent(ident string) []string {
|
|
||||||
ident = strings.Replace(ident, "->", " ", -1)
|
|
||||||
return strings.Split(ident, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func genKey(split []string) string {
|
|
||||||
key := fmt.Sprintf("%s:%s->%s:%s,%s", split[0], split[2], split[1], split[3], split[4])
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matcher *requestResponseMatcher) deleteOlderThan(t time.Time) int {
|
|
||||||
keysToPop := make([]string, 0)
|
|
||||||
for item := range matcher.openMessagesMap.IterBuffered() {
|
|
||||||
// Map only contains values of type httpMessage
|
|
||||||
message, _ := item.Val.(*httpMessage)
|
|
||||||
|
|
||||||
if message.captureTime.Before(t) {
|
|
||||||
keysToPop = append(keysToPop, item.Key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
numDeleted := len(keysToPop)
|
|
||||||
|
|
||||||
for _, key := range keysToPop {
|
|
||||||
_, _ = matcher.openMessagesMap.Pop(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return numDeleted
|
|
||||||
}
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
b64 "encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type httpReaderDataMsg struct {
|
|
||||||
bytes []byte
|
|
||||||
timestamp time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type tcpID struct {
|
|
||||||
srcIP string
|
|
||||||
dstIP string
|
|
||||||
srcPort string
|
|
||||||
dstPort string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tid *tcpID) String() string {
|
|
||||||
return fmt.Sprintf("%s->%s %s->%s", tid.srcIP, tid.dstIP, tid.srcPort, tid.dstPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* httpReader gets reads from a channel of bytes of tcp payload, and parses it into HTTP/1 requests and responses.
|
|
||||||
* The payload is written to the channel by a tcpStream object that is dedicated to one tcp connection.
|
|
||||||
* An httpReader object is unidirectional: it parses either a client stream or a server stream.
|
|
||||||
* Implements io.Reader interface (Read)
|
|
||||||
*/
|
|
||||||
type httpReader struct {
|
|
||||||
ident string
|
|
||||||
tcpID tcpID
|
|
||||||
isClient bool
|
|
||||||
isHTTP2 bool
|
|
||||||
msgQueue chan httpReaderDataMsg // Channel of captured reassembled tcp payload
|
|
||||||
data []byte
|
|
||||||
captureTime time.Time
|
|
||||||
hexdump bool
|
|
||||||
parent *tcpStream
|
|
||||||
grpcAssembler GrpcAssembler
|
|
||||||
messageCount uint
|
|
||||||
harWriter *HarWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpReader) Read(p []byte) (int, error) {
|
|
||||||
var msg httpReaderDataMsg
|
|
||||||
ok := true
|
|
||||||
for ok && len(h.data) == 0 {
|
|
||||||
msg, ok = <-h.msgQueue
|
|
||||||
h.data = msg.bytes
|
|
||||||
h.captureTime = msg.timestamp
|
|
||||||
}
|
|
||||||
if !ok || len(h.data) == 0 {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
l := copy(p, h.data)
|
|
||||||
h.data = h.data[l:]
|
|
||||||
return l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpReader) run(wg *sync.WaitGroup) {
|
|
||||||
defer wg.Done()
|
|
||||||
b := bufio.NewReader(h)
|
|
||||||
|
|
||||||
if isHTTP2, err := checkIsHTTP2Connection(b, h.isClient); err != nil {
|
|
||||||
SilentError("HTTP/2-Prepare-Connection", "stream %s Failed to check if client is HTTP/2: %s (%v,%+v)\n", h.ident, err, err, err)
|
|
||||||
// Do something?
|
|
||||||
} else {
|
|
||||||
h.isHTTP2 = isHTTP2
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.isHTTP2 {
|
|
||||||
err := prepareHTTP2Connection(b, h.isClient)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP/2-Prepare-Connection-After-Check", "stream %s error: %s (%v,%+v)\n", h.ident, err, err, err)
|
|
||||||
}
|
|
||||||
h.grpcAssembler = createGrpcAssembler(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
for true {
|
|
||||||
if h.isHTTP2 {
|
|
||||||
err := h.handleHTTP2Stream()
|
|
||||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
SilentError("HTTP/2", "stream %s error: %s (%v,%+v)\n", h.ident, err, err, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if h.isClient {
|
|
||||||
err := h.handleHTTP1ClientStream(b)
|
|
||||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
SilentError("HTTP-request", "stream %s Request error: %s (%v,%+v)\n", h.ident, err, err, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := h.handleHTTP1ServerStream(b)
|
|
||||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
SilentError("HTTP-response", "stream %s Response error: %s (%v,%+v)\n", h.ident, err, err, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpReader) handleHTTP2Stream() error {
|
|
||||||
streamID, messageHTTP1, body, err := h.grpcAssembler.readMessage()
|
|
||||||
h.messageCount++
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var reqResPair *envoyMessageWrapper
|
|
||||||
|
|
||||||
switch messageHTTP1 := messageHTTP1.(type) {
|
|
||||||
case http.Request:
|
|
||||||
ident := fmt.Sprintf("%s->%s %s->%s %d", h.tcpID.srcIP, h.tcpID.dstIP, h.tcpID.srcPort, h.tcpID.dstPort, streamID)
|
|
||||||
reqResPair = reqResMatcher.registerRequest(ident, &messageHTTP1, h.captureTime, body, true)
|
|
||||||
case http.Response:
|
|
||||||
ident := fmt.Sprintf("%s->%s %s->%s %d", h.tcpID.dstIP, h.tcpID.srcIP, h.tcpID.dstPort, h.tcpID.srcPort, streamID)
|
|
||||||
reqResPair = reqResMatcher.registerResponse(ident, &messageHTTP1, h.captureTime, body, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqResPair != nil {
|
|
||||||
if h.harWriter != nil {
|
|
||||||
h.harWriter.WritePair(
|
|
||||||
reqResPair.HttpBufferedTrace.Request.orig.(*http.Request),
|
|
||||||
reqResPair.HttpBufferedTrace.Request.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Response.orig.(*http.Response),
|
|
||||||
reqResPair.HttpBufferedTrace.Response.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Request.requestSenderIp,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
jsonStr, err := json.Marshal(reqResPair)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
broadcastReqResPair(jsonStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpReader) handleHTTP1ClientStream(b *bufio.Reader) error {
|
|
||||||
req, err := http.ReadRequest(b)
|
|
||||||
h.messageCount++
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(req.Body)
|
|
||||||
req.Body = io.NopCloser(bytes.NewBuffer(body)) // rewind
|
|
||||||
s := len(body)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-request-body", "stream %s Got body err: %s\n", h.ident, err)
|
|
||||||
} else if h.hexdump {
|
|
||||||
Info("Body(%d/0x%x)\n%s\n", len(body), len(body), hex.Dump(body))
|
|
||||||
}
|
|
||||||
if err := req.Body.Close(); err != nil {
|
|
||||||
SilentError("HTTP-request-body-close", "stream %s Failed to close request body: %s\n", h.ident, err)
|
|
||||||
}
|
|
||||||
encoding := req.Header["Content-Encoding"]
|
|
||||||
bodyStr, err := readBody(body, encoding)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-request-body-decode", "stream %s Failed to decode body: %s\n", h.ident, err)
|
|
||||||
}
|
|
||||||
Info("HTTP/%s Request: %s %s (Body:%d)\n", h.ident, req.Method, req.URL, s)
|
|
||||||
|
|
||||||
ident := fmt.Sprintf("%s->%s %s->%s %d", h.tcpID.srcIP, h.tcpID.dstIP, h.tcpID.srcPort, h.tcpID.dstPort, h.messageCount)
|
|
||||||
reqResPair := reqResMatcher.registerRequest(ident, req, h.captureTime, bodyStr, false)
|
|
||||||
if reqResPair != nil {
|
|
||||||
if h.harWriter != nil {
|
|
||||||
h.harWriter.WritePair(
|
|
||||||
reqResPair.HttpBufferedTrace.Request.orig.(*http.Request),
|
|
||||||
reqResPair.HttpBufferedTrace.Request.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Response.orig.(*http.Response),
|
|
||||||
reqResPair.HttpBufferedTrace.Response.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Request.requestSenderIp,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
jsonStr, err := json.Marshal(reqResPair)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-marshal", "stream %s Error convert request response to json: %s\n", h.ident, err)
|
|
||||||
}
|
|
||||||
broadcastReqResPair(jsonStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h.parent.Lock()
|
|
||||||
h.parent.urls = append(h.parent.urls, req.URL.String())
|
|
||||||
h.parent.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpReader) handleHTTP1ServerStream(b *bufio.Reader) error {
|
|
||||||
res, err := http.ReadResponse(b, nil)
|
|
||||||
h.messageCount++
|
|
||||||
var req string
|
|
||||||
h.parent.Lock()
|
|
||||||
if len(h.parent.urls) == 0 {
|
|
||||||
req = fmt.Sprintf("<no-request-seen>")
|
|
||||||
} else {
|
|
||||||
req, h.parent.urls = h.parent.urls[0], h.parent.urls[1:]
|
|
||||||
}
|
|
||||||
h.parent.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
res.Body = io.NopCloser(bytes.NewBuffer(body)) // rewind
|
|
||||||
s := len(body)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-response-body", "HTTP/%s: failed to get body(parsed len:%d): %s\n", h.ident, s, err)
|
|
||||||
}
|
|
||||||
if h.hexdump {
|
|
||||||
Info("Body(%d/0x%x)\n%s\n", len(body), len(body), hex.Dump(body))
|
|
||||||
}
|
|
||||||
if err := res.Body.Close(); err != nil {
|
|
||||||
SilentError("HTTP-response-body-close", "HTTP/%s: failed to close body(parsed len:%d): %s\n", h.ident, s, err)
|
|
||||||
}
|
|
||||||
sym := ","
|
|
||||||
if res.ContentLength > 0 && res.ContentLength != int64(s) {
|
|
||||||
sym = "!="
|
|
||||||
}
|
|
||||||
contentType, ok := res.Header["Content-Type"]
|
|
||||||
if !ok {
|
|
||||||
contentType = []string{http.DetectContentType(body)}
|
|
||||||
}
|
|
||||||
encoding := res.Header["Content-Encoding"]
|
|
||||||
Info("HTTP/%s Response: %s URL:%s (%d%s%d%s) -> %s\n", h.ident, res.Status, req, res.ContentLength, sym, s, contentType, encoding)
|
|
||||||
bodyStr, err := readBody(body, encoding)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-response-body-decode", "stream %s Failed to decode body: %s\n", h.ident, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ident := fmt.Sprintf("%s->%s %s->%s %d", h.tcpID.dstIP, h.tcpID.srcIP, h.tcpID.dstPort, h.tcpID.srcPort, h.messageCount)
|
|
||||||
reqResPair := reqResMatcher.registerResponse(ident, res, h.captureTime, bodyStr, false)
|
|
||||||
if reqResPair != nil {
|
|
||||||
if h.harWriter != nil {
|
|
||||||
h.harWriter.WritePair(
|
|
||||||
reqResPair.HttpBufferedTrace.Request.orig.(*http.Request),
|
|
||||||
reqResPair.HttpBufferedTrace.Request.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Response.orig.(*http.Response),
|
|
||||||
reqResPair.HttpBufferedTrace.Response.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Request.requestSenderIp,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
jsonStr, err := json.Marshal(reqResPair)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-marshal", "stream %s Error convert request response to json: %s\n", h.ident, err)
|
|
||||||
}
|
|
||||||
broadcastReqResPair(jsonStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBody(bodyBytes []byte, encoding []string) (string, error) {
|
|
||||||
var bodyBuffer io.Reader
|
|
||||||
bodyBuffer = bytes.NewBuffer(bodyBytes)
|
|
||||||
var err error
|
|
||||||
if len(encoding) > 0 && (encoding[0] == "gzip" || encoding[0] == "deflate") {
|
|
||||||
bodyBuffer, err = gzip.NewReader(bodyBuffer)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-gunzip", "Failed to gzip decode: %s\n", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, ok := bodyBuffer.(*gzip.Reader); ok {
|
|
||||||
err = bodyBuffer.(*gzip.Reader).Close()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
_, err = buf.ReadFrom(bodyBuffer)
|
|
||||||
return b64.StdEncoding.EncodeToString(buf.Bytes()), err
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppStats struct {
|
|
||||||
matchedMessages int
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatsTracker struct {
|
|
||||||
stats AppStats
|
|
||||||
statsMutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (st *StatsTracker) incMatchedMessages() {
|
|
||||||
st.statsMutex.Lock()
|
|
||||||
st.stats.matchedMessages++
|
|
||||||
st.statsMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (st *StatsTracker) dumpStats() AppStats {
|
|
||||||
st.statsMutex.Lock()
|
|
||||||
|
|
||||||
stats := AppStats{
|
|
||||||
matchedMessages: st.stats.matchedMessages,
|
|
||||||
}
|
|
||||||
|
|
||||||
st.stats.matchedMessages = 0
|
|
||||||
|
|
||||||
st.statsMutex.Unlock()
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/patrickmn/go-cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Time allowed to write a message to the peer.
|
|
||||||
writeWait = 10 * time.Second
|
|
||||||
|
|
||||||
// Time allowed to read the next pong message from the peer.
|
|
||||||
pongWait = 60 * time.Second
|
|
||||||
|
|
||||||
// Send pings to peer with this period. Must be less than pongWait.
|
|
||||||
pingPeriod = (pongWait * 9) / 10
|
|
||||||
|
|
||||||
// Maximum message size allowed from peer.
|
|
||||||
maxMessageSize = 512
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
newline = []byte{'\n'}
|
|
||||||
space = []byte{' '}
|
|
||||||
hub *Hub
|
|
||||||
outboundSocketNotifyExpiringCache = cache.New(outboundThrottleCacheExpiryPeriod, outboundThrottleCacheExpiryPeriod)
|
|
||||||
)
|
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func (_ *http.Request) bool { return true },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client is a middleman between the websocket connection and the hub.
|
|
||||||
type Client struct {
|
|
||||||
hub *Hub
|
|
||||||
|
|
||||||
// The websocket connection.
|
|
||||||
conn *websocket.Conn
|
|
||||||
|
|
||||||
// Buffered channel of outbound messages.
|
|
||||||
send chan []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutBoundLinkMessage struct {
|
|
||||||
SourceIP string `json:"sourceIP"`
|
|
||||||
IP string `json:"ip"`
|
|
||||||
Port int `json:"port"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// readPump pumps messages from the websocket connection to the hub.
|
|
||||||
//
|
|
||||||
// The application runs readPump in a per-connection goroutine. The application
|
|
||||||
// ensures that there is at most one reader on a connection by executing all
|
|
||||||
// reads from this goroutine.
|
|
||||||
func (c *Client) readPump() {
|
|
||||||
defer func() {
|
|
||||||
c.hub.unregister <- c
|
|
||||||
c.conn.Close()
|
|
||||||
}()
|
|
||||||
c.conn.SetReadLimit(maxMessageSize)
|
|
||||||
c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
|
||||||
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
|
||||||
for {
|
|
||||||
_, message, err := c.conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
|
||||||
log.Printf("error: %v", err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
|
|
||||||
c.hub.onMessageCallback(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// writePump pumps messages from the hub to the websocket connection.
|
|
||||||
//
|
|
||||||
// A goroutine running writePump is started for each connection. The
|
|
||||||
// application ensures that there is at most one writer to a connection by
|
|
||||||
// executing all writes from this goroutine.
|
|
||||||
func (c *Client) writePump() {
|
|
||||||
ticker := time.NewTicker(pingPeriod)
|
|
||||||
defer func() {
|
|
||||||
ticker.Stop()
|
|
||||||
c.conn.Close()
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case message, ok := <-c.send:
|
|
||||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
if !ok {
|
|
||||||
// The hub closed the channel.
|
|
||||||
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w, err := c.conn.NextWriter(websocket.TextMessage)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(message)
|
|
||||||
|
|
||||||
|
|
||||||
if err := w.Close(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-ticker.C:
|
|
||||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Hub struct {
|
|
||||||
// Registered clients.
|
|
||||||
clients map[*Client]bool
|
|
||||||
|
|
||||||
// Inbound messages from the clients.
|
|
||||||
broadcast chan []byte
|
|
||||||
|
|
||||||
// Register requests from the clients.
|
|
||||||
register chan *Client
|
|
||||||
|
|
||||||
// Unregister requests from clients.
|
|
||||||
unregister chan *Client
|
|
||||||
|
|
||||||
// Handle messages from client
|
|
||||||
onMessageCallback func([]byte)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHub(onMessageCallback func([]byte)) *Hub {
|
|
||||||
return &Hub{
|
|
||||||
broadcast: make(chan []byte),
|
|
||||||
register: make(chan *Client),
|
|
||||||
unregister: make(chan *Client),
|
|
||||||
clients: make(map[*Client]bool),
|
|
||||||
onMessageCallback: onMessageCallback,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Hub) run() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case client := <-h.register:
|
|
||||||
h.clients[client] = true
|
|
||||||
case client := <-h.unregister:
|
|
||||||
if _, ok := h.clients[client]; ok {
|
|
||||||
delete(h.clients, client)
|
|
||||||
close(client.send)
|
|
||||||
}
|
|
||||||
case message := <-h.broadcast:
|
|
||||||
// matched messages counter is incremented in this thread instead of in multiple http reader
|
|
||||||
// threads in order to reduce contention.
|
|
||||||
statsTracker.incMatchedMessages()
|
|
||||||
|
|
||||||
for client := range h.clients {
|
|
||||||
select {
|
|
||||||
case client.send <- message:
|
|
||||||
default:
|
|
||||||
close(client.send)
|
|
||||||
delete(h.clients, client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// serveWs handles websocket requests from the peer.
|
|
||||||
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
|
|
||||||
client.hub.register <- client
|
|
||||||
|
|
||||||
// Allow collection of memory referenced by the caller by doing all work in
|
|
||||||
// new goroutines.
|
|
||||||
go client.writePump()
|
|
||||||
go client.readPump()
|
|
||||||
}
|
|
||||||
|
|
||||||
func startOutputServer(port string, messageCallback func([]byte)) {
|
|
||||||
hub = newHub(messageCallback)
|
|
||||||
go hub.run()
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
serveWs(hub, w, r)
|
|
||||||
})
|
|
||||||
err := http.ListenAndServe("0.0.0.0:" + port, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Output server error: ", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func broadcastReqResPair(reqResJson []byte) {
|
|
||||||
hub.broadcast <- reqResJson
|
|
||||||
}
|
|
||||||
|
|
||||||
func broadcastOutboundLink(srcIP string, dstIP string, dstPort int) {
|
|
||||||
cacheKey := fmt.Sprintf("%s -> %s:%d", srcIP, dstIP, dstPort)
|
|
||||||
_, isInCache := outboundSocketNotifyExpiringCache.Get(cacheKey)
|
|
||||||
if isInCache {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
outboundSocketNotifyExpiringCache.SetDefault(cacheKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
socketMessage := OutBoundLinkMessage{
|
|
||||||
SourceIP: srcIP,
|
|
||||||
IP: dstIP,
|
|
||||||
Port: dstPort,
|
|
||||||
Type: "outboundSocketDetected",
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonStr, err := json.Marshal(socketMessage)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error marshalling outbound socket detection object: %v", err)
|
|
||||||
} else {
|
|
||||||
hub.broadcast <- jsonStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/layers" // pulls in all layers decoders
|
|
||||||
"github.com/google/gopacket/reassembly"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The TCP factory: returns a new Stream
|
|
||||||
* Implements gopacket.reassembly.StreamFactory interface (New)
|
|
||||||
* Generates a new tcp stream for each new tcp connection. Closes the stream when the connection closes.
|
|
||||||
*/
|
|
||||||
type tcpStreamFactory struct {
|
|
||||||
wg sync.WaitGroup
|
|
||||||
doHTTP bool
|
|
||||||
harWriter *HarWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream {
|
|
||||||
Debug("* NEW: %s %s\n", net, transport)
|
|
||||||
fsmOptions := reassembly.TCPSimpleFSMOptions{
|
|
||||||
SupportMissingEstablishment: *allowmissinginit,
|
|
||||||
}
|
|
||||||
Debug("Current App Ports: %v\n", appPorts)
|
|
||||||
dstIp := net.Dst().String()
|
|
||||||
dstPort := int(tcp.DstPort)
|
|
||||||
|
|
||||||
if factory.shouldNotifyOnOutboundLink(dstIp, dstPort) {
|
|
||||||
broadcastOutboundLink(net.Src().String(), dstIp, dstPort)
|
|
||||||
}
|
|
||||||
isHTTP := factory.shouldTap(dstIp, dstPort)
|
|
||||||
stream := &tcpStream{
|
|
||||||
net: net,
|
|
||||||
transport: transport,
|
|
||||||
isDNS: tcp.SrcPort == 53 || tcp.DstPort == 53,
|
|
||||||
isHTTP: isHTTP && factory.doHTTP,
|
|
||||||
reversed: tcp.SrcPort == 80,
|
|
||||||
tcpstate: reassembly.NewTCPSimpleFSM(fsmOptions),
|
|
||||||
ident: fmt.Sprintf("%s:%s", net, transport),
|
|
||||||
optchecker: reassembly.NewTCPOptionCheck(),
|
|
||||||
}
|
|
||||||
if stream.isHTTP {
|
|
||||||
stream.client = httpReader{
|
|
||||||
msgQueue: make(chan httpReaderDataMsg),
|
|
||||||
ident: fmt.Sprintf("%s %s", net, transport),
|
|
||||||
tcpID: tcpID{
|
|
||||||
srcIP: net.Src().String(),
|
|
||||||
dstIP: net.Dst().String(),
|
|
||||||
srcPort: transport.Src().String(),
|
|
||||||
dstPort: transport.Dst().String(),
|
|
||||||
},
|
|
||||||
hexdump: *hexdump,
|
|
||||||
parent: stream,
|
|
||||||
isClient: true,
|
|
||||||
harWriter: factory.harWriter,
|
|
||||||
}
|
|
||||||
stream.server = httpReader{
|
|
||||||
msgQueue: make(chan httpReaderDataMsg),
|
|
||||||
ident: fmt.Sprintf("%s %s", net.Reverse(), transport.Reverse()),
|
|
||||||
tcpID: tcpID{
|
|
||||||
srcIP: net.Dst().String(),
|
|
||||||
dstIP: net.Src().String(),
|
|
||||||
srcPort: transport.Dst().String(),
|
|
||||||
dstPort: transport.Src().String(),
|
|
||||||
},
|
|
||||||
hexdump: *hexdump,
|
|
||||||
parent: stream,
|
|
||||||
harWriter: factory.harWriter,
|
|
||||||
}
|
|
||||||
factory.wg.Add(2)
|
|
||||||
// Start reading from channels stream.client.bytes and stream.server.bytes
|
|
||||||
go stream.client.run(&factory.wg)
|
|
||||||
go stream.server.run(&factory.wg)
|
|
||||||
}
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *tcpStreamFactory) WaitGoRoutines() {
|
|
||||||
factory.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *tcpStreamFactory) shouldTap(dstIP string, dstPort int) bool {
|
|
||||||
if hostMode {
|
|
||||||
if inArrayString(HostAppAddresses, fmt.Sprintf("%s:%d", dstIP, dstPort)) == true {
|
|
||||||
return true
|
|
||||||
} else if inArrayString(HostAppAddresses, dstIP) == true {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
isTappedPort := dstPort == 80 || (appPorts != nil && (inArrayInt(appPorts, dstPort)))
|
|
||||||
if !isTappedPort {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*anydirection {
|
|
||||||
isDirectedHere := inArrayString(ownIps, dstIP)
|
|
||||||
if !isDirectedHere {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *tcpStreamFactory) shouldNotifyOnOutboundLink(dstIP string, dstPort int) bool {
|
|
||||||
if inArrayInt(remoteOnlyOutboundPorts, dstPort) {
|
|
||||||
isDirectedHere := inArrayString(ownIps, dstIP)
|
|
||||||
return !isDirectedHere && !isPrivateIP(dstIP)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
./mizuagent -i any -hardump -targets ${TAPPED_ADDRESSES}
|
|
||||||
BIN
assets/mizu-example.png
Normal file
BIN
assets/mizu-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 811 KiB |
24
assets/mizu-logo.svg
Normal file
24
assets/mizu-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/mizu-ui.png
Normal file
BIN
assets/mizu-ui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 491 KiB |
BIN
assets/validation-example1.png
Normal file
BIN
assets/validation-example1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
assets/validation-example2.png
Normal file
BIN
assets/validation-example2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
15
build-agent-ci.sh
Executable file
15
build-agent-ci.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GCP_PROJECT=up9-docker-hub
|
||||||
|
REPOSITORY=gcr.io/$GCP_PROJECT
|
||||||
|
SERVER_NAME=mizu
|
||||||
|
GIT_BRANCH=ci
|
||||||
|
|
||||||
|
DOCKER_REPO=$REPOSITORY/$SERVER_NAME/$GIT_BRANCH
|
||||||
|
SEM_VER=${SEM_VER=0.0.0}
|
||||||
|
|
||||||
|
DOCKER_TAGGED_BUILD="$DOCKER_REPO:$SEM_VER"
|
||||||
|
|
||||||
|
echo "building $DOCKER_TAGGED_BUILD"
|
||||||
|
docker build -t ${DOCKER_TAGGED_BUILD} --build-arg SEM_VER=${SEM_VER} --build-arg BUILD_TIMESTAMP=${BUILD_TIMESTAMP} --build-arg GIT_BRANCH=${GIT_BRANCH} --build-arg COMMIT_HASH=${COMMIT_HASH} .
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SERVER_NAME=mizu
|
|
||||||
GCP_PROJECT=up9-docker-hub
|
GCP_PROJECT=up9-docker-hub
|
||||||
REPOSITORY=gcr.io/$GCP_PROJECT
|
REPOSITORY=gcr.io/$GCP_PROJECT
|
||||||
|
SERVER_NAME=mizu
|
||||||
GIT_BRANCH=$(git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]')
|
GIT_BRANCH=$(git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]')
|
||||||
DOCKER_TAGGED_BUILD=$REPOSITORY/$SERVER_NAME/$GIT_BRANCH:latest
|
|
||||||
|
DOCKER_REPO=$REPOSITORY/$SERVER_NAME/$GIT_BRANCH
|
||||||
|
SEM_VER=${SEM_VER=0.0.0}
|
||||||
|
|
||||||
|
DOCKER_TAGGED_BUILDS=("$DOCKER_REPO:latest" "$DOCKER_REPO:$SEM_VER")
|
||||||
|
|
||||||
if [ "$GIT_BRANCH" = 'develop' -o "$GIT_BRANCH" = 'master' -o "$GIT_BRANCH" = 'main' ]
|
if [ "$GIT_BRANCH" = 'develop' -o "$GIT_BRANCH" = 'master' -o "$GIT_BRANCH" = 'main' ]
|
||||||
then
|
then
|
||||||
@@ -13,8 +17,12 @@ then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "building $DOCKER_TAGGED_BUILD"
|
echo "building ${DOCKER_TAGGED_BUILDS[@]}"
|
||||||
docker build -t "$DOCKER_TAGGED_BUILD" .
|
DOCKER_TAGS_ARGS=$(echo ${DOCKER_TAGGED_BUILDS[@]/#/-t }) # "-t FIRST_TAG -t SECOND_TAG ..."
|
||||||
|
docker build $DOCKER_TAGS_ARGS --build-arg SEM_VER=${SEM_VER} --build-arg BUILD_TIMESTAMP=${BUILD_TIMESTAMP} --build-arg GIT_BRANCH=${GIT_BRANCH} --build-arg COMMIT_HASH=${COMMIT_HASH} .
|
||||||
|
|
||||||
echo pushing to "$REPOSITORY"
|
for DOCKER_TAG in "${DOCKER_TAGGED_BUILDS[@]}"
|
||||||
docker push "$DOCKER_TAGGED_BUILD"
|
do
|
||||||
|
echo pushing "$DOCKER_TAG"
|
||||||
|
docker push "$DOCKER_TAG"
|
||||||
|
done
|
||||||
|
|||||||
12
build_extensions.sh
Executable file
12
build_extensions.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
for f in tap/extensions/*; do
|
||||||
|
if [ -d "$f" ]; then
|
||||||
|
extension=$(basename $f) && \
|
||||||
|
cd tap/extensions/${extension} && \
|
||||||
|
go build -buildmode=plugin -o ../${extension}.so . && \
|
||||||
|
cd ../../.. && \
|
||||||
|
mkdir -p agent/build/extensions && \
|
||||||
|
cp tap/extensions/${extension}.so agent/build/extensions
|
||||||
|
fi
|
||||||
|
done
|
||||||
1
cli/.gitignore
vendored
Normal file
1
cli/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bin
|
||||||
14
cli/Makefile
14
cli/Makefile
@@ -3,6 +3,7 @@ COMMIT_HASH=$(shell git rev-parse HEAD)
|
|||||||
GIT_BRANCH=$(shell git branch --show-current | tr '[:upper:]' '[:lower:]')
|
GIT_BRANCH=$(shell git branch --show-current | tr '[:upper:]' '[:lower:]')
|
||||||
GIT_VERSION=$(shell git branch --show-current | tr '[:upper:]' '[:lower:]')
|
GIT_VERSION=$(shell git branch --show-current | tr '[:upper:]' '[:lower:]')
|
||||||
BUILD_TIMESTAMP=$(shell date +%s)
|
BUILD_TIMESTAMP=$(shell date +%s)
|
||||||
|
export SEM_VER?=0.0.0
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
@@ -13,7 +14,7 @@ help: ## This help.
|
|||||||
install:
|
install:
|
||||||
go install mizu.go
|
go install mizu.go
|
||||||
|
|
||||||
build: ## build mizu CLI binary (select platform via GOOS / GOARCH env variables)
|
build: ## Build mizu CLI binary (select platform via GOOS / GOARCH env variables).
|
||||||
go build -ldflags="-X 'github.com/up9inc/mizu/cli/mizu.GitCommitHash=$(COMMIT_HASH)' \
|
go build -ldflags="-X 'github.com/up9inc/mizu/cli/mizu.GitCommitHash=$(COMMIT_HASH)' \
|
||||||
-X 'github.com/up9inc/mizu/cli/mizu.Branch=$(GIT_BRANCH)' \
|
-X 'github.com/up9inc/mizu/cli/mizu.Branch=$(GIT_BRANCH)' \
|
||||||
-X 'github.com/up9inc/mizu/cli/mizu.BuildTimestamp=$(BUILD_TIMESTAMP)' \
|
-X 'github.com/up9inc/mizu/cli/mizu.BuildTimestamp=$(BUILD_TIMESTAMP)' \
|
||||||
@@ -21,20 +22,23 @@ build: ## build mizu CLI binary (select platform via GOOS / GOARCH env variables
|
|||||||
-o bin/mizu_$(SUFFIX) mizu.go
|
-o bin/mizu_$(SUFFIX) mizu.go
|
||||||
(cd bin && shasum -a 256 mizu_${SUFFIX} > mizu_${SUFFIX}.sha256)
|
(cd bin && shasum -a 256 mizu_${SUFFIX} > mizu_${SUFFIX}.sha256)
|
||||||
|
|
||||||
build-all: ## build for all supported platforms
|
build-all: ## Build for all supported platforms.
|
||||||
@echo "Compiling for every OS and Platform"
|
@echo "Compiling for every OS and Platform"
|
||||||
@mkdir -p bin && echo "SHA256 checksums available for compiled binaries \n\nRun \`shasum -a 256 -c mizu_OS_ARCH.sha256\` to verify\n\n" > bin/README.md
|
@mkdir -p bin && sed s/_SEM_VER_/$(SEM_VER)/g README.md.TEMPLATE > bin/README.md
|
||||||
@$(MAKE) build GOOS=darwin GOARCH=amd64
|
@$(MAKE) build GOOS=darwin GOARCH=amd64
|
||||||
@$(MAKE) build GOOS=linux GOARCH=amd64
|
@$(MAKE) build GOOS=linux GOARCH=amd64
|
||||||
|
@# $(MAKE) build GOOS=darwin GOARCH=arm64
|
||||||
@# $(MAKE) GOOS=windows GOARCH=amd64
|
@# $(MAKE) GOOS=windows GOARCH=amd64
|
||||||
@# $(MAKE) GOOS=linux GOARCH=386
|
@# $(MAKE) GOOS=linux GOARCH=386
|
||||||
@# $(MAKE) GOOS=windows GOARCH=386
|
@# $(MAKE) GOOS=windows GOARCH=386
|
||||||
@# $(MAKE) GOOS=darwin GOARCH=arm64
|
|
||||||
@# $(MAKE) GOOS=linux GOARCH=arm64
|
@# $(MAKE) GOOS=linux GOARCH=arm64
|
||||||
@# $(MAKE) GOOS=windows GOARCH=arm64
|
@# $(MAKE) GOOS=windows GOARCH=arm64
|
||||||
@echo "---------"
|
@echo "---------"
|
||||||
@find ./bin -ls
|
@find ./bin -ls
|
||||||
|
|
||||||
clean: ## clean all build artifacts
|
clean: ## Clean all build artifacts.
|
||||||
go clean
|
go clean
|
||||||
rm -rf ./bin/*
|
rm -rf ./bin/*
|
||||||
|
|
||||||
|
test: ## Run cli tests.
|
||||||
|
@go test ./... -coverpkg=./... -race -coverprofile=coverage.out -covermode=atomic
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# mizu CLI
|
|
||||||
## Usage
|
|
||||||
`./mizu {pod_name_regex}`
|
|
||||||
|
|
||||||
### Optional Flags
|
|
||||||
|
|
||||||
| flag | default | purpose |
|
|
||||||
|----------------------|------------------|--------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `--no-gui` | `false` | Don't host the web interface (not applicable at the moment) |
|
|
||||||
| `--gui-port` | `8899` | local port that web interface will be forwarded to |
|
|
||||||
| `--namespace` | | use namespace different than the one found in kubeconfig |
|
|
||||||
| `--kubeconfig` | | Path to custom kubeconfig file |
|
|
||||||
|
|
||||||
There are some extra flags defined in code that will show up in `./mizu --help`, these are non functional stubs for now
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
Make sure your go version is at least 1.11
|
|
||||||
1. cd to `mizu/cli`
|
|
||||||
2. Run `go mod download` (may take a moment)
|
|
||||||
3. Run `go build mizu.go`
|
|
||||||
|
|
||||||
Alternatively, you can build+run directly using `go run mizu.go {pod_name_regex}`
|
|
||||||
|
|
||||||
|
|
||||||
## Known issues
|
|
||||||
* mid-flight port forwarding failures are not detected and no indication will be shown when this occurs
|
|
||||||
20
cli/README.md.TEMPLATE
Normal file
20
cli/README.md.TEMPLATE
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Mizu release _SEM_VER_
|
||||||
|
|
||||||
|
Download Mizu for your platform
|
||||||
|
|
||||||
|
**Mac** (on Intel chip)
|
||||||
|
```
|
||||||
|
curl -Lo mizu https://github.com/up9inc/mizu/releases/download/_SEM_VER_/mizu_darwin_amd64 && chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux**
|
||||||
|
```
|
||||||
|
curl -Lo mizu https://github.com/up9inc/mizu/releases/download/_SEM_VER_/mizu_linux_amd64 && chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Checksums
|
||||||
|
SHA256 checksums available for compiled binaries.
|
||||||
|
Run `shasum -a 256 -c mizu_OS_ARCH.sha256` to verify.
|
||||||
|
|
||||||
|
|
||||||
178
cli/apiserver/provider.go
Normal file
178
cli/apiserver/provider.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package apiserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"io/ioutil"
|
||||||
|
core "k8s.io/api/core/v1"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiServerProvider struct {
|
||||||
|
url string
|
||||||
|
isReady bool
|
||||||
|
retries int
|
||||||
|
}
|
||||||
|
|
||||||
|
var Provider = apiServerProvider{retries: config.GetIntEnvConfig(config.ApiServerRetries, 20)}
|
||||||
|
|
||||||
|
func (provider *apiServerProvider) InitAndTestConnection(url string) error {
|
||||||
|
healthUrl := fmt.Sprintf("%s/", url)
|
||||||
|
retriesLeft := provider.retries
|
||||||
|
for retriesLeft > 0 {
|
||||||
|
if response, err := http.Get(healthUrl); err != nil {
|
||||||
|
logger.Log.Debugf("[ERROR] failed connecting to api server %v", err)
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
responseBody := ""
|
||||||
|
data, readErr := ioutil.ReadAll(response.Body)
|
||||||
|
if readErr == nil {
|
||||||
|
responseBody = string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("can't connect to api server yet, response status code: %v, body: %v", response.StatusCode, responseBody)
|
||||||
|
|
||||||
|
response.Body.Close()
|
||||||
|
} else {
|
||||||
|
logger.Log.Debugf("connection test to api server passed successfully")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
retriesLeft -= 1
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if retriesLeft == 0 {
|
||||||
|
provider.isReady = false
|
||||||
|
return fmt.Errorf("couldn't reach the api server after %v retries", provider.retries)
|
||||||
|
}
|
||||||
|
provider.url = url
|
||||||
|
provider.isReady = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *apiServerProvider) ReportTappedPods(pods []core.Pod) error {
|
||||||
|
if !provider.isReady {
|
||||||
|
return fmt.Errorf("trying to reach api server when not initialized yet")
|
||||||
|
}
|
||||||
|
tappedPodsUrl := fmt.Sprintf("%s/status/tappedPods", provider.url)
|
||||||
|
|
||||||
|
podInfos := make([]shared.PodInfo, 0)
|
||||||
|
for _, pod := range pods {
|
||||||
|
podInfos = append(podInfos, shared.PodInfo{Name: pod.Name, Namespace: pod.Namespace})
|
||||||
|
}
|
||||||
|
tapStatus := shared.TapStatus{Pods: podInfos}
|
||||||
|
|
||||||
|
if jsonValue, err := json.Marshal(tapStatus); err != nil {
|
||||||
|
return fmt.Errorf("failed Marshal the tapped pods %w", err)
|
||||||
|
} else {
|
||||||
|
if response, err := http.Post(tappedPodsUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {
|
||||||
|
return fmt.Errorf("failed sending to API server the tapped pods %w", err)
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("failed sending to API server the tapped pods, response status code %v", response.StatusCode)
|
||||||
|
} else {
|
||||||
|
logger.Log.Debugf("Reported to server API about %d taped pods successfully", len(podInfos))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *apiServerProvider) RequestAnalysis(analysisDestination string, sleepIntervalSec int) error {
|
||||||
|
if !provider.isReady {
|
||||||
|
return fmt.Errorf("trying to reach api server when not initialized yet")
|
||||||
|
}
|
||||||
|
urlPath := fmt.Sprintf("%s/api/uploadEntries?dest=%s&interval=%v", provider.url, url.QueryEscape(analysisDestination), sleepIntervalSec)
|
||||||
|
u, parseErr := url.ParseRequestURI(urlPath)
|
||||||
|
if parseErr != nil {
|
||||||
|
logger.Log.Fatal("Failed parsing the URL (consider changing the analysis dest URL), err: %v", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("Analysis url %v", u.String())
|
||||||
|
if response, requestErr := http.Get(u.String()); requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to notify agent for analysis, err: %w", requestErr)
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("failed to notify agent for analysis, status code: %v", response.StatusCode)
|
||||||
|
} else {
|
||||||
|
logger.Log.Infof(uiUtils.Purple, "Traffic is uploading to UP9 for further analysis")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *apiServerProvider) GetGeneralStats() (map[string]interface{}, error) {
|
||||||
|
if !provider.isReady {
|
||||||
|
return nil, fmt.Errorf("trying to reach api server when not initialized yet")
|
||||||
|
}
|
||||||
|
generalStatsUrl := fmt.Sprintf("%s/api/generalStats", provider.url)
|
||||||
|
|
||||||
|
response, requestErr := http.Get(generalStatsUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get general stats for telemetry, err: %w", requestErr)
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("failed to get general stats for telemetry, status code: %v", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = response.Body.Close() }()
|
||||||
|
|
||||||
|
data, readErr := ioutil.ReadAll(response.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read general stats for telemetry, err: %v", readErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var generalStats map[string]interface{}
|
||||||
|
if parseErr := json.Unmarshal(data, &generalStats); parseErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse general stats for telemetry, err: %v", parseErr)
|
||||||
|
}
|
||||||
|
return generalStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *apiServerProvider) GetHars(fromTimestamp int, toTimestamp int) (*zip.Reader, error) {
|
||||||
|
if !provider.isReady {
|
||||||
|
return nil, fmt.Errorf("trying to reach api server when not initialized yet")
|
||||||
|
}
|
||||||
|
resp, err := http.Get(fmt.Sprintf("%s/api/har?from=%v&to=%v", provider.url, fromTimestamp, toTimestamp))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed getting har from api server %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed reading hars %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed craeting zip reader %w", err)
|
||||||
|
}
|
||||||
|
return zipReader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *apiServerProvider) GetVersion() (string, error) {
|
||||||
|
if !provider.isReady {
|
||||||
|
return "", fmt.Errorf("trying to reach api server when not initialized yet")
|
||||||
|
}
|
||||||
|
versionUrl, _ := url.Parse(fmt.Sprintf("%s/metadata/version", provider.url))
|
||||||
|
req := &http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: versionUrl,
|
||||||
|
}
|
||||||
|
statusResp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer statusResp.Body.Close()
|
||||||
|
|
||||||
|
versionResponse := &shared.VersionResponse{}
|
||||||
|
if err := json.NewDecoder(statusResp.Body).Decode(&versionResponse); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionResponse.SemVer, nil
|
||||||
|
}
|
||||||
70
cli/cmd/common.go
Normal file
70
cli/cmd/common.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/errormessage"
|
||||||
|
"github.com/up9inc/mizu/cli/kubernetes"
|
||||||
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetApiServerUrl() string {
|
||||||
|
return fmt.Sprintf("http://%s", kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.Tap.GuiPort))
|
||||||
|
}
|
||||||
|
|
||||||
|
func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||||
|
err := kubernetes.StartProxy(kubernetesProvider, config.Config.Tap.GuiPort, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error occured while running k8s proxy %v\n"+
|
||||||
|
"Try setting different port by using --%s", errormessage.FormatError(err), configStructs.GuiPortTapName))
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("proxy ended")
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForFinish(ctx context.Context, cancel context.CancelFunc) {
|
||||||
|
logger.Log.Debugf("waiting for finish...")
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
|
|
||||||
|
// block until ctx cancel is called or termination signal is received
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Log.Debugf("ctx done")
|
||||||
|
break
|
||||||
|
case <-sigChan:
|
||||||
|
logger.Log.Debugf("Got termination signal, canceling execution...")
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func openBrowser(url string) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
err = exec.Command("xdg-open", url).Start()
|
||||||
|
case "windows":
|
||||||
|
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||||
|
case "darwin":
|
||||||
|
err = exec.Command("open", url).Start()
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported platform")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
cli/cmd/config.go
Normal file
48
cli/cmd/config.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/creasty/defaults"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configCmd = &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Generate config with default values",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
go telemetry.ReportRun("config", config.Config.Config)
|
||||||
|
|
||||||
|
template, err := config.GetConfigWithDefaults()
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Failed generating config with defaults %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if config.Config.Config.Regenerate {
|
||||||
|
data := []byte(template)
|
||||||
|
if err := ioutil.WriteFile(config.Config.ConfigFilePath, data, 0644); err != nil {
|
||||||
|
logger.Log.Errorf("Failed writing config %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logger.Log.Infof(fmt.Sprintf("Template File written to %s", fmt.Sprintf(uiUtils.Purple, config.Config.ConfigFilePath)))
|
||||||
|
} else {
|
||||||
|
logger.Log.Debugf("Writing template config.\n%v", template)
|
||||||
|
fmt.Printf("%v", template)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(configCmd)
|
||||||
|
|
||||||
|
defaultConfig := config.ConfigStruct{}
|
||||||
|
defaults.Set(&defaultConfig)
|
||||||
|
|
||||||
|
configCmd.Flags().BoolP(configStructs.RegenerateConfigName, "r", defaultConfig.Config.Regenerate, fmt.Sprintf("Regenerate the config file with default values to path %s or to chosen path using --%s", defaultConfig.ConfigFilePath, config.ConfigFilePathCommandName))
|
||||||
|
}
|
||||||
@@ -1,21 +1,34 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/up9inc/mizu/cli/apiserver"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu/version"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MizuFetchOptions struct {
|
|
||||||
Limit uint16
|
|
||||||
Directory string
|
|
||||||
}
|
|
||||||
|
|
||||||
var mizuFetchOptions = MizuFetchOptions{}
|
|
||||||
|
|
||||||
var fetchCmd = &cobra.Command{
|
var fetchCmd = &cobra.Command{
|
||||||
Use: "fetch",
|
Use: "fetch",
|
||||||
Short: "Download recorded traffic to files",
|
Short: "Download recorded traffic to files",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
RunMizuFetch(&mizuFetchOptions)
|
go telemetry.ReportRun("fetch", config.Config.Fetch)
|
||||||
|
|
||||||
|
if err := apiserver.Provider.InitAndTestConnection(GetApiServerUrl()); err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, "Couldn't connect to API server, make sure one running")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isCompatible, err := version.CheckVersionCompatibility(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !isCompatible {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
RunMizuFetch()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -23,6 +36,11 @@ var fetchCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(fetchCmd)
|
rootCmd.AddCommand(fetchCmd)
|
||||||
|
|
||||||
fetchCmd.Flags().Uint16VarP(&mizuFetchOptions.Limit, "limit", "l", 1000, "Provide a custom limit for entries to fetch")
|
defaultFetchConfig := configStructs.FetchConfig{}
|
||||||
fetchCmd.Flags().StringVarP(&mizuFetchOptions.Directory, "directory", "d", ".", "Provide a custom directory for fetched entries")
|
defaults.Set(&defaultFetchConfig)
|
||||||
|
|
||||||
|
fetchCmd.Flags().StringP(configStructs.DirectoryFetchName, "d", defaultFetchConfig.Directory, "Provide a custom directory for fetched entries")
|
||||||
|
fetchCmd.Flags().Int(configStructs.FromTimestampFetchName, defaultFetchConfig.FromTimestamp, "Custom start timestamp for fetched entries")
|
||||||
|
fetchCmd.Flags().Int(configStructs.ToTimestampFetchName, defaultFetchConfig.ToTimestamp, "Custom end timestamp fetched entries")
|
||||||
|
fetchCmd.Flags().Uint16P(configStructs.GuiPortFetchName, "p", defaultFetchConfig.GuiPort, "Provide a custom port for the web interface webserver")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +1,25 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"github.com/up9inc/mizu/cli/apiserver"
|
||||||
"bytes"
|
"github.com/up9inc/mizu/cli/config"
|
||||||
"fmt"
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
"io"
|
"github.com/up9inc/mizu/cli/mizu/fsUtils"
|
||||||
"io/ioutil"
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunMizuFetch(fetch *MizuFetchOptions) {
|
func RunMizuFetch() {
|
||||||
resp, err := http.Get(fmt.Sprintf("http://localhost:8899/api/har?limit=%v", fetch.Limit))
|
if err := apiserver.Provider.InitAndTestConnection(GetApiServerUrl()); err != nil {
|
||||||
if err != nil {
|
logger.Log.Errorf(uiUtils.Error, "Couldn't connect to API server, check logs")
|
||||||
log.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
zipReader, err := apiserver.Provider.GetHars(config.Config.Fetch.FromTimestamp, config.Config.Fetch.ToTimestamp)
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
logger.Log.Errorf("Failed fetch data from API server %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
if err := fsUtils.Unzip(zipReader, config.Config.Fetch.Directory); err != nil {
|
||||||
if err != nil {
|
logger.Log.Debugf("[ERROR] failed unzip %v", err)
|
||||||
log.Fatal(err)
|
|
||||||
}
|
}
|
||||||
_ = Unzip(zipReader, fetch.Directory)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Unzip(reader *zip.Reader, dest string) error {
|
|
||||||
dest, _ = filepath.Abs(dest)
|
|
||||||
_ = os.MkdirAll(dest, os.ModePerm)
|
|
||||||
|
|
||||||
// Closure to address file descriptors issue with all the deferred .Close() methods
|
|
||||||
extractAndWriteFile := func(f *zip.File) error {
|
|
||||||
rc, err := f.Open()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := rc.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
path := filepath.Join(dest, f.Name)
|
|
||||||
|
|
||||||
// Check for ZipSlip (Directory traversal)
|
|
||||||
if !strings.HasPrefix(path, filepath.Clean(dest) + string(os.PathSeparator)) {
|
|
||||||
return fmt.Errorf("illegal file path: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.FileInfo().IsDir() {
|
|
||||||
_ = os.MkdirAll(path, f.Mode())
|
|
||||||
} else {
|
|
||||||
_ = os.MkdirAll(filepath.Dir(path), f.Mode())
|
|
||||||
fmt.Print("writing HAR file [ ", path, " ] .. ")
|
|
||||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Println(" done")
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, err = io.Copy(f, rc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range reader.File {
|
|
||||||
err := extractAndWriteFile(f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
50
cli/cmd/logs.go
Normal file
50
cli/cmd/logs.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/creasty/defaults"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/errormessage"
|
||||||
|
"github.com/up9inc/mizu/cli/kubernetes"
|
||||||
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu/fsUtils"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logsCmd = &cobra.Command{
|
||||||
|
Use: "logs",
|
||||||
|
Short: "Create a zip file with logs for Github issue or troubleshoot",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
go telemetry.ReportRun("logs", config.Config.Logs)
|
||||||
|
|
||||||
|
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath())
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Error(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx, _ := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
if validationErr := config.Config.Logs.Validate(); validationErr != nil {
|
||||||
|
return errormessage.FormatError(validationErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("Using file path %s", config.Config.Logs.FilePath())
|
||||||
|
|
||||||
|
if dumpLogsErr := fsUtils.DumpLogs(kubernetesProvider, ctx, config.Config.Logs.FilePath()); dumpLogsErr != nil {
|
||||||
|
logger.Log.Errorf("Failed dump logs %v", dumpLogsErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(logsCmd)
|
||||||
|
|
||||||
|
defaultLogsConfig := configStructs.LogsConfig{}
|
||||||
|
defaults.Set(&defaultLogsConfig)
|
||||||
|
|
||||||
|
logsCmd.Flags().StringP(configStructs.FileLogsName, "f", defaultLogsConfig.FileStr, "Path for zip file (default current <pwd>\\mizu_logs.zip)")
|
||||||
|
}
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu/fsUtils"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu/version"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -9,10 +18,43 @@ var rootCmd = &cobra.Command{
|
|||||||
Short: "A web traffic viewer for kubernetes",
|
Short: "A web traffic viewer for kubernetes",
|
||||||
Long: `A web traffic viewer for kubernetes
|
Long: `A web traffic viewer for kubernetes
|
||||||
Further info is available at https://github.com/up9inc/mizu`,
|
Further info is available at https://github.com/up9inc/mizu`,
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := config.InitConfig(cmd); err != nil {
|
||||||
|
logger.Log.Fatal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
defaultConfig := config.ConfigStruct{}
|
||||||
|
defaults.Set(&defaultConfig)
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().StringSlice(config.SetCommandName, []string{}, fmt.Sprintf("Override values using --%s", config.SetCommandName))
|
||||||
|
rootCmd.PersistentFlags().String(config.ConfigFilePathCommandName, defaultConfig.ConfigFilePath, fmt.Sprintf("Override config file path using --%s", config.ConfigFilePathCommandName))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNewVersionIfNeeded(versionChan chan string) {
|
||||||
|
select {
|
||||||
|
case versionMsg := <-versionChan:
|
||||||
|
if versionMsg != "" {
|
||||||
|
logger.Log.Infof(uiUtils.Yellow, versionMsg)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
// This is called by main.main(). It only needs to happen once to the tapCmd.
|
// This is called by main.main(). It only needs to happen once to the tapCmd.
|
||||||
func Execute() {
|
func Execute() {
|
||||||
|
if err := fsUtils.EnsureDir(mizu.GetMizuFolderPath()); err != nil {
|
||||||
|
logger.Log.Errorf("Failed to use mizu folder, %v", err)
|
||||||
|
}
|
||||||
|
logger.InitLogger()
|
||||||
|
|
||||||
|
versionChan := make(chan string)
|
||||||
|
defer printNewVersionIfNeeded(versionChan)
|
||||||
|
go version.CheckNewerVersion(versionChan)
|
||||||
|
|
||||||
cobra.CheckErr(rootCmd.Execute())
|
cobra.CheckErr(rootCmd.Execute())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,44 +2,51 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"github.com/up9inc/mizu/cli/config"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
"regexp"
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/up9inc/mizu/cli/errormessage"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MizuTapOptions struct {
|
const analysisMessageToConfirm = `NOTE: running mizu with --analysis flag will upload recorded traffic for further analysis and enriched presentation options.`
|
||||||
GuiPort uint16
|
|
||||||
Namespace string
|
|
||||||
AllNamespaces bool
|
|
||||||
KubeConfigPath string
|
|
||||||
MizuImage string
|
|
||||||
MizuPodPort uint16
|
|
||||||
PlainTextFilterRegexes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var mizuTapOptions = &MizuTapOptions{}
|
|
||||||
|
|
||||||
var tapCmd = &cobra.Command{
|
var tapCmd = &cobra.Command{
|
||||||
Use: "tap [POD REGEX]",
|
Use: "tap [POD REGEX]",
|
||||||
Short: "Record ingoing traffic of a kubernetes pod",
|
Short: "Record ingoing traffic of a kubernetes pod",
|
||||||
Long: `Record the ingoing traffic of a kubernetes pod.
|
Long: `Record the ingoing traffic of a kubernetes pod.
|
||||||
Supported protocols are HTTP and gRPC.`,
|
Supported protocols are HTTP and gRPC.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
go telemetry.ReportRun("tap", config.Config.Tap)
|
||||||
return errors.New("POD REGEX argument is required")
|
RunMizuTap()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 1 {
|
||||||
|
config.Config.Tap.PodRegexStr = args[0]
|
||||||
} else if len(args) > 1 {
|
} else if len(args) > 1 {
|
||||||
return errors.New("unexpected number of arguments")
|
return errors.New("unexpected number of arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
regex, err := regexp.Compile(args[0])
|
if err := config.Config.Tap.Validate(); err != nil {
|
||||||
if err != nil {
|
return errormessage.FormatError(err)
|
||||||
return errors.New(fmt.Sprintf("%s is not a valid regex %s", args[0], err))
|
}
|
||||||
|
|
||||||
|
logger.Log.Infof("Mizu will store up to %s of traffic, old traffic will be cleared once the limit is reached.", config.Config.Tap.HumanMaxEntriesDBSize)
|
||||||
|
|
||||||
|
if config.Config.Tap.Analysis {
|
||||||
|
logger.Log.Infof(analysisMessageToConfirm)
|
||||||
|
if !uiUtils.AskForConfirmation("Would you like to proceed [Y/n]: ") {
|
||||||
|
logger.Log.Infof("You can always run mizu without analysis, aborting")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RunMizuTap(regex, mizuTapOptions)
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -47,11 +54,17 @@ var tapCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(tapCmd)
|
rootCmd.AddCommand(tapCmd)
|
||||||
|
|
||||||
tapCmd.Flags().Uint16VarP(&mizuTapOptions.GuiPort, "gui-port", "p", 8899, "Provide a custom port for the web interface webserver")
|
defaultTapConfig := configStructs.TapConfig{}
|
||||||
tapCmd.Flags().StringVarP(&mizuTapOptions.Namespace, "namespace", "n", "", "Namespace selector")
|
defaults.Set(&defaultTapConfig)
|
||||||
tapCmd.Flags().BoolVarP(&mizuTapOptions.AllNamespaces, "all-namespaces", "A", false, "Tap all namespaces")
|
|
||||||
tapCmd.Flags().StringVarP(&mizuTapOptions.KubeConfigPath, "kube-config", "k", "", "Path to kube-config file")
|
tapCmd.Flags().Uint16P(configStructs.GuiPortTapName, "p", defaultTapConfig.GuiPort, "Provide a custom port for the web interface webserver")
|
||||||
tapCmd.Flags().StringVarP(&mizuTapOptions.MizuImage, "mizu-image", "", fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:latest", mizu.Branch), "Custom image for mizu collector")
|
tapCmd.Flags().StringSliceP(configStructs.NamespacesTapName, "n", defaultTapConfig.Namespaces, "Namespaces selector")
|
||||||
tapCmd.Flags().Uint16VarP(&mizuTapOptions.MizuPodPort, "mizu-port", "", 8899, "Port which mizu cli will attempt to forward from the mizu collector pod")
|
tapCmd.Flags().Bool(configStructs.AnalysisTapName, defaultTapConfig.Analysis, "Uploads traffic to UP9 for further analysis (Beta)")
|
||||||
tapCmd.Flags().StringArrayVarP(&mizuTapOptions.PlainTextFilterRegexes, "regex-masking", "r", nil, "List of regex expressions that are used to filter matching values from text/plain http bodies")
|
tapCmd.Flags().BoolP(configStructs.AllNamespacesTapName, "A", defaultTapConfig.AllNamespaces, "Tap all namespaces")
|
||||||
|
tapCmd.Flags().StringSliceP(configStructs.PlainTextFilterRegexesTapName, "r", defaultTapConfig.PlainTextFilterRegexes, "List of regex expressions that are used to filter matching values from text/plain http bodies")
|
||||||
|
tapCmd.Flags().Bool(configStructs.DisableRedactionTapName, defaultTapConfig.DisableRedaction, "Disables redaction of potentially sensitive request/response headers and body values")
|
||||||
|
tapCmd.Flags().String(configStructs.HumanMaxEntriesDBSizeTapName, defaultTapConfig.HumanMaxEntriesDBSize, "Override the default max entries db size")
|
||||||
|
tapCmd.Flags().String(configStructs.DirectionTapName, defaultTapConfig.Direction, "Record traffic that goes in this direction (relative to the tapped pod): in/any")
|
||||||
|
tapCmd.Flags().Bool(configStructs.DryRunTapName, defaultTapConfig.DryRun, "Preview of all pods matching the regex, without tapping them")
|
||||||
|
tapCmd.Flags().String(configStructs.EnforcePolicyFile, defaultTapConfig.EnforcePolicyFile, "Yaml file with policy rules")
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user